ある日、k8s クラスターの計画的な更新中に、新しいノード上のほぼすべての POD (1,000 個中約 500 個) が起動できないことが判明し、数分があっという間に数時間に変わりました。根本原因を積極的に調査しましたが、3 時間経っても PODS はContainerCreating
ステータスのままでした。
幸いなことに、これは本番環境ではなく、メンテナンス ウィンドウは週末に予定されていました。私たちはプレッシャーを感じることなく、問題を調査する時間がありました。
根本原因の調査はどこから始めればよいでしょうか? 私たちが見つけた解決策についてもっと知りたいですか? シートベルトを締めて、お楽しみください!
問題は、クラスター内の各ノードで同時にプルして起動する必要のある Docker イメージが多数あることでした。これは、単一のノードで複数の Docker イメージを同時にプルすると、ディスク使用率が高くなり、コールド スタート時間が長くなる可能性があるためです。
CD プロセスでは、イメージをプルするのに最大 3 時間かかることがあります。ただし、今回は EKS アップグレード (インライン、クラスター内のすべてのノードを置き換えるとき) 中の PODS の量が多すぎたため、完全に停止しました。
当社のアプリはすべて k8s ( EKSベース) で稼働しています。開発環境のコストを節約するために、スポット インスタンスを使用しています。
ノードにはAmazonLinux2イメージを使用します。
開発環境には、Kubernetes クラスターに継続的にデプロイされる多数の機能ブランチ(FB) があります。各 FB には独自のアプリケーション セットがあり、各アプリケーションには独自の依存関係のセット (イメージ内) があります。
私たちのプロジェクトには約 200 個のアプリがあり、この数は増え続けています。各アプリは、サイズが約 2 GB の 7 つの基本 Docker イメージの 1 つを使用します。アーカイブされたイメージ ( ECR内) の最大合計サイズは約 3 GB です。
すべてのイメージは Amazon Elastic Container Registry (ECR) に保存されます。
ノードにはデフォルトの gp3 EBS ボリューム タイプを使用します。
コールド スタート時間の延長:新しいイメージで新しいポッドを起動すると、特に 1 つのノードで複数のイメージが同時にプルされる場合、1 時間以上かかることがあります。
ErrImagePull エラー: ErrImagePull
頻繁に発生したり、 ContainerCreating
状態のままになったりして、イメージのプルに問題があることを示します。
ディスク使用率が高い:イメージ取得プロセス中、ディスク使用率は 100% 近くを維持します。これは主に、解凍 (例: 「unpigz」) に必要な集中的なディスク I/O が原因です。
システム DaemonSet の問題:一部のシステム DaemonSet ( aws-node
やebs-csi-node
など) は、ディスクの負荷により「準備ができていない」状態に移行し、ノードの準備状況に影響を与えました。
ノードにイメージ キャッシュがありません。スポット インスタンスを使用しているため、イメージのキャッシュにローカル ディスクを使用できません。
この結果、特に異なる FB には異なるベース イメージのセットがあるため、機能ブランチでのデプロイメントが停止するケースが多くなります。
すぐに調査した結果、主な問題はunpigz
プロセスによるノードのディスク負荷であることがわかりました。このプロセスは、Docker イメージの解凍を担当します。gp3 EBS ボリューム タイプのデフォルト設定は、このケースには適していないため変更しませんでした。
最初のステップとして、ノード上の POD の数を減らすことにしました。
ソリューションの主なアイデアは、すべてのアプリのルート イメージとして使用される Docker イメージの最大の部分 (JS 依存関係レイヤー) によって、CD プロセスが開始する前にノードをウォームアップすることです。アプリの種類に関連する JS 依存関係を持つルート イメージは少なくとも 7 種類あります。それでは、元の CI/CD 設計を分析してみましょう。
当社の CI/CD パイプラインには 3 つの柱があります。
独自の CI/CD パイプライン:
Init
it ステップでは、環境/変数を準備し、再構築するイメージのセットを定義します。
Build
ステップでは、イメージをビルドしてECRにプッシュします。
Deploy
手順: イメージを k8s にデプロイします (デプロイメントの更新など)
オリジナルの CICD 設計の詳細:
main
ブランチから分岐しました。CI プロセスでは、FB で変更されたイメージのセットを常に分析し、再構築します。 main
ブランチは常に安定しており、定義どおり、ベース イメージの最新バージョンが常に存在するはずです。ウォームアッププロセスには要件があります。
必須:
ContainerCreating
問題に対処して解決します。改善されてよかった点:
要件と制約を分析した後、ベース JS キャッシュ イメージを使用してノードを事前加熱するウォームアップ プロセスを実装することにしました。このプロセスは CD プロセスの開始前にトリガーされ、ノードが FB の展開の準備が整っていることが保証され、キャッシュをヒットする可能性が最大限に高まります。
この改善は、3 つの大きなステップに分割されます。
各FBごとにノードセット(仮想ノードグループ)を作成する
新しいノードのcloud-init スクリプトにベースイメージを追加します。
CD プロセスが開始する前に、必要な Docker イメージをノードにダウンロードするために、 initContainers
セクションでDaemonSet を実行する事前デプロイ手順を追加します。
CI パイプラインから API 呼び出し (サードパーティの自動スケーリング システムへ) を介して、各 FB の新しいノード セットを作成します。
解決された問題:
分離: 各 FB には独自のノード セットがあり、環境が他の FB の影響を受けないことを保証します。
柔軟性: ノードタイプとその有効期間を簡単に変更できます。
コスト効率: FB が削除されるとすぐにノードを削除できます。
透明性: ノードの使用状況とパフォーマンスを簡単に追跡できます (各ノードには FB に関連するタグがあります)。
スポット インスタンスの効果的な使用方法: スポット インスタンスは、すでに事前定義されたベース イメージを使用して起動します。つまり、スポット ノードが起動すると、ノード上には (メイン ブランチからの) ベース イメージがすでに存在します。
cloud-init
スクリプトを使用して、メイン ブランチからすべての JS ベース イメージを新しいノードにダウンロードします。
イメージがバックグラウンドでダウンロードされている間も、CD プロセスは問題なく新しいイメージの構築を続行できます。さらに、このグループの次のノード (自動スケーリング システムによって作成されるノード) は、開始前にイメージをダウンロードする指示がすでに含まれた更新されたcloud-init
データを使用して作成されます。
解決された問題:
問題の解決: メイン ブランチからベース イメージのダウンロードを追加してcloud-init
スクリプトを更新したため、ディスク プレッシャーはなくなりました。これにより、FB の最初の起動時にキャッシュにアクセスできるようになりました。
スポット インスタンスの効果的な使用法: スポット インスタンスは更新されたcloud-init
データで起動します。つまり、スポット ノードが起動すると、ノード上にベース イメージ (メイン ブランチから) がすでに存在することになります。
パフォーマンスの向上: CD プロセスは問題なく新しいイメージの構築を継続できます。
このアクションにより、CI/CD パイプラインに約 17 秒 (API 呼び出し) が追加されました。
このアクションは、FB を初めて起動するときのみ意味があります。次回は、前回のデプロイメントで配信したベース イメージが既に存在するノードにアプリをデプロイします。
FB イメージはメイン ブランチ イメージとは異なるため、この手順が必要です。CD プロセスを開始する前に、FB ベース イメージをノードにダウンロードする必要があります。これにより、複数の重いイメージが同時にプルされたときに発生する可能性のある、コールド スタート時間の延長とディスク使用率の上昇を軽減できます。
事前デプロイ手順の目的
ディスク負荷の防止: 最も重い Docker イメージを順番にダウンロードします。init-deploy ステップの後、ノード上にベース イメージがすでに存在するため、キャッシュにヒットする可能性が高くなります。
デプロイメント効率の向上: ノードが必須の Docker イメージで事前に加熱されていることを確認し、POD の起動時間を短縮 (ほぼ即時) します。
安定性の向上: ErrImagePull
/ ContainerCreating
エラーが発生する可能性を最小限に抑え、システム デーモン セットが「準備完了」状態を維持するようにします。
このステップでは、CD プロセスに 10 ~ 15 分を追加します。
デプロイ前の手順の詳細:
initContainers
セクションを使用して DaemonSet を作成します。initContainers
セクションはメイン コンテナーが起動する前に実行され、メイン コンテナーが起動する前に必要なイメージがダウンロードされるようにします。予熱プロセスにおける元の手順と更新された手順の比較。
ステップ | 初期デプロイ手順 | 展開前の手順 | 展開する | 合計時間 | 差分 |
---|---|---|---|---|---|
予熱なし | 0 | 0 | 11分21秒 | 11分21秒 | 0 |
予熱あり | 8秒 | 58秒 | 25秒 | 1分31秒 | -9分50秒 |
主な変更点は、「デプロイ」時間(最初の適用コマンドからポッドの実行状態まで)が 11 分 21 秒から 25 秒に変更されたことです。合計時間は 11 分 21 秒から 1 分 31 秒に変更されました。
重要な点として、メイン ブランチからのベース イメージがない場合、「デプロイ」時間は元の時間と同じか、少し長くなります。いずれにせよ、ディスク プレッシャーとコールド スタート時間の問題は解決しました。
ContainerCreating
主な問題はウォームアップ プロセスによって解決されました。その利点として、POD のコールド スタート時間が大幅に短縮されました。
ノードにベース イメージがすでに存在するため、ディスク プレッシャーは解消されました。システム daemonSets は「準備完了」かつ「正常」な状態にあり (ディスク プレッシャーがないため)、この問題に関連するErrImagePull
エラーは発生していません。
追伸: Justt ( https://www.linkedin.com/company/justt-ai ) の素晴らしい技術チームに、彼らが直面するあらゆる問題に対してたゆまぬ努力と本当に創造的なアプローチをしていることに感謝したいと思います。特に、チームの素晴らしい仕事の責任者である素晴らしいリーダー、Ronny Sharaby に感謝したいと思います。皆さんの創造性が Justt 製品にどのような影響を与えるかを示す素晴らしい例を、もっともっと見ることを楽しみにしています。