はじめに
近年、機械学習を使ったアプリケーションの需要が高まっており、Kubernetes と GPU を組み合わせて使うパターンが多く存在します。その中で問題となることの 1 つが、コンテナイメージのサイズが大きくなることです。コンテナイメージのサイズが大きくなると、コンテナレジストリへの Push や Pull にかかる時間が増加し、コンテナの起動時間も長くなります。これらは結果的に、ワークロードの起動時間の増加につながります。起動時間の増加は、開発中の試行錯誤のサイクルの効率を下げ、開発者にとって大きな負担となります。
コンテナイメージを Pull する時間を削減するための方法として、Google Cloud のマネージド Kubernetes である GKE では、イメージストリーミングという機能が提供されています。これによって、コンテナイメージの Pull 時間を削減することができます。今回は、AWS の公式レポジトリで公開されている soci-snapshotter について調べます。soci-snapshotter は、GKE のイメージストリーミングと同様の機能を提供するツールです。
イメージ Pull の速度改善に取り組む意義
コンテナが起動するまでには、いくつかのプロセスがあります。大まかな流れは以下のようなものです。
- コンテナイメージのコンテナレジストリへの取得リクエスト
- レジストリからコンテナイメージの Pull
- コンテナの作成
今回注目するのは、2. のコンテナレジストリからコンテナイメージの Pull の部分です。Tyler Harter らの調査によると、このプロセスはコンテナ起動時間の 76% を占めているにもかかわらず、実際に使用されているデータは約 6.4% に過ぎないことが報告されています (Slacker: Fast Distribution with Lazy Docker Containers)。この調査結果から、コンテナイメージの Pull 速度の改善に取り組むことが、コンテナ起動の高速化につながると考えられます。次のセクションでは、実際にどのように高速化を図るのかを見ていきます。
速度改善の手法
コンテナイメージの Pull 速度を改善する手法としては、以下のようなものがあります。
- イメージの軽量化
- イメージのレイヤー数の削減
- コンテナランタイムの最適化
- コンテナイメージのプリロード(キャッシュ)
- Lazy Pull
この記事では、特に Lazy Pull について詳しく見ていきます。
Lazy Pull
コンテナイメージはレイヤー単位で構成されています。コンテナイメージを Pull するということは、これらのレイヤーを Pull することを意味します。 従来のコンテナイメージの Pull では、イメージ全体、つまりすべてのレイヤーをダウンロードする必要があります。コンテナイメージのサイズが大きくなるほど、Pull に時間がかかり、コンテナの起動時間のボトルネックとなります。
Lazy Pull は、コンテナイメージの必要なレイヤーのみを部分的に Pull し、準備ができ次第コンテナの起動を開始することで、起動時間の短縮を図る手法です。 コンテナイメージの Pull を担当しているのは、コンテナランタイムと呼ばれるコンポーネントです。下記の図の場合、containerd と呼ばれるコンテナランタイムが使用されています。 多くの Lazy Pull プロジェクトは、この containerd のプラグインとして実装されています。 より具体的に言うと、containerd 内の snapshotter と呼ばれるコンテナイメージの Pull を担当するコンポーネントのプラグインであるため、snapshotter プラグインとも呼ばれます。
参考: https://github.com/containerd/stargz-snapshotter/blob/main/docs/images/overview01.png
Lazy Pull を使ってコンテナ起動時間の改善に取り組んでいるプロジェクト
コンテナ起動時間改善に取り組んでいるプロジェクトをいくつか紹介します。
先述したようにいずれのプロジェクトも containerd の snapshotter プラグインの形で提供されており、
Lazy Pull の手法を用いてコンテナ起動時間の改善を図ろうとしています。どのコンテナイメージのフォーマットも OCI(Open Container Initiative)/ Docker 準拠なのも特徴の 1 つです。
- stargz-snapshotter
stargz-snapshotter は、OCI / Docker 準拠のイメージフォーマットを利用した snapshotter プラグインであり、eStargz という特殊なイメージフォーマットを採用しています。eStargz は、CRFS (Container Registry Filesystem) によって開発された stargz イメージフォーマットをベースにしつつ、ランタイムの最適化やコンテンツの検証などの追加機能を備えています。
stargz-snapshotter は、Lazy Pull の仕組みを利用することで、ワークロードの起動に必要なイメージのレイヤーを必要になったタイミングで Pull することができます。これによりコンテナの起動時間を短縮できます。
ただし、eStargz フォーマットを使用したイメージは、実行時のパフォーマンスが通常のイメージと比べて低下する可能性があることに注意が必要です。stargz-snapshotter は、OCI / Docker 準拠のイメージフォーマットを採用しているため、eStargz に対応していないランタイムでも動作可能であり、幅広い環境で利用できます。 - nydus-snapshotter
nydus-snapshotter は、RAFS (Registry Acceleration File System) と呼ばれるファイルシステムの形式を採用している snapshotter プラグインです。
RAFS は、チャンク単位でコンテンツを配置可能なファイルシステムです。 nydus-snapshotter は、 P2P ネットワークを介して RAFS 形式のイメージを取得します。このアプローチにより、コンテナの起動速度、イメージスペースの効率化、ネットワーク帯域幅の最適化、および複数のランタイムバックエンドとのデータの整合性が向上します。
さらに、nydus-snapshotter は、eStargz 形式のイメージの Lazy Pull にも対応しています。これにより、イメージの必要なレイヤーを選択的に Pull できるため、コンテナの起動時間をさらに短縮できます。 - soci-snapshotter
soci-snapshotter は、OCI / Docker 準拠のコンテナイメージをビルド時の変換なしで Lazy Pull できる containerd の snapshotter プラグインです。
soci-snapshotter は、コンテナイメージ全体のダウンロードを不要にし、データを必要に応じて Lazy Pull しつつ、バックグラウンドでプリフェッチする仕組みを提供します。元の コンテナイメージを変更する代わりに、”SOCI index” と呼ばれる別のインデックスアーティファクトを構築し、コンテナ起動時にコンテナレジストリに問い合わせます。SOCI index の構築手法に関しては後述しますが、別のコンポーネントによって作成します。あらかじめコンテナレジストリに Push しておき、コンテナ起動時に参照する方式です。
本記事では soci-snapshotter について詳しく取り扱います。
soci-snapshotter の概要
soci-snapshotter は stargz-snapshotter プロジェクトのフォークから始まり、開発中の変更が根本的であったため、独立したプロジェクトとして soci-snapshotter を作成することに決定されました。将来的には、非コアプロジェクトとして containerd に参加し、 CNCF のベストプラクティスに従うことを目指しています。
soci-snapshotter は OCI / Docker 準拠のコンテナイメージをビルド時の変換ステップなしで Lazy Pull することを可能にします。”SOCI” は “Seekable OCI” の略で、”so-CHEE” と発音します。containerd は CRI の基準に準拠したコンテナランタイムで、コンテナライフサイクルを管理することができます。様々な機能を提供しており、その機能をプラグインの形で機能を追加したり置き換えたりできます。そのうちの snapshot 機能にプラグインの形で soci-snapshotter を導入します。
参考: https://github.com/containerd/containerd/blob/main/docs/historical/design/architecture.png
現在開発されている既存のその他の Lazy Pull が可能な snapshotter プラグインは Pull 高速化のための特別なビルドステップが必要です。これは以下のようなデメリットを生じます。
- コンテナイメージ変換のビルドステップが必要
- CI/CD パイプラインを変更したくない、変更できない環境では運用が困難
- イメージ署名が変換後に保持できない
soci-snapshotter はこの問題を解決するために、別のアプローチを取ります。コンテナイメージを変換する代わりに、リモートレジストリにあるそれぞれのコンテナイメージに付随するインデックスアーティファクトである SOCI index を作成します。SOCI index は soci-snapshotter がどの順番でコンテナイメージのレイヤーを Pull してくるかが書かれたインデックスアーティファクトで、コンテナイメージと一緒にリモートレポジトリに保管されます。このアプローチによってビルドステップが必要なくなります。
また、現状未実装なのですが soci-snapshotter が掲げている特徴がワークロードに応じたコンテナイメージのレイヤーの Pull 順序の最適化です。既存の Lazy Pull が可能な snapshotter プラグインが用いるコンテナイメージに応じてレイヤーを並び替えて最適化する方法とは異なり、ワークロードに基づいてコンテナイメージのレイヤーの Pull 順序を最適化することを想定しています。これを実現するためには具体的には LOD (load order document) という実装をします。これはロードするファイルやファイルセグメントを指定するための別のアーティファクトで 1 つのイメージはワークロードに応じた複数の LOD を持つことができます。この実装は、例えば、Python3 のベースレイヤーを共有している複数のアプリケーションがある場合などに効果を発揮します。既存の Lazy Pull が可能な snapshotter プラグインはアプリケーションの数だけビルドしたコンテナイメージが必要になりますが、soci-snapshotter の LOD 機能ならばコンテナイメージは 1 つで済みます。
ECS の soci-snapshotter を使ったコンテナ起動時間の削減
ECS ではすでに soci-snapshotter を利用することができます。2023 年 7 月 17 日に AWS 公式のブログで Fargate を使った ECS のアプリケーションに対して利用することが可能になったことが発表されました。簡単に記事の内容を紹介します。
SOCI index を使用しコンテナ起動時間の高速化をするための準備として、以下の 2 つの方法を挙げています。
- AWS SOCI Index Builder を使う
AWS SOCI Index Builder は AWS が公開している SOCI index を作成するためのサーバレスアーキテクチャです。これを使用することによってコンテナイメージの SOCI index を作成し ECR レポジトリに Push するまでの一連の流れを自動的に行うことができます。詳細はこちらをご参照ください。 - SOCI index を手動で作成する
SOCI index を手動で作成する方法は soci-snapshotter によって提供される cli コマンド、 soci を使用して行います。こちらの手法はより柔軟な SOCI index の作成が行えます。
この記事では SOCI index を手動で作成する方法を採用しています。実際のコンテナ起動までの全体の手順としては以下のようになっています。
- ECR レポジトリの作成とコンテナイメージの Push
- SOCI index の作成
- SOCI index を ECR レポジトリへ Push
- パフォーマンス測定
1 つずつ見ていきます。
1.ECR レポジトリの作成とコンテナイメージの Push
ECS が利用するコンテナイメージと SOCI index を保管しておくための ECR レポジトリを作成します。
作成した ECR レポジトリに対して以下のコマンドでコンテナイメージを Push します。なお、コンテナイメージの操作には nerdctl を用いています。nerdctl は docker コマンドと互換性のある cli コマンドで docker コマンドのように使用することができます。詳細はこちらをご参照ください。 この例では pytorch のイメージを pytorch-soci
というタグを付けて ECR レポジトリへ Push しています。
$ ECRSOCIURI=xyz.dkr.ecr.us-east-1.amazonaws.com/pytorch-soci:latest
$ SAMPLE_IMAGE="763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:1.5.1-cpu-py36-ubuntu16.04"
$ aws ecr get-login-password --region us-east-1 | sudo nerdctl login --username AWS --password-stdin xyz.dkr.ecr.ap-southeast-1.amazonaws.com
# コンテナイメージをリモートレポジトリから Pull してくる
$ sudo nerdctl pull --platform linux/amd64 $SAMPLE_IMAGE
# Pull してきたコンテナイメージにタグを付ける
$ sudo nerdctl tag $SAMPLE_IMAGE $ECRSOCIURI
# ECR レポジトリへコンテナイメージを Push
$ sudo nerdctl push $ECRSOCIURI
2.SOCI index の作成
SOCI index は soci-snapshotter で Lazy Pull を行うためには必須のインデックスアーティファクトです。SOCI index によって soci-snapshotter はコンテナイメージのレイヤーの取得する順番を変えることができます。作成するには以下のコマンドを実行します。
# SOCI index の作成
$ sudo soci create $ECRSOCIURI
3.SOCI index を ECR レポジトリへ Push
先程の手順で SOCI index を作成できました。コマンドで確認することができます。
$ sudo soci index list
DIGEST SIZE IMAGE REF PLATFORM MEDIA TYPE CREATED
sha256:ea5c3489622d4e97d4ad5e300c8482c3d30b2be44a12c68779776014b15c5822 1931 xyz.dkr.ecr.us-east-1.amazonaws.com/pytorch-soci:latest linux/amd64 application/vnd.oci.image.manifest.v1+json 10m4s ago
sha256:ea5c3489622d4e97d4ad5e300c8482c3d30b2be44a12c68779776014b15c5822 1931 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:1.5.1-cpu-py36-ubuntu16.04 linux/amd64 application/vnd.oci.image.manifest.v1+json 10m4s ago
作成した SOCI index を ECR レポジトリに Push します。
$ PASSWORD=$(aws ecr get-login-password --region us-east-1)
$ sudo soci push --user AWS:$PASSWORD $ECRSOCIURI
4.パフォーマンス測定
通常のコンテナイメージの Pull と soci-snapshotter を用いたコンテナイメージの Pull 時間を比較するために SCOI index あり (pytorch-soci)、なし (pytorch-without-soci) の ECR レポジトリを用意しています。
準備ができたらパフォーマンスを測定するために ECS のタスクを Fargate タイプで作成します。 以下のコマンドによって作成できます。
$ aws ecs \
--region us-east-1 \
run-task \
--count 5 \
--launch-type FARGATE \
--task-definition arn:aws:ecs:us-east-1:XYZ:task-definition/pytorch-soci \
--cluster socidemo
$ aws ecs \
--region us-east-1 \
run-task \
--count 5 \
--launch-type FARGATE \
--task-definition arn:aws:ecs:us-east-1:XYZ:task-definition/pytorch-without-soci \
--cluster socidemo
このコマンドによって合計 10 個のタスクが出来上がります。この記事では各タスクの createdAt と startedAt を以下のスクリプトで集計しています。
#!/bin/bash
CLUSTER=<CLUSTER_NAME>
TASKDEF=<TASK_DEFINITION>
REGION="us-east-1"
TASKS=$(aws ecs list-tasks \
--cluster $CLUSTER \
--family $TASKDEF \
--region $REGION \
--query 'taskArns[*]' \
--output text)
aws ecs describe-tasks \
--tasks $TASKS \
--region $REGION \
--cluster $CLUSTER \
--query "tasks[] | reverse(sort_by(@, &createdAt)) | [].[{startedAt: startedAt, createdAt: createdAt, taskArn: taskArn}]" \
--output table
startedAt と createdAt の差分を平均した結果は、pytorch-without-soci は約 129 秒、pytorch-soci は約 60 秒になったそうです。コンテナの起動速度が約 50% 削減できました。
まとめ
コンテナ起動速度の改善は、アプリケーションの開発・運用において重要な課題の一つです。今回は、その解決策の一つである Lazy Pull という手法を見てきました。そして、いくつかの Lazy Pull のプロジェクトの中で soci-snapshotter にフォーカスを当て、コンテナの起動時間が約 50% 削減できた例を紹介しました。 コンテナ起動の高速化はアプリケーションの開発・運用の効率化につながります。
今後、soci-snapshotter のようなプロジェクトがさらに増え、マネージドサービスでも利用できるようになることが期待されます。これにより、開発者はインフラの詳細を意識することなく、高速かつ効率的にアプリケーションを開発・デプロイできるようになるでしょう。 コンテナ技術の発展とともに、その運用の効率化も重要な課題となっています。Lazy Pull はその一つの解決策ですが、今後もさまざまな工夫や新しい手法が生まれてくることが期待されます。
参考記事
- https://github.com/containerd/stargz-snapshotter
- https://github.com/containerd/nydus-snapshotter
- https://github.com/awslabs/soci-snapshotter
- https://aws.amazon.com/jp/about-aws/whats-new/2023/07/aws-fargate-container-startup-seekable-oci/
- https://aws-ia.github.io/cfn-ecr-aws-soci-index-builder/
- https://aws.amazon.com/jp/blogs/aws/aws-fargate-enables-faster-container-startup-using-seekable-oci/
- https://github.com/containerd/stargz-snapshotter/blob/main/docs/images/overview01.png
- https://github.com/containerd/containerd/blob/main/docs/historical/design/architecture.png