Posted at

kubernetesでenvoy使ってgRPCしながら終了時にパケットロストしない方法


概要

kubernetesでmicroservicesアーキテクチャを採用している場合、envoyでgRPC通信するケースが多いかと思います。詳しくはこちらの記事を。

そんなときに瞬断の影響が大きいサービスを運用している場合、アプリをシャットダウンするタイミングで何も考えないで運用しているとパケットをロストします。

これは、serviceから切り離される前にPodが終了してしまうことや、Podに対して終了シグナルが送られた場合に、アプリより先にenvoyが終了しちゃう事などで発生します。

これはサイドカーしている他のコンテナでも同様のことが言えます。


対策

grpcはhttp2の通信なのでコネクションを永続化します。つまりserviceから張られていたコネクションは処理中のものも含めて切断されてしまいます。

それを避けるためには

① serviceから当該Podへの新規のコネクションを止める

② すべての処理が終わるまですべてのコンテナを落とさない

を行う必要があります。

しかしながら②を実現することは困難なので sleep 30 で妥協することにしました。

30秒程度あればすべての処理は終了するだろうという考えです。


事前準備

まず、複数のコンテナ間で状態を共有するためにunhealthyという名前の共有volumeを作成します。

これを各コンテナの /tmp にマウントしておきます。

ここに状態共有のための情報を書き込みます。


appコンテナの挙動

appコンテナはSIGTERMを受け取ると /tmpunhealthy というファイルを作成し30秒sleepします。

同時に unhealthy がある場合は readinessProbe を失敗させ、serviceから切り離します。

envoyはheadless servicesを利用してバランシングしているので、このPodに対して新しいコネクションは張られなくなります。


envoyコンテナの挙動

envoyコンテナはSIGTERMを受け取ると unhealthy ファイルを発見するまでループします。

これでappコンテナが終了するよりも前にenvoyコンテナが終了することを防ぎます。

unhealthy ファイルを発見したら、こちらも同様に30秒sleepします。

30秒後に両コンテナが完全に終了するという流れになります。

具体的には下記のようなyamlになります。

apiVersion: extensions/v1beta1

kind: Deployment
metadata:
name: app
spec:
...
containers:
- name: sidecar-envoy
image: envoyproxy/envoy:v1.7.0
...
volumeMounts:
- name: unhealthy
mountPath: /tmp
readOnly: true
lifecycle:
preStop:
exec:
command:
- sh
- -c
- while (! test -e /tmp/unhealthy); do sleep 1; done; sleep 30
- name: app
image: path/to/app:latest
...
readinessProbe:
exec:
command:
- sh
- -c
- exit `! test -e /tmp/unhealthy; echo $?`
initialDelaySeconds: 5
timeoutSeconds : 3
periodSeconds: 3
failureThreshold: 1
lifecycle:
preStop:
exec:
command:
- sh
- -c
- touch /tmp/unhealthy; sleep 30
volumes:
- name: unhealthy
emptyDir: {}

initialDelaySeconds とか timeoutSeconds あたりは短めにしてるって程度で深い意味はありません!

これでHappy gRPC lifeを!!