前回、前々回の続きです。
これがやりたくてやってました。
画像をPOSTすると、その画像に含まれる顔が検出されて、ログに出力されます。 重要なのは、顔検出を行なっているのがWebアプリケーションではなく、envoy proxyのwasmのfilterだということです。
リポジトリは前回と同じで、諸々はproxy-wasm
のディレクトリに入っています。
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-onnx
と wasabi
は前回と同様に、顔検出を動かすためのものです。
envoy-sdk
は Rustで envoy のwasm filterを書くためのSDKです。
GWに見た時はdeprecateじゃなかったのですが、この記事を書いているときに見たらarchiveされてました。
今はproxy-wasmを使うべきらしいです。
jpeg-decoder は JPEGの画像をdecodeするためのものです。 GWの時点ではjpeg-decoderの最新リリースバージョンにまだ以下の変更が取り込まれておらず、wasmでこのcrateが使えなかったのでgitのmasterを指定しています。
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.wasm
の http_filters
では、envoy.wasm.runtime.v8
で /etc/envoy/proxy_wasm.wasm
のwasmを動かすようにしています。
docker-compose
docker-compose.yaml
は以下のようになっており、app
とenvoy
を起動します。
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 の使い所として、独自メトリクスの追加がよく聞く話です。
例えば今回の画像に含まれる情報のような、メトリクスを取るのが難しいようなものを機械学習モデルを使ってエイヤとやるようなところで有用なんじゃないかと思います。