ツナワタリマイライフ

日常ネタから技術ネタ、音楽ネタまで何でも書きます。

Envoy の Trafic Mirroring による負荷テストを考える

弊社には以前 Shadow Proxy というものがいた。

qiita.com

当時のモチベーションとして Database への Index 追加等をぶつけでなく事前にやりたい、ということだった。現状は Production database の Snapshot から Develop database を毎晩リストアしており、事前にそちらで実行するようにしている。

2020年の今やるなら Envoy かな、と。

で、ここでモチベーションとしてあげている負荷テストとは特に、Database の Migration のことを考えている。

現状 EC2 上に MongoDB をセルフホストしている。そのためインスタンスのスケールアップが必要なときにはいろいろあって1日ほどかかる。マネージドサービスの Atlas にすればスケールアップも容易で早くなるし、パフォーマンスに関する分析も提供してくれるようだ。

この Atlas に本番環境を移行する前に、Prodcution と同様の Query を実行しても Performance に問題がないことを事前に確認できればいい。


最近、一部のサービスで、Rails Application から MongoDB への接続の間に Envoy を経由するようにした。

    admin:
      access_log_path: /dev/stdout
      address:
        socket_address: { address: 127.0.0.1, port_value: 9901 }
    static_resources:
      listeners:
      - name: listener_0
        address:
          socket_address: { address: 0.0.0.0, port_value: 10000 }
        filter_chains:
        - filters:
          - name: envoy.http_connection_manager
            config:
              stat_prefix: ingress_http
              codec_type: AUTO
              route_config:
                name: local_route
                virtual_hosts:
                - name: service_http
                  domains: ["*"]
                  routes:
                  # Envoy admin endpoints
                  - match: { prefix: "/ready" }
                    route: { cluster: envoy_admin }
                  - match: { prefix: "/stats" }
                    route: { cluster: envoy_admin }
              http_filters:
              - name: envoy.router
                config: {}
      - name: mongo_proxy
        address:
          socket_address: { address: 0.0.0.0, port_value: 27017 }
        filter_chains:
        - filters:
          - name: envoy.mongo_proxy
            typed_config:
              "@type": type.googleapis.com/envoy.config.filter.network.mongo_proxy.v2.MongoProxy
              stat_prefix: mongo_proxy
          - name: envoy.tcp_proxy
            typed_config:
              "@type": type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy
              stat_prefix: mongo_tcp_proxy
              cluster: mongo_proxy
      clusters:
      - name: mongo_proxy
        connect_timeout: 0.25s
        type: strict_dns
        lb_policy: round_robin
        load_assignment:
          cluster_name: mongo_proxy
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: mongodb-0
                    port_value: 27017
            - endpoint:
                address:
                  socket_address:
                    address: mongodb-1
                    port_value: 27017
      - name: envoy_admin
        connect_timeout: 0.250s
        type: LOGICAL_DNS
        lb_policy: ROUND_ROBIN
        hosts:
        - socket_address:
            protocol: TCP
            address: 127.0.0.1
            port_value: 9901

MongoProxy filter で metrics をとりつつ、基本的には TCP で proxy しているだけである。そもそも普段、アプリケーションやコマンドラインでは、MongoDB Wired Protocol で MongoDB と通信している。

docs.mongodb.com

Cluster を Load Balancing しているのは Primary と Secondary Node である。

さて、このような状態でどうするか?複数の Cluster を定義し、Mirror したものを別の Cliuster (Atlas) に送り、レスポンスは棄却する、ということがやりたい。

request mirror policies が使えそうだが、これは http だ。

www.envoyproxy.io

残念ながら TCP Layer でこれらは実現できそうにない。

Envoy Slack で "The upstream envoy filter is only for http right now" という回答を得た。やむなし。

とすると http でやるしかない。

こういう例。

blog.markvincze.com

どこでミラーするのかあらためて考える。

f:id:take_she12:20200404104139p:plain

graph TD
  A[Nginx] -->|http://service-a.quipper.com| B[k8s service]
subgraph Kubernetes
  B[Nginx] -->|http://service-a| C[k8s service]
  C -->D[Rails Application Pods]
subgraph Pod
  D -->|mongo://localhost:27017| E[Envoy]
end
end
  E -->|mongo://mongo-fqdn:27017| F[MongoDB]

Front Proxy 相当の動きをしている、Kubernetes 内の Nginx でやるしかあるまい。

その場合、この Nginx を Envoy にすぐに置き換えることは難しいので、これまた Sidecar として Envoy を使うことになるだろう。

こうなる。

f:id:take_she12:20200404105626p:plain

graph TD
  A[Nginx] -->|http://service-a.quipper.com| B[k8s service]
subgraph Kubernetes
subgraph Pod-Nginx
  B[Nginx] -->|http://localhost| C[Envoy]
end
  C[Envoy] -->|http://service-a| D[k8s service a]
  D -->E[Rails Application Pods]
subgraph Pod-Rails-a
  E -->|mongo://localhost:27017| F[Envoy]
end
  C -->|Mirroring| H[k8s service b]
  H -->I[Rails Application Pods]
subgraph Pod-Rails-b
  I -->|mongo://localhost:27017| J[Envoy]
end

end
  F -->|mongo://mongo-fqdn:27017| G[MongoDB]
  J -->|mongo://mongo-fqdn:27017| K[Managed MongoDB]

何が問題になりそうか

  • 外部サービスもすべて同様に用意する必要がある。Redis とか PostgreSQL とか。めんどい。
  • Mirroring をはじめるタイミング。どこかでうんしょと MongoDB のデータを同期した状態からはじめる必要があるが、Production のデータは常に変わりはじめるので、完全に同期はできない。そのためデータ不整合等のエラーが出る可能性は高い。割り切るしかない?

良い点

  • 何より現実のトラフィックと同じものを用意できる点。なんだかんだ自前で負荷テストをやろうとすると Application の知識も必要だし、「これは本当に実際の負荷を再現できているのか?」という永遠に解決しない悩みと向き合い続ける必要がある。それがなくなる点が大きい。
  • Kubernetes Cluster を別に作る必要がない点。単純に Deployment が増えているだけなので、下の Node が Cluster Autoscaler で倍増するだけのはずだ。
  • 段階的に試せる。ミラーリングはパーセンテージを指定できるので、最初は 1% とかで、本番リソース全体を枯渇させる、みたいなリスクを回避できる。

次回は具体的にどういう config でやるのかを試す。