istio-proxyが自分以外のコンテナ終了を待たずに終了するのをなんとかする
TL;DR
pod 削除時、istio-proxy と実アプリケーションコンテナの終了は同期されません。そして大抵 istio-proxy のほうが早く終了します。
pod は istio-proxy のプロセス終了後、リクエストを受けると即座に503を返すようになります。
つまり、例えば http を serve するようなアプリケーションだと、graceful shutdown が実装されていてもそれが無効になってしまいます。デプロイ時などには非常に困る挙動です。
何もしなければこの問題は現在最新の istio 1.1.1 でも起こります。詳細は以下の issue などが参考になります。
https://github.com/istio/istio/issues/7136
対処方法としては、実アプリケーションの pod 終了を待つように istio-proxy に preStop を入れるか、VirtualService の retry を使うと良いです。
実験準備
httpbin を使い、リクエスト処理中に pod を delete するとどうなるか実験してみます。
準備として、docker.io/citizenstig/httpbin
を使った Deployment を2つ作り、片方にだけ istio-proxy を注入します。
その2つの Deployment に Ingress と istio の Gateway を紐づけて挙動を見てみます。ここではGKE(ingress-gce)を使っています。
cat <<EOF | kubectl create -f - apiVersion: extensions/v1beta1 kind: Deployment metadata: name: httpbin-1 namespace: httpbin spec: replicas: 1 template: metadata: labels: app: httpbin-1 spec: containers: - image: docker.io/citizenstig/httpbin imagePullPolicy: IfNotPresent name: httpbin ports: - containerPort: 8000 EOF
はい
kubectl expose deployment httpbin-1 --type LoadBalancer --name httpbin-svc-1 -n httpbin
同じ感じで istio-proxy を持ったものを作ります
cat <<EOF | istioctl kube-inject -f - | kubectl apply -f - apiVersion: extensions/v1beta1 kind: Deployment metadata: name: httpbin-2 namespace: httpbin spec: replicas: 1 template: metadata: labels: app: httpbin-2 spec: containers: - image: docker.io/citizenstig/httpbin imagePullPolicy: IfNotPresent name: httpbin ports: - containerPort: 8000 EOF
httpbin-2 に istio の gateway を割り当てたものを作ります
kubectl expose deployment httpbin-2 --type ClusterIP --name httpbin-svc-2-ci -n httpbin
cat <<EOF | kubectl apply -f - apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: httpbin-gateway namespace: httpbin spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - "*" --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: httpbin-vs namespace: httpbin spec: hosts: - "*" gateways: - httpbin-gateway http: - match: route: - destination: host: httpbin-svc-2-ci port: number: 8000 EOF
一応整理しておくとこんな感じです
名前 | 種類 | 接続先 |
---|---|---|
httpbin-svc-1 | Ingress | httpbin-1 (istio-proxyなし) |
istio-ingressgateway | Gateway | httpbin-2 (istio-proxyあり) |
実験
httpbin には /delay
エンドポイントがあります。 /delay/10
のようにすれば10秒間サーバー側で sleep してからレスポンスを返してくれます。
ちなみに、sleep の値はデフォルトで2秒、最大10秒までしか待てないので注意です。
/delay
を使って、ここまでに作った pod に対して、curl でリクエストしつつ裏で pod を delete するという操作をしてみます。
httpbin には graceful shutdown が実装されているため、delay 中でサーバー側で処理が続いている場合に pod を delete してもきちんとレスポンスは帰ってくるはず、というのが期待する振る舞いです。
$ export ING1=$(kubectl get svc httpbin-svc-1 -n httpbin -o jsonpath='{.status.loadBalancer.ingress[0].ip}') $ export GATE=$(kubectl get svc istio-ingressgateway -n istio-system -o jsonpath="{.status.loadBalancer.ingress[0].ip}")
ING1(Ingress&istio-proxyなし)の場合、しっかり10秒待ってからレスポンスが得られます。
$ time curl -so /dev/null -w '%{http_code} ' $ING1:8000/delay/10 $ kubectl delete po (kubectl get po -n httpbin | grep httpbin-1 | cut -d ' ' -f 1) -n httpbin # この操作は別の terminal でやる pod "httpbin-1-545cbd998-989d8" deleted 200 curl -so /dev/null -w '%{http_code} ' $ING1:8000/delay/10 0.01s user 0.01s system 0% cpu 10.414 total
GATE(Gateway&istio-proxyあり)の場合も上記と同様に、しっかり10秒まってからレスポンスを返してほしいのですが・・・。
$ time curl -so /dev/null -w '%{http_code} ' $GATE/delay/10 $ kubectl delete po $(kubectl get po -n httpbin | grep httpbin-2 | cut -d ' ' -f 1) -n httpbin # この操作は別の terminal でやる pod "httpbin-2-67cd76c979-dhl2m" deleted 503 curl -so /dev/null -w '%{http_code} ' $GATE/delay/10 0.01s user 0.01s system 0% cpu 2.666 total
503が返ってきてしまいました。
対処法1:preStopを挟む
こちらの issue で言及されている通り、istio-proxy を編集しもう片方の container の終了を待つように preStop を作ります。
containers: - name: istio-proxy lifecycle: preStop: exec: command: ["/bin/sh", "-c", "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"]
ぱっと見一番シンプルかつ楽な方法に見えます。この方法で上記の問題を解決できるのか試してみます。
試すには httpbin-2 の Deployment を直接編集して preStop を追加すればよいのですが、それではクラスタ全体にこの変更を適用できないため、ここでは istio-sidecar-injector を差し替える方法を紹介したいと思います。
とはいえ難しいことは特になく、istio-system に istio-sidecar-injector という configMap と pod があるので、それを編集するだけです。前者の configMap の値が実際に適用される yaml となります。
# configMapの値は jsonpath を使うときれいに取れる kubectl get cm istio-sidecar-injector -n istio-system -o jsonpath='{.data.config}' > xxx/config kubectl delete cm istio-sidecar-injector # 先程とった config を編集して preStop を差し込む kubectl create cm istio-sidecar-injector -n istio-system --from-file xxx/config
新しい configMap を istio-sidecar-injector を読み込みませます
kubectl delete po istio-sidecar-injector-c8c4c568b-k9g5f -n istio-system
istio-proxy を更新するためには最終的に Deployment を更新する必要があります。kube-inject をもう一度行います。
cat <<EOF | istioctl kube-inject -f - | kubectl apply -f - apiVersion: extensions/v1beta1 kind: Deployment metadata: name: httpbin-2 namespace: httpbin spec: replicas: 1 template: metadata: labels: app: httpbin-2 spec: containers: - image: docker.io/citizenstig/httpbin imagePullPolicy: IfNotPresent name: httpbin ports: - containerPort: 8000 EOF
kubectl get deploy httpbin-2 -n httpbin -o yaml
とかして istio-proxy の preStop が定義されていることを確認してください。
その状態で先程の実験ように time curl -so /dev/null -w '%{http_code} ' $GATE/delay/10
の最中に pod を delete してみると、しっかり200が返ってくることが確認できると思います。
以下はおまけの話ですが、この方法でやる場合 istio の Gateway を使わず Ingress でもきちんとことが確認できます。
httpbin-1 と同じように type: LoadBalancer
のものを作ります。
$ kubectl expose deployment httpbin-2 --type LoadBalancer --name httpbin-svc-2 -n httpbin $ export ING=$(kubectl get svc httpbin-svc-2 -n httpbin -o jsonpath='{.status.loadBalancer.ingress[0].ip}') $ time curl -so /dev/null -w '%{http_code} ' $ING2:8000/delay/10 $ kubectl delete po $(kubectl get po -n httpbin | grep httpbin-2 | cut -d ' ' -f 1) -n httpbin # この操作は別の terminal でやる 200 curl -so /dev/null -w '%{http_code} ' $ING2:8000/delay/10 0.01s user 0.01s system 0% cpu 10.281 total
Gateway を使っていなければこちらの対処方法のほうが気楽かもしれないです。
対処法2:VirtualServiceでretryする
別の対処法として、VirtualService で retry してしまうという手があります。こちらも手軽な方法かと思います。
$ cat <<EOF | kubectl create -f - apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: httpbin-vs namespace: httpbin spec: hosts: - "*" gateways: - httpbin-gateway http: - match: route: - destination: host: httpbin-svc-2-ci port: number: 8000 retries: attempts: 2 perTryTimeout: 1s EOF
なお、この retry は http status が502-503の場合に行われるようです(参考:https://github.com/istio/istio/issues/8846)
アプリケーションの実装が500以外も頻繁に使われるようなものだと、予期しない挙動になる可能性もあるため注意が必要です。
また、言うまでもないですが外部からのリクエストに VirtualService を適用するためには Gateway が必要です。つまり上記のING2ではこちらの方法は通用しません。