はやし雑記

はやしです

顔検出をenvoyのproxy-wasmで動かした

前回、前々回の続きです。

blog.hayashikun.com

blog.hayashikun.com

これがやりたくてやってました。

画像をPOSTすると、その画像に含まれる顔が検出されて、ログに出力されます。 重要なのは、顔検出を行なっているのがWebアプリケーションではなく、envoy proxyのwasmのfilterだということです。

リポジトリは前回と同じで、諸々はproxy-wasmのディレクトリに入っています。

github.com

appはpythonのwebサーバーで、データのサイズを返すだけです。

docker-compose up をすれば、appとenvoyが立ち上がります。

app

appはこれだけのシンプルなweb serverです。

import flask

app = flask.Flask(__name__)


@app.route('/', methods=['POST'])
def index():
    data = flask.request.data
    return f"size:  {len(data)}"


app.run(host='0.0.0.0', port=8000)

依存関係

Cargo.toml で指定している依存関係は以下の通りです。

[dependencies]
tract-onnx = "0.16.5"
wasabi = { path = "..", default-features = false }
envoy-sdk = "0.2.0-alpha.1"
jpeg-decoder = { git = "https://github.com/image-rs/jpeg-decoder", branch = "master", default-features = false }

tract-onnxwasabi は前回と同様に、顔検出を動かすためのものです。

envoy-sdk は Rustで envoy のwasm filterを書くためのSDKです。 GWに見た時はdeprecateじゃなかったのですが、この記事を書いているときに見たらarchiveされてました。

github.com

Deprecates this crate in favor of proxy-wasm by codefromthecrypt · Pull Request #71 · tetratelabs/envoy-wasm-rust-sdk · GitHub

今はproxy-wasmを使うべきらしいです。

github.com

jpeg-decoder は JPEGの画像をdecodeするためのものです。 GWの時点ではjpeg-decoderの最新リリースバージョンにまだ以下の変更が取り込まれておらず、wasmでこのcrateが使えなかったのでgitのmasterを指定しています。

Fixing issue with worker on wasm targets by gents83 · Pull Request #236 · image-rs/jpeg-decoder · GitHub

jpeg-decoder は 前々回でも使用していたimage crateにも含まれているものです。

envoy filter Rust実装

以下が今回実装したfilterの主要部分です。

リサイズするのが面倒だったので、対応している画像サイズは640x480のみです。

impl WasabiHttpFilter {
    pub fn new() -> Self {
        WasabiHttpFilter {
            cf: CenterFace::new(640, 480).unwrap(),
            req_body: vec![],
        }
    }
}

impl HttpFilter for WasabiHttpFilter {
    fn on_request_body(
        &mut self,
        data_size: usize,
        end_of_stream: bool,
        ops: &dyn RequestBodyOps,
    ) -> Result<FilterDataStatus> {
        let bs = ops.request_data(0, data_size)?;
        self.req_body.append(&mut bs.to_vec());

        if !end_of_stream {
            return Ok(FilterDataStatus::Continue);
        }

        let bytes = self.req_body.as_slice();

        let mut decoder = Decoder::new(Cursor::new(bytes));

        let pixels = decoder.decode()?;
        log::info!("pixels: {:?}", pixels.len());

        let width = 640;
        let height = 480;

        let image: Tensor = Array4::from_shape_fn(
            (1, 3, height, width),
            |(_, c, y, x)| {
                pixels[(y * width + x) * 3 + c] as f32
            },
        ).into();
        let faces: Vec<Face> = self.cf.detect(image).unwrap();
        log::info!("{} faces found!", faces.len());

        self.req_body = vec![];
        Ok(FilterDataStatus::Continue)
    }
}

on_request_body はリクエスト時に呼ばれる関数です。

ただし、request bodyが大きいと複数回に分かれて呼ばれるので、 self.req_bodyに入れていって、end_of_stream で判断するということをやっています。

envoy

以下がenvoy.yamlです。

admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901

static_resources:
  listeners:
    - name: ingress
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 10000
      traffic_direction: INBOUND
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: ingress_http
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: app_service
                http_filters:
                  - name: envoy.filters.http.wasm
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                      config:
                        configuration:
                          "@type": type.googleapis.com/google.protobuf.StringValue
                          value: |
                            {"param":"value"}
                        name: wasabi.http_filter
                        root_id: wasabi.http_filter
                        vm_config:
                          vm_id: vm.examples.http_filter
                          runtime: "envoy.wasm.runtime.v8"
                          code:
                            local:
                              filename: /etc/envoy/proxy_wasm.wasm
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
    - name: app_service
      connect_timeout: 0.25s
      type: logical_dns
      lb_policy: round_robin
      load_assignment:
        cluster_name: app_service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: app
                      port_value: 8000

envoy.filters.http.wasmhttp_filters では、envoy.wasm.runtime.v8/etc/envoy/proxy_wasm.wasm のwasmを動かすようにしています。

docker-compose

docker-compose.yaml は以下のようになっており、appenvoyを起動します。

version: "3"

services:
  app:
    build: ./app
    volumes:
      - ./app:/app
      - ./app/tmp:/tmp
    ports:
      - 8000:8000
    expose:
      - 8000

  envoy:
    image: envoyproxy/envoy:v1.22-latest
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml
      - ./target/wasm32-unknown-unknown/release/proxy_wasm.wasm:/etc/envoy/proxy_wasm.wasm
    ports:
      - 10000:10000
      - 10001:10001
    links:
      - app

buildしたwasm ./target/wasm32-unknown-unknown/release/proxy_wasm.wasm は、コンテナの/etc/envoy/proxy_wasm.wasmに置かれます。

Build & Run

cargo build --release -p proxy-wasm --target wasm32-unknown-unknown

とすると、wasmがtarget/wasm32-unknown-unknown/release/proxy_wasm.wasm にできます。

その上で、docker-compose upをするとappとenvoyが起動します。

適当に顔が含まれた画像を640x480にリサイズして、

curl -X POST -H 'Content-Type: image/jpeg' --data-binary @path/to/image.jpg http://localhost:10000

に投げると、docker-composeのenvoyのログに、画像に含まれる顔の数が出てきます。

appに直接投げても同じリクエストが返ってきますが、顔の数はログに出ません。

curl -X POST -H 'Content-Type: image/jpeg' --data-binary @path/to/image.jpg http://localhost:8000

使いどころ

proxy-wasm の使い所として、独自メトリクスの追加がよく聞く話です。

blog.flatt.tech

例えば今回の画像に含まれる情報のような、メトリクスを取るのが難しいようなものを機械学習モデルを使ってエイヤとやるようなところで有用なんじゃないかと思います。