gRPCサーバをサーバサイドでロードバランスしようと思う.
なお,この記事はKubernetesを前提にしている. ECSの場合これと同じ方法でうまく行かない気がしているので注意.
gRPCサーバとクライアントを作る
適当にレスポンスを返してくれるgRPCサーバが必要になる. というわけで以前適当に作ったこいつを使う.
gRPCサーバと
https://cloud.docker.com/u/h3poteto/repository/docker/h3poteto/grpc_example-server-python
gRPCクライアントのDocker imageがある.
https://cloud.docker.com/u/h3poteto/repository/docker/h3poteto/grpc_example-client-python
gRPCサーバを動かす
apiVersion: apps/v1 kind: Deployment metadata: name: grpc-server-deployment namespace: envoy-grpc-example labels: app: grpc-server spec: replicas: 2 selector: matchLabels: app: grpc-server strategy: rollingUpdate: maxSurge: 2 maxUnavailable: 1 template: metadata: labels: app: grpc-server spec: volumes: - name: envoy-config configMap: name: server-sidecar-envoy containers: - name: envoy image: envoyproxy/envoy:latest volumeMounts: - name: envoy-config mountPath: /var/opt/envoy command: ["envoy", "-c", "/var/opt/envoy/envoy.yaml"] resources: limits: memory: 512Mi ports: - name: app containerPort: 15001 - name: envoy-admin containerPort: 8001 - name: python image: h3poteto/grpc_example-server-python:master imagePullPolicy: Always ports: - name: grpc containerPort: 50051 protocol: TCP env: - name: SERVER_IP value: 0.0.0.0 - name: SERVER_PORT value: "50051" resources: requests: memory: 200Mi cpu: 500m terminationGracePeriodSeconds: 60
pythonのgRPCサーバは50051ポートでリクエストを受け付ける.
ただし外からのリクエストは一度envoyを通ってからpythonのコンテナに届くことになる. で,envoyの設定だが,これはConfigMapで注入にしている.
というわけでConfigMap.
apiVersion: v1 kind: ConfigMap metadata: name: server-sidecar-envoy namespace: envoy-grpc-example data: envoy.yaml: | admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 8001 } static_resources: listeners: - name: listener_grpc address: socket_address: { address: 0.0.0.0, port_value: 15001 } 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 domains: ["*"] routes: - match: { prefix: "/" } route: { cluster: backend_grpc } http_filters: - name: envoy.router clusters: - name: backend_grpc connect_timeout: 0.25s type: STATIC lb_policy: ROUND_ROBIN http2_protocol_options: {} health_checks: - timeout: 5s interval: 10s unhealthy_threshold: 2 healthy_threshold: 2 tcp_health_check: {} load_assignment: cluster_name: backend_grpc endpoints: lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 50051
envoyは15001ポートで受け付けたリクエストを 127.0.0.1:50051
に流している.
envoyとpythonのgRPCサーバは同じPodに配置している.同一Pod内の通信は,すべて127.0.0.1にバインドされるので,この設定だけでEnvoyが受け付けたリクエストはpythonのgRPCサーバに流れる.
gRPCサーバをアクセス可能な状態にする
Serviceを定義する.ここがKubernetesの良いところで,内部でService Discoveryを持っているとコンテナのIPやポートをいちいちこちらで調べたり管理する必要がない.
apiVersion: v1 kind: Service metadata: name: grpc-server-service namespace: envoy-grpc-example spec: clusterIP: None selector: app: grpc-server ports: - name: grpc port: 15001 targetPort: 15001 protocol: TCP
こうしておくと,このnamespace内では grpc-server-service:15001
でgRPCサーバにアクセスできる.
gRPCクライアントを動かす
今回は外からアクセスするのではなく,クラスタ内からアクセスするだけとする.
というわけでgRPCクライアントのコンテナを立てる.
apiVersion: apps/v1 kind: Deployment metadata: name: grpc-client-deployment namespace: envoy-grpc-example labels: app: grpc-client spec: replicas: 1 selector: matchLabels: app: grpc-client template: metadata: labels: app: grpc-client spec: volumes: - name: envoy-config configMap: name: client-sidecar-envoy containers: - name: envoy image: envoyproxy/envoy:latest volumeMounts: - name: envoy-config mountPath: /var/opt/envoy command: ["envoy", "-c", "/var/opt/envoy/envoy.yaml"] resources: limits: memory: 512Mi ports: - name: app containerPort: 15001 - name: envoy-admin containerPort: 8001 - name: client image: h3poteto/grpc_example-client-python:master imagePullPolicy: Always env: - name: SERVER_IP value: "127.0.0.1" - name: SERVER_PORT value: "9001"
gRPCクライアントは,127.0.0.1:9001にgRPCのリクエストを投げる. ただし,これは同一Pod内のenvoyに拾われる.
で,envoyの設定.
apiVersion: v1 kind: ConfigMap metadata: name: client-sidecar-envoy namespace: envoy-grpc-example data: envoy.yaml: | admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 8001 } static_resources: listeners: - name: listener_grpc address: socket_address: { address: 0.0.0.0, port_value: 9001 } filter_chains: - filters: name: envoy.http_connection_manager config: stat_prefix: egress_http codec_type: AUTO route_config: name: local_route virtual_hosts: - name: grpc-server domains: ["*"] routes: - match: { prefix: "/" } route: { cluster: grpc_server } http_filters: - name: envoy.router clusters: - name: grpc_server connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN http2_protocol_options: {} load_assignment: cluster_name: grpc_server endpoints: lb_endpoints: - endpoint: address: socket_address: address: grpc-server-service port_value: 15001
これは9001に来たアクセスを grpc-server-service:15001
に流す.
これだけでロードバランスできる
envoyの設定は特に動的な設定項目を指定していないが,Headless Serviceのお陰でこれだけでロードバランスできる.
ただ,Kubernetes以外,例えばECSでこれをやろうと思うと,envoyのバックエンドエンドポイントをうまいこと指定するのが結構難しい. 仮にネットワークモードとしてawsvpcを使っていれば,ECS Service DiscoveryでRoute53にコンテナアクセス可能なAレコードを作成してくれるので,そこをエンドポイントに指定することで可能になるかもしれない.