弊社には以前 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:
- 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
どこでミラーするのかあらためて考える。
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 を使うことになるだろう。
こうなる。
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 でやるのかを試す。