paint-brush
大規模な Docker イメージ向けに Kubernetes を最適化する方法@kksudo
新しい歴史

大規模な Docker イメージ向けに Kubernetes を最適化する方法

Kazakov Kirill10m2024/09/30
Read on Terminal Reader

長すぎる; 読むには

🚀 Kubernetes の達人、ノードがウォームアップするまで何時間も待つのにうんざりしていませんか? その時間をわずか数秒に短縮することを想像してみてください! この画期的な記事では、3 GB の巨大なイメージと 1000 個のポッドがあっても、Kubernetes のデプロイメント プロセスを高速化する方法を明らかにします。 クラスターのパフォーマンスを低速から超高速に変える秘密のソースを見つけてください。ウォームアップが遅いからといって、躊躇しないでください。Kubernetes ワークフローを革新する方法を今すぐ学んでください!
featured image - 大規模な Docker イメージ向けに Kubernetes を最適化する方法
Kazakov Kirill HackerNoon profile picture
0-item

問題の概要

ある日、k8s クラスターの計画的な更新中に、新しいノード上のほぼすべての POD (1,000 個中約 500 個) が起動できないことが判明し、数分があっという間に数時間に変わりました。根本原因を積極的に調査しましたが、3 時間経っても PODS はContainerCreatingステータスのままでした。


Kubernetes が 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-nodeebs-csi-nodeなど) は、ディスクの負荷により「準備ができていない」状態に移行し、ノードの準備状況に影響を与えました。

  • ノードにイメージ キャッシュがありません。スポット インスタンスを使用しているため、イメージのキャッシュにローカル ディスクを使用できません。


この結果、特に異なる FB には異なるベース イメージのセットがあるため、機能ブランチでのデプロイメントが停止するケースが多くなります。

すぐに調査した結果、主な問題はunpigzプロセスによるノードのディスク負荷であることがわかりました。このプロセスは、Docker イメージの解凍を担当します。gp3 EBS ボリューム タイプのデフォルト設定は、このケースには適していないため変更しませんでした。


クラスターを回復するための修正プログラム

最初のステップとして、ノード上の POD の数を減らすことにしました。

  1. 新しいノードを「Cordon」状態に移動します
  2. 詰まったPODSをすべて取り除き、ディスク圧力を軽減します。
  3. PODを1つずつ実行してノードをウォームアップします
  4. その後、ウォームアップしたノードを通常の状態(「unCordon」)に移行します。
  5. スタック状態のすべてのノードを削除しました
  6. すべてのPODSはDockerイメージキャッシュを使用して正常に起動しました


独自のCI/CD設計

ソリューションの主なアイデアは、すべてのアプリのルート イメージとして使用される Docker イメージの最大の部分 (JS 依存関係レイヤー) によって、CD プロセスが開始する前にノードをウォームアップすることです。アプリの種類に関連する JS 依存関係を持つルート イメージは少なくとも 7 種類あります。それでは、元の CI/CD 設計を分析してみましょう。


当社の CI/CD パイプラインには 3 つの柱があります。 独自のCI/CDパイプライン

独自の CI/CD パイプライン:

  1. Init it ステップでは、環境/変数を準備し、再構築するイメージのセットを定義します。

  2. Buildステップでは、イメージをビルドしてECRにプッシュします。

  3. Deploy手順: イメージを k8s にデプロイします (デプロイメントの更新など)


オリジナルの CICD 設計の詳細:

  • 機能ブランチ (FB) はmainブランチから分岐しました。CI プロセスでは、FB で変更されたイメージのセットを常に分析し、再構築します。 mainブランチは常に安定しており、定義どおり、ベース イメージの最新バージョンが常に存在するはずです。
  • JS 依存関係の Docker イメージを環境ごとに個別に構築し、ECR にプッシュして、Dockerfile のルート (ベース) イメージとして再利用します。JS 依存関係の Docker イメージは 5 ~ 10 種類程度あります。
  • FB は、別の名前空間の k8s クラスターにデプロイされますが、FB の共通ノードにデプロイされます。FB には最大 200 個のアプリを含めることができ、イメージ サイズは最大 3 GB です。
  • クラスター自動スケーリング システムがあり、負荷または保留中の PODS に基づいて、対応する nodeSelector と toleration を使用してクラスター内のノードをスケーリングします。
  • ノードにはスポットインスタンスを使用します。

ウォームアッププロセスの実施

ウォームアッププロセスには要件があります。

必須:

  1. 問題解決: ContainerCreating問題に対処して解決します。
  2. パフォーマンスの向上: 事前加熱されたベースイメージ (JS 依存関係) を利用することで起動時間が大幅に短縮されます。

改善されてよかった点:

  1. 柔軟性: ノード タイプとその寿命 (例: 高 SLA または存続期間の延長) を簡単に変更できます。
  2. 透明性: 使用状況とパフォーマンスに関する明確な指標を提供します。
  3. コスト効率: 関連する機能ブランチが削除された直後に VNG を削除することでコストを節約します。
  4. 分離: このアプローチにより、他の環境が影響を受けないことが保証されます。

解決

要件と制約を分析した後、ベース JS キャッシュ イメージを使用してノードを事前加熱するウォームアップ プロセスを実装することにしました。このプロセスは CD プロセスの開始前にトリガーされ、ノードが FB の展開の準備が整っていることが保証され、キャッシュをヒットする可能性が最大限に高まります。


この改善は、3 つの大きなステップに分割されます。

  1. 各FBごとにノードセット(仮想ノードグループ)を作成する

  2. 新しいノードのcloud-init スクリプトにベースイメージを追加します。

  3. CD プロセスが開始する前に、必要な Docker イメージをノードにダウンロードするために、 initContainersセクションでDaemonSet を実行する事前デプロイ手順を追加します。


更新された CI/CD パイプラインは次のようになります。 更新されたCI/CDパイプライン


更新された CI/CD パイプライン:

  1. 初期ステップ
    1.1.(新しい手順)初期デプロイ: FB を初めて起動する場合は、ノード インスタンスの新しい個人セット (用語では仮想ノード グループまたは VNG) を作成し、メイン ブランチからすべての JS ベース イメージ (5~10 イメージ) をダウンロードします。FB をメイン ブランチからフォークしたので、これを行うのは妥当です。重要な点は、これがブロッキング操作ではないことです。
  2. ビルドステップ
  3. デプロイ前の手順 ECR から特定の FB タグが付いた最新の JS ベース イメージをダウンロードします。
    3.1.(新しいステップ)重要なポイント: ディスク負荷を軽減する必要があるため、これはブロッキング操作です。関連する各ノードのベースイメージを 1 つずつダウンロードします。
    ちなみに、「 init deploy」ステップに感謝します。メイン ブランチからの基本 Docker イメージがすでにあるので、最初の起動時にキャッシュをヒットする大きなチャンスが得られます。
  4. **展開する
    **このステップでは変更はありません。ただし、前のステップのおかげで、必要なノードにはすでにすべての重い Docker イメージ レイヤーが存在します。

初期デプロイ手順

CI パイプラインから API 呼び出し (サードパーティの自動スケーリング システムへ) を介して、各 FB の新しいノード セットを作成します


解決された問題:

  1. 分離: 各 FB には独自のノード セットがあり、環境が他の FB の影響を受けないことを保証します。

  2. 柔軟性: ノードタイプとその有効期間を簡単に変更できます。

  3. コスト効率: FB が削除されるとすぐにノードを削除できます。

  4. 透明性: ノードの使用状況とパフォーマンスを簡単に追跡できます (各ノードには FB に関連するタグがあります)。

  5. スポット インスタンスの効果的な使用方法: スポット インスタンスは、すでに事前定義されたベース イメージを使用して起動します。つまり、スポット ノードが起動すると、ノード上には (メイン ブランチからの) ベース イメージがすでに存在します。


cloud-initスクリプトを使用して、メイン ブランチからすべての JS ベース イメージを新しいノードにダウンロードします


イメージがバックグラウンドでダウンロードされている間も、CD プロセスは問題なく新しいイメージの構築を続行できます。さらに、このグループの次のノード (自動スケーリング システムによって作成されるノード) は、開始前にイメージをダウンロードする指示がすでに含まれた更新されたcloud-initデータを使用して作成されます。


解決された問題:

  1. 問題の解決: メイン ブランチからベース イメージのダウンロードを追加してcloud-initスクリプトを更新したため、ディスク プレッシャーはなくなりました。これにより、FB の最初の起動時にキャッシュにアクセスできるようになりました。

  2. スポット インスタンスの効果的な使用法: スポット インスタンスは更新されたcloud-initデータで起動します。つまり、スポット ノードが起動すると、ノード上にベース イメージ (メイン ブランチから) がすでに存在することになります。

  3. パフォーマンスの向上: CD プロセスは問題なく新しいイメージの構築を継続できます。


このアクションにより、CI/CD パイプラインに約 17 秒 (API 呼び出し) が追加されました。

このアクションは、FB を初めて起動するときのみ意味があります。次回は、前回のデプロイメントで配信したベース イメージが既に存在するノードにアプリをデプロイします。

展開前の手順

FB イメージはメイン ブランチ イメージとは異なるため、この手順が必要です。CD プロセスを開始する前に、FB ベース イメージをノードにダウンロードする必要があります。これにより、複数の重いイメージが同時にプルされたときに発生する可能性のある、コールド スタート時間の延長とディスク使用率の上昇を軽減できます。


事前デプロイ手順の目的

  1. ディスク負荷の防止: 最も重い Docker イメージを順番にダウンロードします。init-deploy ステップの後、ノード上にベース イメージがすでに存在するため、キャッシュにヒットする可能性が高くなります。

  2. デプロイメント効率の向上: ノードが必須の Docker イメージで事前に加熱されていることを確認し、POD の起動時間を短縮 (ほぼ即時) します。

  3. 安定性の向上: ErrImagePull / ContainerCreatingエラーが発生する可能性を最小限に抑え、システム デーモン セットが「準備完了」状態を維持するようにします。


このステップでは、CD プロセスに 10 ~ 15 分を追加します。

デプロイ前の手順の詳細:

  • CD では、 initContainersセクションを使用して DaemonSet を作成します。
  • initContainersセクションはメイン コンテナーが起動する前に実行され、メイン コンテナーが起動する前に必要なイメージがダウンロードされるようにします。
  • CD では、daemonSet のステータスを継続的にチェックしています。daemonSet が「準備完了」状態であれば、デプロイメントを続行します。そうでない場合は、daemonSet が準備完了になるまで待機します。

比較

予熱プロセスにおける元の手順と更新された手順の比較。

ステップ

初期デプロイ手順

展開前の手順

展開する

合計時間

差分

予熱なし

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 製品にどのような影響を与えるかを示す素晴らしい例を、もっともっと見ることを楽しみにしています。