今回は Kubernetes の代表的な Service Mesh である Istio の timeout やら retry やらについての紹介です。
Istio の timeout, retry, circuit breaking, etc
こんにちは、スリーシェイクの寺岡です。今回は Kubernetes の代表的な Service Mesh である Istio の timeout やら retry やらについての紹介です。
環境
Istio は 1.7.3 で、その中で使われている Envoy は 1.15.1 です。
Kubernetes は EKS で 1.17 です。
Istio と Envoy の関係
まずは、Istio と Envoy の関係についてですが、大雑把に言うと Istio は Envoy を Pod のサイドカーとして挿入し、それらの設定を管理するものです。私だけかもしれませんが、最初はそれらの Envoy 同士が特別な接続を持っているのではないかと勝手に想像していました。でも実際にはそれぞれの Envoy はただ Pod の外から中、中から外への通信の間に入って Proxy するだけで Envoy 同士の間には何の関係もありません。ですから timeout や retry についても一つの Pod に棲み着く Envoy だけを見れば良いことになります。
Envoy 内のリクエストの流れ
Envoy のドキュメントサイトに Life of a Request と言うページがありここで説明されています。
Listener で受け付けたリクエストを filter で HTTP Header や TLS の SNI などから得られるホスト名や port 番号、HTTP の Request 内容から振り分け先となる Cluster を選定 (Routing) し、その Cluster に紐づく Endpoint に対して Proxy します。
Istio リソースとの対応付け
Istio には VirtualService と DestinationRule というリソースがあります。Gateway など他にもありますが、今回は登場しません。
VirtualService は Envoy における Listener から Routing filter での Cluster 選択までを、DestinationRule が Cluster から Endpoint 部分にあたります。
Istio には istioctl というコマンドラインツールがあり、 istioctl proxy-config
というサブコマンドで listener
, routes
, cluster
, endpoint
設定を確認することができます。これが Envoy 内での役割に近い形での分類になっています。
istioctl proxy-config <clusters|listeners|routes|endpoints|bootstrap> <pod-name[.namespace]>
ただし、このコマンドで確認できるのはまだ Istio としての設定であり、Envoy の設定としてどうなっているのかを確認するためにはサイドカーとして Inject される istio-proxy コンテナで http://localhost:15000/config_dump にアクセスする必要があります。
Timeout と Retry
リクエスト全体の Timeout と Retry の設定は VirtualService の担当です。
次のような VirtualService があった場合
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: example-app
spec:
hosts:
- '*'
http:
- match:
- port: 8080
retries:
attempts: 2
perTryTimeout: 5s
retryOn: connect-failure,refused-stream
timeout: 10s
route:
- destination:
host: example-app
timeout: 10s
により、HTTP リクエスト全体のタイムアウトは 10 秒になります。 retries.attempts: 2
で 2 回まで retry を行います。ただし、 retries.retryOn
で指定した条件の場合に限られます。ここでは connect-failuer
と refused-stream
が指定されているため connect に失敗した場合、あるいは HTTP/2 でリクエストが拒否された場合、つまり、リクエストがサーバー側に受け入れられていない場合に限ります。冪等性の担保されたサービスであればサーバーからのレスポンスが遅い場合などにも retry は可能でしょう。 retryOn
の他の選択肢については Envoy のドキュメントで詳しく説明されています。Retry の条件をクライアントが HTTP Header で指定するなんてこともできます。perTryTimeout
によって 1 度の試行ごとの timeout も指定可能です。レスポンスが遅い場合にも retry する設定であれば、長く待つよりも早々に諦めて別の endpoint に移らせるといったことができます。DestinationRule で設定する connectTimeout
も影響してきます。
Istio でのデフォルト値は timeout
が 0s
でタイムアウトなし(一時期 15s に変わったことがありました)、 attempts
は 2
、 retryOn
は 503
(HTTP で 503 が返ってきた場合) となっています。 timeout
の値は Envoy の max_grpc_timeout
という設定にも使われます。
Circuit Breaking
Istio では DestinationRule の Connection Pool で Circuit Breaking 設定が行えます。Circuit Breaking はリクエストが多すぎて Proxy 先が過負荷でダウンしてしまわないように手前でリクエスト数を調整し、閾値を超える場合はサーバーにリクエストを送らずにクライアントにエラーを返す機能です。(電子レンジとオーブンとドライヤーを一緒に使ってブレーカーが落ちたりするやつです。)
それでは DestinationRule の設定例です。値に深い意味はありません、テキトーです。
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: example-app
spec:
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 100
http2MaxRequests: 100
maxRequestsPerConnection: 1000
maxRetries: 100
idleTimeout: 1m
tcp:
connectTimeout: 1s
maxConnections: 10
tcpKeepalive:
probes: 9
time: 2h
interval: 75s
loadBalancer:
localityLbSetting:
enabled: false
outlierDetection:
baseEjectionTime: 30s
consecutiveGatewayErrors: 3
interval: 5s
maxEjectionPercent: 99
connectionPool
に http
と tcp
の設定があります。( connectionPool
の設定が全て Circuit Breaker の設定というわけではありませんが Istio の設定の塊なのでここでまとめて紹介します)まず http
ですが、 http1MaxPendingRequests
は HTTP/1.1 の場合、1 connection あたり、同時に1リクエストしか処理できませんから connection 数を上回る場合は pending 状態となります。この pending 状態のリクエストがこの値を超えると 503
を即座に返します。 http2MaxRequests
の方は HTTP/2 ですので 1 connection で多くのリクエストを同時に処理します、このため同時に処理するリクエストの数を制限することになり、この値を超えると 503
を即座に返します。 maxRequestsPerConnection
は Circuit Breaker の機能では有馬ませんが、1 つの connection でこの数のリクエストを処理したら一旦接続を閉じます。値を 1 にすることで HTTP の Keep-Alive を無効にすることになります。これはあまりに小さくしすぎると TLS / mTLS 環境では TLS handshake の分、負荷やレイテンシが上がってしまいます。 maxRetries
は、多くのリクエストが retry しまくることによって負荷が上がってしまうことを避けるための設定で retry 中のリクエストがこれを超える場合は retry せずに 503 を返します。 idleTimeout
も Circuit Breaker の設定ではありませんね、HTTP の Keep-Alive 接続も idle 状態がこの時間続くと切断します。(HTTP/2 の PING は active とは扱われず、idle が続いているとして扱われます)
次に tcp
の設定です。 connectTimeout
はその名の通りで、デフォルトは 10s
です。 maxConnections
はその Cluster (DestinationRule) の endpoint 全体に対する接続数の合計の上限です。これを超える接続は行いません。クライアント側の Pod が複数あれば、サーバー側 (Upstream) から見た合計の最大接続数はクライアント側 (Downstream) の数を掛けた数となります。
connectionPool
の tcp
設定には TCP の KeepAlive 設定もあります。上の設定例は Istio のドキュメントにある Linux のデフォルトとされるものです。
# sysctl -a | grep tcp_keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
Idle 状態が 2 時間続いた後から 75 秒間隔で keepalive packet を送り、9 回続けてレスポンスがないと切断します。かなり緩いですね。必要になるのは通信相手が RESET を送ることなく(送るチャンスなく)いなくなってしまった場合です。(接続された状態で Chaos Mesh の Network Chaos で packet を drop するようにした場合に遭遇しました。)デフォルトがこんなに緩いとこれを短くするのはデメリットがあるのだろうか?と思ってしまいますがどうなんでしょう? http
設定の idleTimeout を短くしておくことでも対策にはなりますが相手が生きていれば切断しなくて良い分 tcp
の方が良さそうな気もします。
未設定で Envoy の設定を確認すると Circuit Breaker のデフォルト設定は 2³² – 1 でした。
"circuit_breakers": {
"thresholds": [
{
"max_connections": 4294967295,
"max_pending_requests": 4294967295,
"max_requests": 4294967295,
"max_retries": 4294967295
}
]
}
Outlier Detection
次に Outlier Detection (Envoy)です。直訳すると外れ値検出ですが、エラーの続く endpoint を一時的に無効にしてリクエストを送らないようにする機能です。
outlierDetection:
baseEjectionTime: 30s
consecutiveGatewayErrors: 3
interval: 5s
maxEjectionPercent: 99
この例では Gateway Error (connect のエラーや 502, 503, 504) が 3 回連続すると 30 秒間外されます。2 度目は 2 倍の 60 秒間、3 度目は 3 倍の 180 秒間となります。何度もエラーが続く endpoint は徐々に長く外されることになります。この外したり戻したりを評価する間隔が interval
(っぽい)です。 consecutive5xxErrors
という設定もあります。Envoy には success rate を使った条件設定などもありますが Istio からは指定できません。
Locality Load Balancing
最後に Locality Load Balancing です。Istio のデフォルト設定では Outlier Detection を有効にすると、この Locality Load Balancing も有効になります。先の DestinationRule の設定例では無効にしていました。
loadBalancer:
localityLbSetting:
enabled: false
Locality Load Balancing が有効になるとデフォルトでは endpoint 選択に置いて、可能な限りクライアント側と同じ zone (AWS であれば availability zone) のものが使われるようになります。そもそも同一 zone に存在しない場合や Outlier Detection により同一 zone の endpoint が全て外されてしまった場合にのみ別 zone の endpoint に振り分けられるようになります。
次のように指定することで振り分けの割合を指定することも可能です。
spec:
trafficPolicy:
loadBalancer:
localityLbSetting:
enabled: true
distribute:
- from: "ap-northeast-1/ap-northeast-1a"
to:
"ap-northeast-1/ap-northeast-1a": 80
"ap-northeast-1/ap-northeast-1c": 20
- from: "ap-northeast-1/ap-northeast-1c"
to:
"ap-northeast-1/ap-northeast-1a": 20
"ap-northeast-1/ap-northeast-1c": 80
Istio の ConfigMap でクラスタ全体のデフォルト値を設定可能です。
同一 zone に閉じる方がレイテンシも小さくなりますし、クラウドサービスによっては通信費用も安くなります。上で触れませんでしたが、VirtualService での retry 設定には retryRemoteLocalities
というものもあります。
あ、もしかしてデフォルトの動作は Zone aware routing で、割合を指定したやつを Locality weighted load balancing と呼ぶのかな。
以上、Istio / Envoy の timeout や retry についての紹介でした。主に HTTP の場合の紹介でした、TCP での場合は機能がだいぶ限定されます。
Istio 1.7 繋がりで、こちらもどうぞ
メインコンテナの起動前に istio-proxy の起動を完了させる