kubernetes(今回はGKE内)でgRPCの通信を場合にぶち当たる問題として、ロードバランシングの問題があります。
kubernetesの内部で使っているLBはL4LBのため、一度張ったコネクションを使い続けてしまいます。つまりロードバランシングされないのです。
そのままの状態で使うとバックエンドにあるサービスがスケールしても分散されないということになります。

具体例

スクリーンショット 2018-02-08 11.21.20.png

上記のような構成でhoge-gateway(4pod)からhoge-app(10pod)に向けて通信をする場合、hoge-appが4podしか使われない状態になります。
下記がその状態です。

GKE Container - CPU usage for hoge-app
スクリーンショット 2018-02-08 11.22.49.png

GKE Container - CPU usage for hoge-gateway
スクリーンショット 2018-02-08 11.24.26.png

解決方法

これを解決する手段としてgRPCのclientLoadbalancingを使う方法がありますが、clientに依存する方法はあまりスマートとは言えません。
そこで登場するのが envoy です。

envoyはCNCFにも参加しているサービスでL7プロキシです。
基本的にはアプリのサイドカーとして使うマイクロサービス向けのミドルウェアとなっています。様々な言語や通信のアプリ間に便利な機能を追加できる便利な存在です。

C++11で書かれていてモダンだったり、CoreはL3/L4 network proxyでできていたり、HTTP L7のフィルターも可能です。
その他バッファリングやサーキットブレーカー、ルーティングなどの機能があります。
当然gRPCもサポートしていて、HTTP/2の全ての通信に対応しています。

//またMongoDBやDynamoDBのプロキシフィルターとしても動かせる。
//サービスディスカバリの機能もあり、非同期DNS分析かRESTベースが可能。

つまり、アプリの改修なしにうまいことバランシングできるというのです。

今回はサービスディスカバリにはkubernetesのheadless servicesを利用します。headless serviceはServiceに対してDNSリクエストをすると稼働しているPodのIPアドレス一覧を返す仕組みです。この仕組をenvoyと組み合わせることでgRPCをうまくバランシングさせようという試みです。

envoyの導入

先程の構成にenvoyをサイドカーで組み合わせると下記のようになります。
スクリーンショット 2018-02-08 11.29.09.png
サイドカーで導入できるメリットは、元のアプリをまったくいじることなく導入できる所です。
まずはhoge-appのアプリにenvoyをサイドカーします。下記がサイドカーするためのDevploymentの例です。

hoge-app-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: hoge-app
spec:
  replicas: 10
  template:
    metadata:
      labels:
        app: hoge-app
    spec:
      volumes:
        - name: envoy
          configMap:
            name: hoge-app-envoy
      containers:
        - name: envoy
          image: envoyproxy/envoy:latest
          command:
            - "/usr/local/bin/envoy"
          args:
            - "--config-path /etc/envoy/envoy.json"
          resources:
            limits:
              memory: 512Mi
          ports:
            - containerPort: 15001
              name: app
            - containerPort: 8001
              name: envoy-admin
          volumeMounts:
            - name: envoy
              mountPath: /etc/envoy
        - name: hoge-app
          image: asia.gcr.io/hoge/hoge-app:latest
          ports:
          - containerPort: 50051

もともとのアプリと並列でport:15001でenvoyが動くような形です。envoyのconfファイルはkubernetesのconfigmapでマウントします。次がenvoyのconfigmapです。

hoge-app-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: hoge-app-envoy
data:
  # Adding new entries here will make them appear as files in the deployment.
  # Please update k8s.io/k8s.io/README.md when you update this file
  envoy.json: |
    {
      "listeners": [
        {
          "address": "tcp://0.0.0.0:15001",
          "filters": [
            {
              "type": "read",
              "name": "http_connection_manager",
              "config": {
                "codec_type": "auto",
                "stat_prefix": "ingress_http",
                "route_config": {
                  "virtual_hosts": [
                    {
                      "name": "service",
                      "domains": ["*"],
                      "routes": [
                        {
                          "timeout_ms": 0,
                          "prefix": "/",
                          "cluster": "local_service"
                        }
                      ]
                    }
                  ]
                },
                "filters": [
                  {
                    "type": "decoder",
                    "name": "router",
                    "config": {}
                  }
                ]
              }
            }
          ]
        }
      ],
      "admin": {
        "access_log_path": "/dev/stdout",
        "address": "tcp://127.0.0.1:8001"
      },
      "cluster_manager": {
        "clusters": [
          {
            "name": "local_service",
            "service_name": "hoge-app.default.svc.cluster.local",
            "connect_timeout_ms": 250,
            "type": "static",
            "lb_type": "round_robin",
            "features": "http2",
            "hosts": [
              {
                "url": "tcp://127.0.0.1:50051"
              }
            ]
          }
        ]
      }
    }

port:15001でingressの通信を受けて、それを元のhoge-appにproxyしてます。
hoge-app側にenvoyを挟むメリットは今の時点ではあまりないのですが、今後マイクロサービスが増えた場合などに便利なので挟んでおくと良さそうです。

次にhoge-gateway側にも同様の作業を行います。
deploymentに関してはhoge-appとほぼ同じなので説明は省略します。configmapをhoge-appと別のものにするところだけ注意して下さい。

envoyのconfigmapがhoge-appと異なるものになります。下記がその例です。

hoge-gateway-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: hoge-gateway-envoy
data:
  # Adding new entries here will make them appear as files in the deployment.
  # Please update k8s.io/k8s.io/README.md when you update this file
  envoy.json: |
    {
      "listeners": [
        {
          "address": "tcp://0.0.0.0:15001",
          "filters": [
            {
              "type": "read",
              "name": "http_connection_manager",
              "config": {
                "codec_type": "auto",
                "stat_prefix": "ingress_http",
                "route_config": {
                  "virtual_hosts": [
                    {
                      "name": "service",
                      "domains": ["*"],
                      "routes": [
                        {
                          "timeout_ms": 0,
                          "prefix": "/",
                          "cluster": "local_service"
                        }
                      ]
                    }
                  ]
                },
                "filters": [
                  {
                    "type": "decoder",
                    "name": "router",
                    "config": {}
                  }
                ]
              }
            }
          ]
        },
        {
          "address": "tcp://127.0.0.1:9001",
          "filters": [
            {
              "type": "read",
              "name": "http_connection_manager",
              "config": {
                "codec_type": "auto",
                "access_log": [
                  {
                    "path": "/dev/stdout"
                  }
                ],
                "stat_prefix": "egress_http",
                "route_config": {
                  "virtual_hosts": [
                    {
                      "name": "hoge-app",
                      "domains": ["*"],
                      "routes": [
                        {
                          "timeout_ms": 0,
                          "prefix": "/",
                          "headers": [
                            {"name": "content-type", "value": "application/grpc"}
                          ],
                          "cluster": "hoge-app"
                        }
                      ]
                    }
                  ]
                },
                "filters": [
                  {
                    "type": "decoder",
                    "name": "router",
                    "config": {}
                  }
                ]
              }
            }
          ]
        }
      ],
      "admin": {
        "access_log_path": "/dev/stdout",
        "address": "tcp://127.0.0.1:8001"
      },
      "cluster_manager": {
        "clusters": [
          {
            "name": "local_service",
            "service_name": "hoge-gateway.default.svc.cluster.local",
            "connect_timeout_ms": 250,
            "type": "static",
            "lb_type": "round_robin",
            "hosts": [
              {
                "url": "tcp://127.0.0.1:8080"
              }
            ]
          },
          {
            "name": "hoge-app",
            "features": "http2",
            "connect_timeout_ms": 250,
            "type": "strict_dns",
            "lb_type": "round_robin",
            "hosts": [{"url": "tcp://hoge-app-service:15001"}]
          }
        ]
      }
    }

port:15001でingressの通信をうけるところは同じですが、tcp://127.0.0.1:9001でegressの通信を受けている所がポイントです。9001で受けたegressの通信はhoge-appクラスタに向かいます。hoge-appクラスタはcluster_managerに示しているようにtcp://hoge-app-service:15001に向かって通信するようになります。しかし、これだけでは結局kubernetesのL4LBを使っている事になるのでバランシングされません
そこで、このtcp://hoge-app-service:15001に通信した時にkubernetesのheadless serviceを利用するように設定する必要があります。
kubernetesのheadless serviceはDNSリクエストを行うと、そのサービスが持つPodのアドレス一覧を返す仕組みです。これを利用することでenvoy側でラウンドロビンサせることができるようになります。

具体的にはhoge-app-serviceの設定を下記のようにします。

hoge-app-service.yaml
  apiVersion: v1
  kind: Service
  metadata:
    name: hoge-app-service
  spec:
#    type: NodePort
    clusterIP: None
    selector:
      app: hoge-app
    ports:
      - name: grpc
        port: 15001
        targetPort: 15001
        protocol: TCP

port:15001に変更しているのと、spec.typeを削除し、spec.clusterIP: Noneを追加しています。これでこのserviceはheadless serviceになります。
kubectl get servicesしてTYPEがClusterIPになっていることが確認できると思います。

hoge-gateway-serviceのPortも15001に変更すれば準備完了です。

結果

実際に通信してみると下記のように綺麗に分散されるのがわかります。
GKE Container - CPU usage for hoge-app
スクリーンショット 2018-02-08 11.49.55.png
GKE Container - CPU usage for hoge-gateway
スクリーンショット 2018-02-08 11.50.23.png

また、Podの数を5台から10台に途中で切り替えても綺麗に分散されます。
スクリーンショット 2018-02-08 11.50.56.png

まとめ

世間はistio、istioと盛り上がってますが、まだまだ実用段階ではない事を考えるとこのenvoyProxyは今GKEでマイクロサービスを構築するには必須のツールとなりそうですね!!

参考

https://cloud.google.com/community/tutorials/envoy-flask-google-container-engine
https://github.com/bakins/kubernetes-envoy-example

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.