kubernetesでgRPCするときにenvoy挟んでみたよ

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

## 具体例
<img width="603" alt="スクリーンショット 2018-02-08 11.21.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/3553/43b84a5d-1015-2180-697a-d3fdb95ed176.png">

上記のような構成でhoge-gateway(4pod)からhoge-app(10pod)に向けてコネクションプーリングが1で通信をする場合、hoge-appが最大4podしか使われない状態になります。
下記がその状態です。

GKE Container - CPU usage for hoge-app
<img width="723" alt="スクリーンショット 2018-02-08 11.22.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/3553/02c3d6e6-6f2d-49ce-baa2-093021411fbb.png">

GKE Container - CPU usage for hoge-gateway
<img width="710" alt="スクリーンショット 2018-02-08 11.24.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/3553/c21d4dcb-72e6-f382-7610-03bccd318450.png">

## 解決方法
これを解決する手段としてgRPCのclientLoadbalancingを使う方法がありますが、clientに依存する方法はあまりスマートとは言えません。
そこで登場するのが **[envoy](https://www.envoyproxy.io/)** です。

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

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

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

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

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

# envoyの導入
先程の構成にenvoyをサイドカーで組み合わせると下記のようになります。
<img width="573" alt="スクリーンショット 2018-02-08 11.29.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/3553/8f1b4579-d830-337a-0783-2247b1adcdfb.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
<img width="715" alt="スクリーンショット 2018-02-08 11.49.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/3553/6167116d-a4e7-0db5-601d-c8a193be69e4.png">
GKE Container - CPU usage for hoge-gateway
<img width="713" alt="スクリーンショット 2018-02-08 11.50.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/3553/4547cbc1-a0d5-45e2-7acb-0caa854279c5.png">

また、Podの数を5台から10台に途中で切り替えても綺麗に分散されます。
<img width="715" alt="スクリーンショット 2018-02-08 11.50.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/3553/efdbd70f-4b11-6341-e675-51a648b6e5a9.png">

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

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