Carpe Diem

備忘録

Outlier Detection と自動リトライを使ってデプロイ時・Podの再配置時のエラーを減らす

背景

過去、デプロイ時やPodの再配置時のエラーは

といった対応を行って解消していました。

しばらくはこれで安定していたのですが、GKEでIstio(Anthos Service Mesh)のアップデートがされてから再び出るようになってしまったのでその時の対応策を共有します。

環境

  • Anthos Service Mesh v1.20.8-asm.33

課題

今回のケースはエッジケースだと思っているので、状況をなるべく詳細に説明します。

事象

  • デプロイ時・Podの再配置時に503エラーが稀に発生する
    • マイクロサービスA → マイクロサービスB という依存関係で、Bの入れ替わり時にAでエラーが発生
  • 具体的なエラーとしては以下
    • upstream request timeout
    • upstream connect error or disconnect/reset before headers. retried and the latest reset reason: remote connection failure, transport failure reason: delayed connect error: 111
  • IstioのエラーコードではUR, URX

調査

起動時か終了時か

christina04.hatenablog.com

で説明したように、このエラーは起動時・終了時両方起きる可能性があります。

今回はGKEやアプリケーションログから、次のような時系列が分かりました。

  1. 15:23:10 マイクロサービスBのistio-sidecarがready
  2. 15:23:11 マイクロサービスAでupstream request timeoutが頻発
  3. 15:23:12 マイクロサービスBのアプリケーションコンテナが起動開始
  4. 15:23:13 マイクロサービスBでしばらくstartup probeが失敗(grpcサーバが起動完了していない)
  5. 15:23:19 マイクロサービスBでアプリケーションコンテナがready
  6. 15:23:30 マイクロサービスAでupstream request timeoutが止む

なので起動時に発生していると問題の切り分けができました。

probeは設定されているか

Istioがある場合のprobeは設定値によって挙動が変わるため、過去にまとめたことがあります。

christina04.hatenablog.com

これを元に現状の設定を確認しましたが問題はありませんでした。
なので前述したように常に起きるわけでなく、たまに起きるという状況で再現ができませんでした。

Podがreadyになる前にKubernetes Serviceがトラフィックを流すことはあるのか

逆パターン(アプリ起動後サイドカー起動)ではありますが、、どうやら過去にも同様の事象(全てがreadyになってないのにトラフィック流れてる)は報告されていました。

Initial check for container being ready in a pod · Issue #82482 · kubernetes/kubernetes

こちらも同様な問題に対して、Podでコンテナ起動順に依存関係を持たせられないかというIssueです。

Support startup dependencies between containers on the same Pod · Issue #65502 · kubernetes/kubernetes

後者ではkubexitといツールがワークアラウンドとして提案されていましたが、Istioのように自動で注入されるケースは想定されていない(全部自分で管理するようなケース)ようだったので今回の解決策としては使えなそうでした。

調査結果

これらのファクトから、Pod内で

  • Istio-sidecarはready
  • アプリケーションコンテナはreadyでない

という状態にも関わらずトラフィックがPodに流れることによって upstream request timeout が発生していると考えられました。

これは本来Kubernetes Serviceに期待される挙動ではないため、何らかの条件を満たしたときのバグ(なので必ずしも起きる訳ではない再現性の難しさが含まれる)と考えました。

対応方針

適切なパラメータ・構成を取っていても稀に異常系が発生するため、根本対応を取ることが難しいです。

そこで今回は緩和策として、Virtual Serviceにおける自動リトライと Outlier Detection を使ってエラーを低減する方針を取りました。

Readyじゃないためエラーが返る場合に…

Outlier Detectionで切り離し、失敗したリクエストも自動リトライで成功させる

Outlier Detection とは

こちらで詳細を説明していますが、

christina04.hatenablog.com

今回は

  • Readyになる前にServiceからトラフィック流れてくる
  • ただし実際にPodからは5xxが返る
  • 通常リクエストは流れ続けてエラーが継続する
  • そこでOutlier Detectionによって早期にPodを切り離す
  • そして自動リトライによって失敗したリクエストを正常のPodに再ルーティングさせる

といった方針です。

設定

Outlier Detection と自動リトライの設定方法です。

Outlier Detection

DestinationRuleとして次のような設定を入れます。

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: specific-outlier-detection
spec:
  host: my-service  # 対象のサービス名に書き換えてください
  trafficPolicy:
    outlierDetection:
      splitExternalLocalOriginErrors: true
      consecutiveLocalOriginFailures: 3
      consecutive5xxErrors: 0
      consecutiveGatewayErrors: 5
      baseEjectionTime: 15s
      interval: 5s
      maxEjectionPercent: 50
      minHealthPercent: 50

パラメータは前回の説明を参考にしてください。

自動リトライ

Virtual Serviceで設定します。

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: my-service-vs
spec:
  hosts:
  - my-service   # DestinationRuleのhostと一致させる
  http:
  - route:
    - destination:
        host: my-service  # ここへルーティングされる時にDestinationRuleが適用される
    retries:
      attempts: 3
      perTryTimeout: 2s
      # DestinationRuleで検知しようとしているエラー種別に合わせるのが効果的
      retryOn: connect-failure,gateway-error,reset,refused-stream,unavailable
    # --- 全体のタイムアウト ---
    timeout: 10s

retryOnの設定値の目的は以下です。

retryOn 設定値 対応するエラーログ / 事象 解説
connect-failure delayed connect error: 111
(Connection Refused)
アプリのプロセスは立ち上がっているが、まだポートをリッスンしていない状態を検知してリトライ。
gateway-error upstream request timeout
(502/503/504)
Envoyがバックエンドへの接続や応答を待ちきれずにタイムアウトしたケースを拾う。
reset disconnect/reset before headers TCP接続は確立できたものの、ヘッダー送信前(通信開始直後)に切断された状態を拾う。
unavailable gRPC Status: UNAVAILABLE アプリケーション(gRPCサーバー)が起動シーケンス中などで、明示的に「今は利用不可」と返してきたケースを対応。
deadline-exceeded gRPC Status: DEADLINE_EXCEEDED Podが準備不足で応答を返さず、クライアントやEnvoyの待ち時間を超過した場合に、次のPodへ切り替えるために使用します。

注意として、一般には timeout は高負荷で重い状況に起きます。
その状態でさらにリトライで負荷を与えるとサーバにとどめを刺すことになるので、perTryTimeouttimeout設定で早期に失敗させるフォローが必要です。

とはいえ今回はPodが起動処理中などで沈黙しているだけなので、リトライして別の稼働済みPodに当たれば即座に正常処理されシステムへの悪影響は小さいと考えます。

結果

期待通り、

  • 早期にReadyでないPodを外す
  • ↑のPodにルーティングされて失敗したリクエストを自動リトライで成功させる

がうまくいったようで、このケースにおけるエラーは出なくなりました。

まとめ

エッジケースではありますが、ReadyでないPodにリクエストが流れ込んでしまったとしても、Outlier Detectionと自動リトライを使うことでエラーを低減することができるようになりました。

参考