Research on K8s Lossy Release

問題提起

アプリケーションのデプロイ時にトラフィック損失が発生することはよくある問題です。この現象は通常、下図に示すようにトラフィックモニタリングにフィードバックされます。デプロイ中にサービスの RT が急激に上昇し、一部のビジネスレスポンスが遅延します。ユーザーにとって最も直感的な体感は「重くなった」というものです。また、500 エラーのリクエスト数が急増し、ユーザーはサービスが劣化または利用不能になったと感じる可能性があり、ユーザーエクスペリエンスに影響を及ぼします。

アプリケーションのリリースにはトラフィック損失が伴うため、リリース計画をビジネスのオフピーク時に移行し、リリース時間を厳しく制限する必要があります。しかし、リリースに伴うリスクを完全に回避することはできず、やむを得ずリリースを中止する場合もあります。汎用アプリケーション管理システムとして、アプリケーションのデプロイは EDAS の最も基本的な機能の一つであり、K8s アプリケーションは EDAS における最も一般的なアプリケーション形態です。以下では、K8s のトラフィックフローから始め、EDAS のお客様の実際のシナリオをまとめ、損失を伴うデプロイの原因を分析し、実用的なソリューションを提示します。

トラフィックパス解析

K8s では、トラフィックは通常以下のパスからアプリケーション Pod に流入します。各パスは大きく異なり、トラフィック損失の原因も異なります。各パスのルーティングメカニズムと、Pod の変更がトラフィックパスに与える影響について見ていきます。

LB サービストラフィック

LoadBalancer タイプの Service を介してアプリケーションにアクセスする場合、トラフィックパスのコアコンポーネントは LoadBalancer と ipvs/iptables です。LoadBalancer は K8s クラスターの外部トラフィックを受信してノードに転送する役割を担い、ipvs/iptables はノードが受信したトラフィックを Pod に転送する役割を担います。コアコンポーネントの動作は CCM (Cloud Controller Manager) と kube-proxy によって駆動され、それぞれ LoadBalancer のバックエンドと ipvs/iptables ルールの更新を担当します。

アプリケーションがデプロイされると、ready 状態の Pod はエンドポイントのバックエンドに追加され、Terminating 状態の Pod はエンドポイントから削除されます。kube-proxy コンポーネントは各ノードの ipvs/iptables ルールを更新します。エンドポイントの変更を検知した後、CCM コンポーネントはクラウドベンダーの API を呼び出してロードバランサーのバックエンドを更新し、ノードの IP とポートをバックエンドリストに追加します。トラフィックが流入すると、ロードバランサーに設定されたバックエンドリストに基づいて対応するノードに転送され、さらにノードの ipvs/iptables によって実際の Pod に転送されます。

Service は externalTrafficPolicy の設定をサポートしています。パラメーターの違いにより、ノードの kube-proxy コンポーネントによる ipvs/iptables リストの更新と、CCM によるロードバランサーバックエンドの更新の動作が異なります。

・Local モード:CCM は対象サービスが存在するノードのみをロードバランサーのバックエンドアドレスリストに追加します。トラフィックがノードに到達した後、そのノードの Pod のみに転送されます。

・Cluster モード:CCM はすべてのノードをロードバランサーのバックエンドアドレスリストに追加します。トラフィックがノードに到達した後、他のノードの Pod への転送が許可されます。

Nginx Ingress トラフィック

Nginx Ingress が提供する SLB を介してアプリケーションにアクセスする場合、トラフィックパスのコアコンポーネントは Ingress コントローラーです。これはプロキシサーバーとしてバックエンドサービスの Pod にトラフィックを転送するだけでなく、エンドポイントに基づいて Ingress ゲートウェイのルーティングルールを更新します。

アプリケーションがデプロイされると、Ingress コントローラーはエンドポイントの変更を検知し、Ingress ゲートウェイのルーティングバックエンドを更新します。トラフィックが流入すると、ルーティングルールに一致するアップストリームに転送され、アップストリームのバックエンドリストからバックエンドが選択されてリクエストが転送されます。

デフォルトでは、コントローラーは Service のエンドポイントの変更を検知した後、Nginx の動的設定バックエンドインターフェイスを呼び出し、Nginx ゲートウェイのアップストリームバックエンドリストを Service のエンドポイントリスト(Pod の IP とポートのリスト)に更新します。したがって、Ingress コントローラーに流入したトラフィックは、バックエンドの Pod IP とポートに直接転送されます。

マイクロサービストラフィック

マイクロサービスを使用してアプリケーションにアクセスする場合、コアコンポーネントはレジストリです。プロバイダーが起動すると、サービスがレジストリに登録され、コンシューマーはレジストリでサービスのアドレスリストをサブスクライブします。

アプリケーションがデプロイされると、プロバイダーは起動後に Pod の IP とポートをレジストリに登録し、停止した Pod はレジストリから削除されます。サーバーリストの変更はコンシューマーにサブスクライブされ、キャッシュされたサービスバックエンドの Pod IP とポートリストが更新されます。トラフィックが流入すると、コンシューマーはサービスアドレスリストに基づいてクライアントサイドのロードバランサーにより対応するプロバイダー Pod に転送されます。

原因分析と一般的なソリューション

アプリケーションのデプロイプロセスは、新しい Pod が起動し、古い Pod が停止するプロセスです。トラフィックルーティングルールの更新とアプリケーション Pod の起動・停止の連携がうまくいかない場合、トラフィック損失が発生します。アプリケーションリリースによるトラフィック損失は、起動時の損失と停止時の損失に分類できます。一般的に、起動時と停止時の損失の原因は以下の通りです。詳細は以降のセクションで説明します。

・損失を伴う起動:新しい Pod が起動した際、ルーティングバックエンドに早期に追加され、トラフィックが準備の整っていない Pod に早期にルーティングされる。

・損失を伴う停止:古い Pod が停止した後、ルーティングルールがバックエンドを適時に削除できず、トラフィックが既に停止した Pod にルーティングされ続ける。

起動時の損失分析と対策

K8s における Pod の起動プロセスを下図に示します。

Pod の起動時に Pod 内のサービスの可用性チェックが行われない場合、Pod 起動後に早期にエンドポイントバックエンドに追加され、さらに他のゲートウェイコントローラーによってゲートウェイルーティングルールに追加されます。トラフィックが Pod に転送された後、接続が拒否されます。そのため、ヘルスチェックが特に重要です。Pod が起動完了してからトラフィックを分担できるようにし、トラフィック損失を回避する必要があります。K8s は Pod に readiness プローブを提供しており、新しい Pod の準備が整っているかを検証します。適切な readiness プローブを設定してアプリケーションの実際の起動状態を確認し、Service バックエンドのエンドポイントに追加されるタイミングをコントロールします。

エンドポイントベースのトラフィックシナリオ

LB サービストラフィックや Nginx Ingress トラフィックなど、エンドポイントベースのトラフィックパスが制御されるシナリオでは、適切な readiness プローブを設定することで、サービスがエンドポイントバックエンドに追加される前にヘルスチェックに合格していることを保証し、トラフィック損失を回避できます。たとえば、Spring Boot 2.3.0 以降では、ヘルスチェックインターフェイス /actuator/health/readiness と /actuator/health/liveness が追加され、K8s 環境にデプロイされた readiness プローブと liveness プローブの設定をサポートしています。

マイクロサービストラフィックシナリオ

マイクロサービスアプリケーションでは、サービスの登録とディスカバリはレジストリによって管理されており、K8s の readiness プローブのようなチェックメカニズムはありません。また、Java アプリケーションは通常起動が遅いため、サービス登録が成功した後でも、データベース接続プール、スレッドプール、JIT コンパイルなどのリソースがまだ初期化中の可能性があります。このタイミングで大量のマイクロサービスリクエストが流入すると、リクエスト RT の上昇やタイムアウトなどの例外が発生する可能性が高くなります。

上記の問題を解決するため、Dubbo は遅延登録とサービスウォームアップのソリューションを提供しています。機能の概要は以下の通りです。

・遅延登録機能により、ユーザーは一定の期間を指定できます。プログラム起動後、設定された待機時間を先に完了してからサービスをレジストリに登録します。待機中にプログラムは初期化を完了できるため、サービスリクエストの流入を回避できます。

・サービスウォームアップ機能により、ユーザーはウォームアップ期間を設定できます。プロバイダーがレジストリにサービスを登録する際、メタデータを通じてウォームアップ期間とサービスの起動時間をレジストリに登録します。コンシューマーはレジストリで関連するサービスインスタンスのリストをサブスクライブします。インスタンスのウォームアップ期間に基づき、プロバイダーの起動時間と組み合わせて呼び出しの重みを計算し、起動直後のインスタンスへのトラフィック割り当てを少なくコントロールします。小規模なトラフィックでのプレヒートにより、プログラムは低負荷状態でクラスローディングや JIT コンパイルを完了でき、プレヒート後に新しいインスタンスが安定的にトラフィックを分担できるようになります。

プログラムに以下の設定を追加することで、遅延登録とサービスウォームアップ機能を有効化できます。

上記のパラメーターを設定した後、プロバイダーアプリケーションの Pod を 1 つ拡張し、新しい Pod の起動時の QPS 曲線を確認して、トラフィックウォームアップの効果を検証します。QPS データを下図に示します。

Pod の受信トラフィックの QPS 曲線から、Pod 起動後に即座にトラフィックが割り当てられるのではなく、設定された 120 秒のウォームアップ時間内で 1 秒あたりの処理量が線形増加傾向を示し、120 秒後に安定化することが確認できます。これはトラフィックウォームアップの期待される効果に合致しています。

停止時の損失分析と対策

K8s における Pod の停止プロセスを下図に示します。

図からわかるように、Pod が削除された後、その状態はエンドポイントコントローラーと kubelet にサブスクライブされ、それぞれエンドポイントと Pod が削除されます。これら 2 つのコンポーネントの操作は同時に行われます。先にエンドポイントが削除され、その後 Pod が削除されるという順序通りの実行は保証されないため、Pod が既に SIGTERM シグナルを受信しているにもかかわらず、トラフィックがまだ流入し続ける状況が発生する可能性があります。

K8s は Pod の停止プロセスに preStop フックのメカニズムを提供しており、kubelet が Pod の状態が Terminating になったことを検知した際、コンテナに SIGTERM シグナルを送信する代わりに、事前処理を実行できます。上記の問題に対する一般的な対策として、preStop にスリープを設定して SIGTERM の送信を一定時間遅らせ、その期間に流入するトラフィックの損失を回避できます。さらに、Pod が受信したトラフィックを継続して処理できます。

以上、デプロイ時の Pod の停止とエンドポイント更新のタイミングが期待される順序に一致しない場合に発生し得るトラフィック損失の問題を説明しました。アプリケーションが複数のタイプのゲートウェイにアクセスすると、トラフィックパスの複雑さが増し、他のルーティングリンクでもトラフィック損失が発生する可能性があります。

LB サービストラフィックシナリオ

LoadBalancer タイプの Service を使用してアプリケーションにアクセスする場合、externalTrafficPolicy を Local モードに設定すると、トラフィックの中継転送を防ぎ、リクエストパケットのソース IP アドレスを保持できます。

アプリケーションのデプロイ中、Pod が停止してノードの ipvs リストから削除されますが、CCM がエンドポイントの変更を検知してクラウドベンダーの API を呼び出し、ロードバランサーのバックエンドを更新する際に遅延が発生する可能性があります。新しい Pod が別のノードにスケジュールされ、元のノードに利用可能な Pod がない場合、ロードバランサーのルーティングルールが適時に更新されないと、ロードバランサーは引き続き元のノードにトラフィックを転送しますが、そのパスには利用可能なバックエンドがなく、トラフィック損失が発生します。

この時、Pod の preStop にスリープを設定すると、LoadBalancer バックエンドが更新される前に Pod を一定時間正常に稼働させられますが、CCM が LoadBalancer バックエンドを削除する前に、kube-proxy がノード内の ipvs/iptables ルールを削除しないことを保証できません。上図のシナリオに示すように、Pod の停止プロセス中にリクエストパス 2 は既に削除されている一方、リクエストパス 1 はまだ適時に更新されていません。スリープにより Pod が一定期間サービスを提供し続けても、転送ルールが不完全なため、トラフィックは Pod に転送される前に破棄されます。

ソリューション:

・externalTrafficPolicy を Cluster に設定すると、停止時のトラフィック損失を回避できます。Cluster モードではクラスター内のすべてのノードがロードバランサーのバックエンドに追加され、ノード内の ipvs はクラスター内のすべての利用可能な Pod のリストを維持します。自ノードに利用可能な Pod がない場合、他のノードの Pod に中継転送できますが、二次転送での損失が発生し、ソース IP アドレスを保持できません。

・Pod をインプレースアップグレードに設定します。ノードに特定のラベルを付けることで、新しい Pod が同じノードにスケジュールされます。このプロセスではクラウドベンダーの API を呼び出してロードバランサーのバックエンドを更新する必要がなく、トラフィックが流入すると新しい Pod に転送されます。

Nginx Ingress トラフィックシナリオ

Nginx Ingress の場合、デフォルトではトラフィックは Service の ClusterIP を介さず、ゲートウェイを通じてバックエンド Pod IP に直接転送されます。アプリケーションのデプロイ中、Pod が停止してから Ingress コントローラーがエンドポイントの変更を検知して Nginx ゲートウェイを更新するまでに遅延があります。トラフィックが流入した後、まだ停止した Pod に転送される可能性があり、トラフィック損失が発生します。

ソリューション:

・Ingress アノテーション「nginx.ingress.kubernetes.io/service-upstream」は「true」または「false」の設定をサポートし、デフォルトは「false」です。このアノテーションを「true」に設定すると、ルーティングルールは ClusterIP を Ingress のアップストリームサービスアドレスとして使用します。「false」に設定すると、ルーティングルールは Pod IP を Ingress のアップストリームサービスアドレスとして使用します。Service の ClusterIP は常に同じであるため、Pod の起動時や停止時に Nginx ゲートウェイ設定の変更を考慮する必要がなく、上記のトラフィック損失の問題は発生しません。ただし、この機能を使用する場合、トラフィックの負荷分散は K8s によって制御され、一部の Ingress コントローラー機能(セッション維持、リトライポリシーなど)が無効になる点に注意が必要です。

・Pod の preStop にスリープを設定して一定時間待機させ、Pod が SIGTERM シグナルを受信する前に猶予を持たせ、その期間にトラフィックを受信して処理できるようにし、トラフィック損失を回避します。

マイクロサービストラフィックシナリオ

プロバイダーアプリケーションのデプロイ中、Pod が停止してレジストリからログアウトされますが、コンシューマーがサブスクライブするサーバーリストの変更には一定の遅延があります。トラフィックがコンシューマーに流入した際、コンシューマーがまだサーバーリストをリフレッシュしていない場合、停止した Pod にアクセスする可能性があります。

マイクロサービスアプリケーション Pod の停止時、サービスの登録とディスカバリはレジストリを通じて行われ、エンドポイントに依存しません。前述のエンドポイントコントローラーによるエンドポイントの削除では、Pod IP のレジストリからの削除は実現できません。preStop でスリープするだけでは、コンシューマーのサーバーリストのキャッシュリフレッシュ遅延の問題を解決できません。古い Pod をグレースフルに停止するには、preStop で先にレジストリから登録を解除し、受信したトラフィックを処理できるようにする必要があります。さらに、コンシューマーが停止する前に、クライアントにキャッシュされたプロバイダーインスタンスのリストをリフレッシュしていることを確認する必要があります。停止するインスタンスは、レジストリのインターフェイスを呼び出すか、プログラムのサービスフレームワークが提供するインターフェイスを呼び出して preStop に設定することで、上記の効果を実現できます。EDAS では

エンタープライズ向けワンストップソリューション

以上、アプリケーションリリース中に 3 つの一般的なトラフィックパスで発生するトラフィック損失の原因を分析し、ソリューションを提示しました。一般的に、トラフィック損失なしを確保するには、ゲートウェイパラメーターと Pod のライフサイクルプローブおよびフックの観点から、トラフィックパスと Pod の起動・停止の連携を確保する必要があります。EDAS は上記の問題に対する非侵襲的なソリューションを提供しています。プログラムコードやパラメーター設定を変更することなく、EDAS コンソールでアプリケーションの無損失での起動と停止を実現できます。下図に示します。

・LB Service は externalTrafficPolicy の設定をサポート

・Nginx Ingress は「nginx.ingress.kubernetes.io/service-upstream」アノテーションの設定をサポート

・マイクロサービスアプリケーションの無損失起動パラメーターの設定

さらに、EDAS は Nginx Ingress、ALB イングレス、Cloud Native Gateway などのさまざまなトラフィックゲートウェイ管理方法、およびバッチデプロイ、カナリアデプロイなどの多様なデプロイ方法、さらに Ingress モニタリングやアプリケーションモニタリングなどの異なる次元の可観測性手段を提供しています。EDAS で管理するアプリケーションは、複数のデプロイシナリオにおいて簡単に無損失での起動と停止を実現できます。

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00

phone お問い合わせ
Hi, I'm Alibaba Cloud AI Assistant!
I can help with questions and solutions.