1 ポイント 投稿者 GN⁺ 2024-07-13 | 1件のコメント | WhatsAppで共有
  • コンテナイメージは通常、専用レジストリにアップロードするが、S3バケットを HTTPで公開し、ファイルを決められたパスに配置すれば、docker pull の対象のように使える
  • カスタムイメージビルダーで pull 可能なイメージを 数秒以内に作る工程がボトルネックになっており、レイヤーの push 時間が大きな割合を占めていた
  • 198 MiB レイヤーのアップロードベンチマークでは、ECRは24〜28 MiB/s、S3は115〜190 MiB/sと測定され、S3が最大 8倍高速だった
  • この差は、S3が単一レイヤーのチャンクを 並列アップロードできる一方で、OCI Distribution SpecベースのECRはチャンクを順次処理しなければならないことから生じる
  • この方式は、docker push、イメージの妥当性検証、セキュリティスキャン、プライベートリポジトリのアクセス制御といった レジストリ機能を代替できないため、実験的な最適化と見るべき

S3バケットで docker pull できる条件

  • S3をコンテナレジストリのように使うには、バケットを HTTPで公開し、Dockerが想定するパスにイメージファイルをアップロードする必要がある
  • 条件が合えば、通常のレジストリのように docker pull でイメージを取得できる
  • デモイメージは cowsay を実行し、バケットURLをイメージ名のように使って docker run --rm .../cowsay で動作する
  • デモには Cloudflare R2 が使われている
    • 無料の egress が選定理由
    • R2とS3はAPI互換性があるため、AWS SDKでR2にイメージをアップロードしている

なぜ専用レジストリではなくS3を検討したのか

  • コンテナイメージは通常、DockerHubGitHub Container RegistryECR のような 専用レジストリでホストする
  • カスタムイメージビルダーの目標は、要件から pull 可能なイメージまで数秒以内に到達すること
  • AWS環境ではECRが最も簡単な選択肢だが、実際のアップロード速度では S3とECRの差が大きく現れる
  • 性能最適化のためコードに実行トレース(trace)を入れたところ、レイヤーをコンテナレジストリに push する時間が大きなボトルネックだと確認された

198 MiB レイヤーのアップロード結果

  • 小規模なベンチマークでは、198 MiB レイヤーをECRとS3にそれぞれアップロードし、所要時間とスループットを比較した
  • 観測された速度は次のとおり
    • ECR: 最小 24 MiB/s、8.2秒
    • ECR: 最大 28 MiB/s、7.0秒
    • S3: 最小 115 MiB/s、1.7秒
    • S3: 最大 190 MiB/s、1.0秒
  • 結果としてS3はECRより最大 8倍高速な水準
  • 実験コードはAWS上で実行され、S3とECRはVPC経由で内部接続されており、パブリックインターネットを経由していない
    • この条件は、可能な限り良好なレイテンシと帯域幅を提供する

並列チャンクアップロードが生んだ速度差

  • S3アップロードでは、単一レイヤーのチャンクを 並列にアップロードできる
  • 十分な帯域幅があれば、並列チャンクアップロードによってスループットが大きく向上する
  • AWSドキュメントでも、帯域幅の使用を最大化するために並列チャンクアップロードを推奨している
  • ECRは OCI Distribution Spec を実装している
    • この仕様は、複数のレジストリで docker pulldocker push が動作するようにする標準
    • レイヤーの push は順次進める必要があり、チャンクアップロードを行う場合でも、前のチャンクが終わってから次のチャンクに進む必要がある
  • S3でも順次アップロードをテストすると、スループットは ECRと同程度まで下がる

docker pull の実際のリクエスト構造

  • docker pull の内部リクエストは、複数の HEADGET リクエストで構成される
  • 例の流れは次のとおり
    • イメージマニフェストの存在確認: HEAD /v2/my-image/manifests/latest
    • イメージマニフェストのダウンロード: GET /v2/my-image/manifests/latest
    • マニフェストハッシュで再度ダウンロード: GET /v2/my-image/manifests/sha256:...
    • イメージメタデータ blob のダウンロード: GET /v2/my-image/blobs/sha256:...
    • イメージレイヤー blob のダウンロード: GET /v2/my-image/blobs/sha256:...
  • 結局、docker pull は必要なファイルをHTTPでダウンロードする処理に近い
  • 静的ファイルサーバーが必要なファイルを想定されたパスに置き、各リクエストに合った Content-Type ヘッダーを設定すれば、コンテナイメージを pull できる
  • S3バケットはこの2つの条件を満たせるため、慎重に構成すればコンテナレジストリのように動作する

実験的アプローチの限界

  • この方式はまだ 実験的であり、さらに調査する前に強い結論を出すのは難しい
  • S3は厳密な意味でのコンテナレジストリではない
    • docker push はできない
    • docker pull できるのは、HTTPリクエスト構造と静的ファイル提供方式がうまく噛み合った結果
  • 既存のコンテナレジストリは、バケットにファイルを置く方式より多くの機能を提供する
    • 標準の push 方式でアップロードされたイメージが実際に有効だと信頼できる
    • レイヤーに対する自動セキュリティスキャンと警告を提供できる
    • プライベートリポジトリへのアクセス権限をネイティブに指定できる
  • 期待どおりに動作するなら、パブリックコンテナイメージをCloudflare R2でホストする方式も可能になるかもしれない

1件のコメント

 
GN⁺ 2024-07-13
Hacker Newsの意見
  • OCI Distribution Spec は、よく設計された仕様として読めないのが残念
    仕様上、レイヤーのプッシュは逐次的に行う必要があるため、チャンクでアップロードしても各チャンクが完了しないと次に進めない。DockerHubとGHCRでテストした限りでは、チャンクアップロード自体も結局壊れていて、クライアントは各blob/レイヤーを丸ごとアップロードすることが多い。仕様では、RFC7233形式に合わない Content-Range 値の形式も推奨している
    もちろんblob単位の並列性はあるが、blob内部の並列性はない。また、タグ一覧のページネーション標準化の機会を逃したのも不満。標準から関連文言が誤って削除されたことで [1]、レジストリごとにばらばらに実装されることになった
    [1] https://github.com/opencontainers/distribution-spec/issues/4...

    • Dockerとコンテナ周辺技術は、概してこういう感じ。ユーザー体験としてのDockerは素晴らしいが、技術としてはかなりひどい
      とはいえ、完全に非難したいわけではない。実際に革命的で、Linux名前空間の利用を以前よりはるかに簡単にし、世の中をより良い方向に変えた。ただ、常に技術的完成度よりもユーザー体験を優先してきたし、それ自体が必ずしも悪いわけでもない。PerlやFTPでやり取りするCSVで高価な問題を解いている退屈な会社が多いように、退屈だったり、時には悪い技術であっても、良いパッケージとして提供すれば大きな価値がある
      それでも時々、今よりずっと良くできたはずなのにと思って苦く感じる
    • これに加えて、OCI仕様の問題なのかAWSが特殊なのかは分からないが、GitLabやNexusと違って AWS ECR はフォルダの自動作成をサポートしていない
      たとえば .dkr.ecr..amazonaws.com/foo/bar/baz:tag のような形式は使えず、フラットな保存しかできないため、イメージ名やタグが過度に長くなる。理論上はTerraformでECRリポジトリオブジェクトを作成して似たようにまねることはできるが、結果のイメージパスが動的なパイプラインではいまいち。CIパイプラインのIAMロールに不便なほど多くの権限を与える必要があり、中央のTerraformリポジトリの外でAWSリソースが管理されるのも気に入らない
      [1] https://stackoverflow.com/questions/64232268/storing-images-...
  • CloudflareがR2を使うコンテナレジストリサーバーをオープンソースとして公開したことがある
    使ったことがある人がいるのか気になる
    [1]: https://github.com/cloudflare/serverless-registry

    • 良さそう。ただし、レイヤーあたり500MB制限があると書かれている
      用途によっては大きな問題ではないかもしれないが、別の場合には即座に候補から外れる条件になり得る
  • 投稿者です。OCI 仕様で、なぜレイヤーのプッシュは逐次的でなければならないのか、知っている人がいれば教えてほしいです
    単なる歴史的な偶然なのか、隠れた理由があるのか気になります。明確にすると、複数のレイヤーは当然並列にプッシュできますが、ここで言っているのは単一レイヤーの内容を逐次的にプッシュしなければならない部分です

    • 後片付けを単純にするためかもしれません。「最後の」チャンクまで到達していなければ、N+Timeout 後に完了していないアップロードであることが明確なので削除すればよい、ということです
      部分アップロードをどう扱うかという実装の詳細を単純化しているわけです。そうでなければ、各チャンクの終わりごとに他のチャンクがすべてあるか確認して完了処理をしなければなりません。ただしこれは実装の詳細であり、意味のある、あるいは意図された設計だったのかは疑わしいです。S3 方式はうまく機能しそうですし、以前、大きなイメージを配布していた会社で似たことをしたことがあります。月あたり GB あたり 0.10 ドルがかなり積み上がりました
      ECR の追加機能は失いますが、個人的にはそれらの機能はかなり限定的だと思っています
    • プッシュは扱ったことがありませんが、こういうアプローチはうれしいです。Docker 初期には使えるプライベートレジストリのコンテナがなかったので、イメージを nginx の背後に置いて pull していました。そのため記事を興味深く読みました
    • OCI 互換レジストリ [1] を実装したことがあり、仕様が複雑だったので、多くの部分は仕様よりもリファレンス実装 [2] の挙動に従いました
      クライアントが blob アップロードを完了するとき、blob 全体のダイジェストを提供する必要があります。この要件は、サーバーが受け取ったバイト列の完全性を検証できるようにするためのものに見えます。サーバーが最後の HTTP リクエストでようやくダイジェスト検査を開始するなら、それ以前の HTTP リクエストで既にストレージに書き込んだ blob の内容をすべて読み直さなければなりません。大きなレイヤーではこの遅延は許容しにくいかもしれません。特定のクライアント要件のために、150GiB の blob まで動作することを検証しました
      代わりに実装では、リクエストシーケンス全体にわたってダイジェスト計算を継続します。blob データをチャンク単位で受け取りながら、同時にダイジェスト計算と blob ストレージへのストリーミングを行います。各リクエストの間には、ダイジェスト計算の状態を Location ヘッダーでクライアントに返すアップロード URL の中にシリアライズします。おおよそこのコードで処理されています: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...
      リファレンス実装も同じアプローチを使っていると理解しています。ダイジェスト計算は逐次的にしかできないため、アップロードも逐次的に進める必要があります
      [1] https://github.com/sapcc/keppel
      [2] https://github.com/distribution/distribution
    • ブログ記事をありがとう。「この4か月、Outerbounds と協力してカスタムコンテナイメージビルダーを開発した」とありましたが、別記事のネタだとはいえ、少しでも詳細を教えてもらえるのか気になります
      GitHub リポジトリのリンクだけでもあるとうれしいです。背景としては、$PROGRAMMING_LANGUAGE の中からプログラム的に OCI イメージを作る方法を探している、あるいは自分で実装しようと考えているためです。Buildah のようにしたいのですが、コマンドラインインターフェースではなく、実際のプログラミング言語向けの API が欲しいです。もちろん Buildah をサブプロセスとして呼び出すこともできますが、やや面倒ですし、Buildah の内部状態とのやり取りやクリーンアップの問題にも気を配る必要があります。また、現在 Buildah は Mac もサポートしていません
    • はっきりした理由はすぐには思い浮かびませんが、負荷ベースなのかもしれません
      以前 Docker に並列プッシュを追加したような気がしますが、pull と push を混同しているのかもしれません。後から見ると、私の作業は最終的なプッシュではなく、検査の並列化の方でした。レイヤーがどのレイヤーの「上に」乗るのかを明示するなら、参照先の ID が既に存在している必要があるので、その可能性もあります
  • かなり面白いユースケースです
    個人的には Nexus をそのまま使っています。十分うまく動作し、OCI イメージから apt パッケージ、カスタム Maven、NuGet、npm リポジトリのようなものまでサポートしています。ただし設定とリソース使用量は少し厄介で、特にクリーンアップポリシー周りがそうです: https://www.sonatype.com/products/sonatype-nexus-repository
    それでも、docker pull が「単に」HEADGET リクエストのまとまりである点は本当に良いです。昔からうまく動いていたものをそのまま使い、無駄に複雑にしない、こうした常識的な判断をもっと多くの技術で見たいです。認証とクリーンアップ機能まで備えた単純なコンテナストレージがもっと多くないのは意外です。Nexus と Harbor は、実際に使うにはどちらもかなり複雑です

    • パッケージ用途だけに Gitea を使っています。Docker、npm、Python などを扱えます
      このスレッドで誰も言及していないのが意外です
  • CNCF の Distribution、旧 Docker Registry には、S3 から取得する CloudFront 署名付き URL でレジストリを支える機能が含まれています [1]
    https://distribution.github.io/distribution/storage-drivers/...

  • https://github.com/distribution/distribution は何が問題なのか気になります

    • これは以前見たことがなく、実際に S3 をサポートしてはいます。ただ、クライアントへのダウンロードを S3 から直接提供するのか、それとも S3 を自前のストレージバックエンドとして使うだけで、pull 時には事実上プロキシのように振る舞うのかが気になります
  • この方式はかなり高くつきそうに見えるので、記事でコストにも触れてくれていたらよかったと思う。S3 と R2 の両方が気になる

    • S3 標準ティアは、保存容量 1GB あたりのコスト基準では ECR の5分の1
      無料のインターネット向け送信トラフィックのコストは同じだが、公開 ECR リポジトリには AWS 内部利用向けの送信トラフィックを無料にする例外がある
    • コストは結局 S3 のコスト。リージョンとストレージティアによって異なるが、1GB あたりの保存コスト、GET/PUT コスト、帯域幅コストは AWS のウェブサイトで確認できる: https://aws.amazon.com/s3/pricing/
  • 開発ツール以外では Docker をあまり使わないが、なぜプライベートコンテナレジストリが存在しなければならないのか理解したことがない
    ただのレントシーキングに見える。自分で管理するイメージファイルのようなものを作って好きに使うのと比べて、実際にどんな利点があるのか気になる

    • 必ず使う必要はない。docker savedocker import を使えばいい
      docker save alpine:3.19 > alpine.tar
      docker load < alpine.tar
      ただし、その tar ファイルを管理しなければならず、すべてのシステムがその場所とアクセス方法を知っている必要がある。あるいは車輪の再発明をせず、Docker がすでに提供している方式を使えばいい
    • 世の中に公開しないイメージがある可能性は高い。そうしたイメージは通常、k8s クラスターや CI/CD ランナーのようなインフラからアクセス可能である必要がある
      そのため、自分でレジストリを作るか、誰かにお金を払って任せる必要がある。もちろん開発用途だけでイメージを使うなら、こうしたことはすべて意味がなく、開発マシンにイメージを保存しておけばよい
    • コードリポジトリの代わりにファイルをメールでやり取りしない理由と同じ
      以前のバージョンがすべてあり、複数の利用者が簡単にアクセスできる中央リポジトリが必要だから。アプリをビルドしたあと、実行され得るすべての場所にいちいち押し込みたくない。一度ビルドして中央リポジトリにプッシュし、すべての場所がそのリポジトリを参照するようにすればよい
      プライベートリポジトリのホスティングに必ずしもお金を払う必要はない。自分でホストできるツールはたくさんある
    • プライベートクラウドレジストリは、Docker イメージに関して必須の認証/認可要件があるプロジェクトで非常に有用
      環境ごとに Terraform、Bicep、Pulumi ですべて構成できる
    • それをどう管理するかも問題。公開イメージで使っているのと同じツールを使いたいなら、コンテナレジストリを運用すればよい
  • ECR は実際、イメージレイヤーを複数のパートに分けてアップロードできるように設計されているように見える
    関連する ECR API には、各イメージレイヤーのアップロード開始時に呼び出す InitiateLayerUpload API、各レイヤーチャンクごとに呼び出す UploadLayerPart API(最大 20MB)、レイヤーのアップロード後にイメージレイヤー参照を含むイメージマニフェストをプッシュする PutImage API がある。妙なのは、レイヤーチャンクをbase64 エンコードしてアップロードしなければならず、データが約 33% 増えること

    • その API を直接使ってみたが、残念ながらそこでも順序付きアップロードが要求された
  • ファイルパスの配置をエンドポイント制御の手段として使うアイデアは興味深い
    ただし、Docker-Content-Digest ヘッダーをどう扱うのかが気になる。必須ではないがレスポンスに含めることが推奨されており、多くのクライアントがこれを期待し、ヘッダーのないレイヤーを拒否する可能性がある。また、OCI 1.1 仕様のreferrers APIのような機能は取りこぼすかもしれない。実装はかなり厄介そう