Skip to content
Published on

containerd 2.3のEROFSネイティブレイヤー — アンパックをなくす代わりに何を差し出したか

シェア
Authors

はじめに — イメージプルが遅い理由は2つある

コンテナイメージのプルが遅いと言うとき、実は2つの異なる問題が一つの言葉にまとまっています。

一つはダウンロード時間です。レジストリから数GBを引っ張ってくるネットワークコストです。もう一つは展開時間です。tar+gzipをストリーミングで解凍しながら数万個のファイルをホストファイルシステムに一つずつ作り出すコストです。後者はネットワークではなくCPUとファイルシステムのメタデータジャーナルの問題なので、100Gbpsのリンクを敷いてもそのまま残ります。

ここ数年、この分野の話題の大半は最初の問題についてでした。eStargz、SOCI、Nydusのような遅延ロード(lazy pull)系は「全部受け取らず必要な断片だけ受け取ろう」と言い、AWSのSOCIはむしろ方向を変えて「とにかく並列で速く受け取ろう」というモードを追加しました。この系譜はコンテナランタイム比較の回で扱ったことがあります。

2026年4月30日に出たcontainerd 2.3は、2番目の問題を正面から突きます。発想は単純です — tarを展開してファイルシステムを作る代わりに、最初からファイルシステムをレイヤーとして配布しよう。 本稿は、それが実際どこまで来ているのかを、コードとレジストリのマニフェストで確認した記録です。

containerd 2.3で実際にマージされたもの

まず日付とPRを確定させておきます。EROFS自体は2.3で初めて出たわけではありません。

時点内容
containerd 2.1.0 (2025-05-07)EROFS snapshotterとdifferを初搭載 (#10705、2025-02-05マージ)
containerd 2.2.0 (2025-11-06)mount manager基盤の改善、tar indexモード、並列アンパック
containerd 2.3.0 (2026-04-30)ネイティブEROFSレイヤー — レジストリからEROFS blobをそのまま受信

2.3に入った断片はこうです。

  • #12567 (2026-01-06マージ) — media type application/vnd.erofs.layer.v1 を追加。PRの説明どおり「containerdがEROFSネイティブコンテナイメージを直接取得できるようにする」もの。
  • #13091 (2026-04-01マージ) — os.featureserofsがあれば、transfer serviceがEROFS snapshotter/differを選ぶように。
  • #12502 (2026-04-01マージ) — EROFS snapshotterにdm-verityサポートを追加。
  • #13185 (2026-04-12マージ) — application/vnd.erofs.layer.v1+zstd、zstdで包んだEROFSレイヤー。

参考までに、2.3.0はcontainerdのリリースポリシーが変わって最初のリリースでもあります。リリースノートの表現では、Kubernetesのスケジュールに合わせた約4か月周期に移行し、2.3はその体系での最初の年次LTSです。endoflife.dateが独立に同じ値を示しています — 2.3のリリースは2026-04-30、EOLは2028-04-30、LTS表記。付け加えると、同じ表でcontainerd 2.1はすでに2026-07-03にEOLを迎えています。

differのコードが語ること — アンパックが消える場所

EROFSの利得はベンチマークのグラフではなく、differのコードの数行に最も鮮明に現れます。containerd 2.3.0のplugins/diff/erofs/differ.goには3つの分岐があります。

1. 通常のOCI tarレイヤーを受け取ったときmkfs.erofsを起動し、tarをEROFSへリアルタイムに変換します。internal/erofsutils/mount.goConvertTarErofsが行うのはこれです。

args := append([]string{"--tar=f", "--aufs", "--quiet", "-Enoinline_data"}, mkfsExtraOpts...)
cmd := exec.CommandContext(ctx, "mkfs.erofs", args...)
cmd.Stdin = r

つまりtarストリームをmkfs.erofsの標準入力へ流し込みます。ファイルを一つずつホストFSに作るのではなくEROFS blob一つに焼き付けるので、overlayfs snapshotterのファイル単位の抽出よりメタデータジャーナルのトラフィックが少なく、GC時に数万個のファイルを消す必要もありません。これは2.1からの話です。

2. 圧縮なしのネイティブEROFSレイヤーを受け取ったとき — 2.3の新しい経路で、コードはこう終わります。

layerBlobPath := path.Join(layer, "layer.erofs")
// Allow copy file range when there is an uncompressed native EROFS layer
if fastcopy {
    f, err := os.Create(layerBlobPath)
    ...
    _, err = io.Copy(f, content.NewReader(ra))
    f.Close()
    ...
    return desc, nil
}

ここが核心です。mkfs.erofsも、tarパーサーも、ファイル単位の生成もありません。content storeのblobをlayer.erofsにコピーして終わりです。アンパック段階そのものがなくなったのではなく、blobコピー1回に縮約されたのです。そのあとsnapshotterがlayer.erofsfs/にマウントし、親レイヤーたちと合わせてoverlayfsマウントを返します。

3. +zstdレイヤーを受け取ったとき — 後段で別途見ます。

レジストリで直接確かめてみる

ドキュメントを読むだけでは面白くないので、実際のレジストリを突いてみました。EROFSメンテナーがテスト用に上げているdocker.io/hsiangkao/ubuntuリポジトリに関連タグがあります。以下は2026-07-16にregistry-1.docker.ioから直接取得した結果です。

22.04-platformsタグのOCI indexを開くと、amd64マニフェストが2つ入っています。

{
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    { "digest": "sha256:1c4cc37c...",
      "platform": { "architecture": "amd64", "os": "linux" } },
    { "digest": "sha256:06ae2788...",
      "platform": { "architecture": "amd64", "os": "linux",
                    "os.features": ["erofs"] } }
  ]
}

同じアーキテクチャ、同じOSなのにos.featuresだけ違います。それぞれのマニフェストのレイヤーを開くと、media typeが分かれます。

形式media typeレイヤーサイズ
標準tar+gzipapplication/vnd.oci.image.layer.v1.tar+gzip29,536,798 B (28.17 MiB)
EROFSネイティブ (無圧縮)application/vnd.erofs.layer.v145,289,472 B (43.19 MiB)
EROFS + zstdapplication/vnd.erofs.layer.v1+zstd27,214,834 B (25.95 MiB)

トレードオフはこの3行にすべて入っています。無圧縮EROFSはtar+gzip比でワイヤーバイトが約1.53倍です — アンパックをなくす対価としてネットワークを53パーセント多く使う計算です。zstdで包むと、むしろtar+gzipより8パーセントほど小さくなります。

正直に添えておくべき注記があります。これはベンチマークではなく、マニフェスト1個を開いてみただけです。イメージは1つ(ubuntu 22.04ベース、単一レイヤー)、しかもEROFSプロジェクト自身がアップロードしたテストアーティファクトであり、EROFS blobの中身がtarレイヤーとバイト単位で同一のrootfsかどうかは私は確認していません。圧縮率はイメージの中身によって大きく揺れます。ここで読み取るべきは正確な比率ではなく符号 — 無圧縮ネイティブは大きくなり、zstdをかぶせると再び小さくなる、という方向性です。

os.features — 1つのタグで2つの世界を提供する

上のindexがなぜ優れているかを見てみます。クライアントがEROFSを使えなければ普通のtar+gzipマニフェストを選び、使えればEROFSマニフェストを選びます。同じタグ、同じpullコマンドです。ctr i pull --platform="linux(+erofs)" docker.io/hsiangkao/ubuntu:22.04-platformsのような構文がPRの説明に出てきます。

ではそもそもos.featuresはこの用途のために作られたのでしょうか。OCI image-specのimage-indexドキュメントを読むと、こう定義されています — このOPTIONALなプロパティは必須OS機能の文字列配列であり、oswindowsのとき実装はwin32kという値を理解すべき(SHOULD)である。そして決定的な一文が続きます。

oswindowsでないとき、値は実装定義(implementation-defined)であり、標準化のためにこの仕様へ提出されるべき(SHOULD)である。

つまりLinuxにおけるos.featuresは、仕様が「各自で使いつつ標準化しに来い」と開けておいた扉であり、containerdはその扉から入ったわけです。まだ提出はされていません。

誤解しやすい点が一つ — os.featuresのサポートは自動スイッチではありません。 PR #13091の説明が自らそう釘を刺しています。

ユーザーはEROFSネイティブイメージを実行するために依然としてEROFS snapshotterを明示的に指定する必要があり、これは設計上そうなっている。これはアンパック過程のユーザー体験を改善するだけである。

デフォルトのsnapshotterで動かしていてEROFSイメージに出会ったとき、overlayfsが吐く得体の知れない抽出エラーの代わりに、明確なエラーが出るようになる — 改善の実体はそれだけです。

+zstd — 圧縮は取り戻したが、アンパック回避は半分だけ

無圧縮ネイティブレイヤーの53パーセントのペナルティは痛く、そこで出てきたのが+zstdです。発想は#12506 RFCにうまく整理されています — ファイルシステム自体は無圧縮のままにして(ランタイム読み取りが速いように)、転送時だけzstdで包む(展開が速いように)。

ところが2.3.0に実際に入ったコードを見ると、+zstdの経路はfastcopy分岐を通りません。標準のzstdプロセッサーチェーンを通過したあとlayer.erofsへコピーされます。

processor := diff.NewProcessorChain(diffLayerType, content.NewReader(ra))
...
if native {
    f, err := os.Create(layerBlobPath)
    _, err = io.Copy(f, rc)
    ...
}

まとめるとこうです。

  • 依然としてmkfs.erofsは動きません — tarのパースもファイル単位の生成もありません。
  • しかし全ストリームの展開パスが戻ってきました。 27MBを受け取り43MBに展開してディスクに書きます。

つまり+zstdは無圧縮ネイティブとtar+gzipのあいだのどこかです。参考までに、differは+zstdだけを受け付け、他の接尾辞は拒否します(unsupported erofs layer suffix)。+gzipは最初から検討対象になっておらず、PR #13185の説明によれば、zstdにskippable frameがあるためです。この点が次の節につながります。

遅延ロードはまだありません

ここが本稿でもっとも重要な部分です。

#12703はEROFSレイヤーフォーマットを文書化しようとする提案です。第一節の動機リストの一番上にこう書かれています — 「ランタイムのランダムアクセス(遅延ロード): コンテナはイメージ全体がダウンロードまたは展開されるのを待たずに即座に起動でき、データはオンデマンドで取得される。」

設計も具体的です。zstdのskippable frame(マジック0x184D2A5E)の中にChunk Mapping Tableを入れ、標準のzstdデコーダーはそれを単に読み飛ばし、対応するリーダーだけが非圧縮オフセットを圧縮範囲へマッピングして特定のチャンクを取得する構造です。ヘッダーにはマジック/バージョン/全体サイズ/チャンクサイズ(例: 4MiB)/ハッシュアルゴリズムが入り、チャンクごとにblob内の絶対オフセットと任意のチェックサムが付きます。必要なメタデータはOCIマニフェストのアノテーションで渡します — dev.containerd.erofs.zstd.chunk_table_offsetdev.containerd.erofs.zstd.chunk_digestdev.containerd.erofs.dmverity.root_digestなど。

問題は、この提案が2025-12-18に開かれてからまだマージされていないことです。本文の最初の一行が「This is a draft proposal, I'm opening as PR rather than issue to make it easier to comment」です。

実装側はさらに決定的です。#12764「Seekable-erofs」が2026-01-09に開かれ2026-04-13に最後に更新され、今もマージされていません。PR本文に実装済みリストと未実装リストが並んでいて、未実装のほうはこうです。

Does NOT implement:

  • lazy-loading: the chunk table is written but not consumed
  • dm-verity usage: the dm-verity data is decoded but yet not used in the differ / at mount time

チャンクテーブルを書きはするが読みはしません。 つまり2026年7月現在、containerdのEROFS経路に遅延ロードはありません。提案の見出し文句と実際に船積みされたものとの距離はこれだけあります。

そのため、containerd 2.3のEROFSネイティブレイヤーを一言でまとめるとこうなります — 「怠惰に受け取る」のではなく「展開しない」なのです。 イメージ全体を事前に受け取るという事実はそのままで、消えたのはアンパックです。悪いことではありません。ただ別の問題を解いているのであり、「イメージプルが速くなる」という期待を持って有効化すると外れます。

dm-verity — 静かにより興味深いほう

遅延ロードの話に埋もれがちですが、2.3で実際に完成して入ったのは整合性まわりです。

EROFS snapshotterはいま3種類の整合性手段を提供します。

  • set_immutable — レイヤーblobにIMMUTABLE_FLを設定し、削除・改名・変更を防ぎ、dirtyなデータを即座にflushします。ハードウェア障害によるデータ破損は捕まえられません。代償があり、containerdのドキュメントが自前の測定値を記載しています — EXT4でのtensorflow:2.19.0の展開時間が10.090秒から21.074秒へ108.86パーセント増加。プロジェクト自身の測定であり、条件はそのイメージ・そのファイルシステムのみで、ランタイム性能には影響しないと明記されています。
  • enable_fsverity — commit時点でfs-verityを有効化し、マウント前に状態を検証します。ドキュメントが率直に書いているとおり、コンテナのすべてのイメージ読み取りが先にマークルハッシュツリーを検証する必要があるため、ランタイムのオーバーヘッドが付きます。
  • dmverity_mode#12502で入った新参です。レイヤーごとにdm-verityデバイスを作り、read-onlyでマウントします。

dm-verityの実装はかなりきれいです。veritysetup CLIを呼ぶ代わりに、containerd/go-dmverityライブラリでマークルツリーを直接作り、ツリーをblob内にインラインで付けたうえで、ルートハッシュとオフセットだけをlayer.erofs.dmverityのJSONに別途持たせます。残りのパラメーターはblob内のスーパーブロックにあり、マウント時に自動検出されます。ブロックサイズは通常モードで4096バイト、tar-indexモードで512バイト(dm-verityのlogical_block_sizeの制約)です。

モードは3つです。auto(デフォルト、.dmverityメタデータがあれば使い、なければ通常のEROFSとしてマウント)、on(全レイヤー強制、なければ失敗)、off(完全無効)。

ここに罠があります。ドキュメントが太字で警告しているのですが、すでにdm-verityなしで展開済みのレイヤーがある状態でdmverity_mode = "on"を有効にすると、それらのレイヤーには.dmverityファイルがなくマウントが失敗します。スナップショットを整理し、differのenable_dmverity = trueとともに再度pullする必要があります。既存ノードへのロールアウトを考えているなら、これは単なる罠ではなく計画すべきマイグレーションです。

提案#12703はここからさらに一歩進みます — dm-verityデータがあれば、そのルートハッシュをレイヤーのDiffIDとして使うべきだというものです。カーネルがブロック単位で検証する値とイメージconfigが指す値が同じになるという意味ですが、まだ提案です。

支払うべきコスト

有効化する前に知っておくべきことをまとめます。大半はcontainerdのEROFS snapshotterドキュメントにあります。

カーネル。 EROFSモジュールが必要で、Linux 5.4以上です(modprobe erofs)。ただし本当に欲しいのはfile-backed mountで、それにはLinux 6.12以上が必要です。それより下ではloopデバイスを経由することになります。dm-verityを使うにはCONFIG_DM_VERITYとdevice-mapperモジュールが追加で必要です。

外部バイナリ。 ネイティブレイヤーではない普通のイメージをEROFSへ変換するにはmkfs.erofsが必要です。ドキュメント基準でerofs-utils 1.7以上、再現可能ビルド用の-T0 --mkfs-timeは1.8以上、不要なtar再整列を避ける--sort=noneは1.8.2以上。つまりcontainerdバイナリ1つでは完結せず、ノードイメージにパッケージ依存が1つ増えます。

明示的な指定。 EROFS snapshotterはデフォルトではありません。--snapshotter erofsを渡すか設定で指定する必要があり、differの優先順位も触る必要があります(default = ["erofs","walking"])。

メンテナー自身のラベル。 containerdのEROFSトラッキングissue #11340は、ネイティブblob関連のPR群をこうまとめています — "Native EROFS blob support in content store and image (experimental)"。実験的だというのは私の評価ではなく、プロジェクトの表記です。同じリストで、go-erofsネイティブライブラリの統合はまだ未完です。

エコシステム。 これがおそらく最大の壁です。今レジストリにあるEROFSネイティブイメージは、事実上プロジェクト関係者がアップロードしたテストアーティファクトです。標準ビルドパイプラインはEROFSレイヤーを吐かず、主要なレジストリ・スキャナー・署名ツールがこのベンダーmedia typeを理解する保証もありません。イメージビルドパイプライン全般はDocker BuildKitとイメージレイヤーの回で扱いましたが、あちらの世界はまだtarです。

ベンチマークは確認できませんでした。 containerdのドキュメントにはTop25イメージと大型AIイメージの展開ベンチマークが載っていますが、数値がPNG画像の中にしかなく、テキストとしては出てきません。しかも条件はドキュメントに記載のとおりcontainerd 2.2.1、ローカルレジストリ、並列アンパックなしです。確認できない数字は引用しません。ウェブ検索のスニペットに「WordPressイメージ14パーセント改善」のような値が出回っていますが、原文のドキュメントを取得してgrepしてもそのような文はありません。

標準はどこにあるのか

一歩引いて見ると、この話の本当の主題はEROFSではなくOCI標準化の速度です。

containerdが使ったmedia typeはapplication/vnd.erofs.layer.v1です。org.opencontainers.*ではありません。アノテーションもdev.containerd.*で、提案#12703が脚注にこう書いています — "Future Goal: 採用が広がればdev.containerd.*アノテーションを標準化されたネームスペース(例: org.opencontainers.*)へ移行する。"

ではOCI側の扉はどんな状態でしょうか。image-specリポジトリの関連issueを日付とともに並べると、絵が鮮明になります。

issue開始日状態
#815 eStargz仕様の追加(遅延プルサポート)2020-12-09open、最終更新2021-10-21、コメント2件
#936 圧縮tarをランダムアクセス形式に置き換える2022-08-15open
#1191 erofs/squashfsレイヤーmedia typeの追加2024-05-29open、コメント18件

遅延プルを仕様に入れようという提案が2020年12月に開かれて2021年10月以降5年近く手つかずでした。EROFSレイヤータイプのissueは2年以上開いたままです。#1191の問題設定は今読んでもそのまま有効です — 大きなアーティファクトは(1)ダウンロードに時間がかかりすぎる、(2)展開に時間がかかりすぎる、そしてこのissueは(2)を扱い始めるものだが — その「始め」がもう2年目です。

だからcontainerdは待たずにベンダー型で先に撃ったわけです。これは非難ではなく観察です。実際に動く実装があってこそ標準化の議論に肉がつくのも事実だからです。ただしこれを使うということは標準ではないものに依存するという意味であり、標準化の過程でmedia typeやアノテーション名が変わりうるという意味でもあります。提案自身がその可能性を記しています。

では、今有効にすべきか

試す価値がある場合

  • ノードでアンパックが実際のボトルネックであることが計測で確認できている場合。数GB級のAIイメージでファイル数が多く、CPU/IOがアンパックに削られている状況なら、2.1からあるEROFS snapshotter + differだけでも(ネイティブレイヤーなしで)得るものがあります。mkfs.erofsでblobを焼くほうがファイル単位の抽出より有利で、GCもファイル数万個の削除ではなくblob1個の削除で済みます。
  • VMベースのランタイムを使う場合。containerdのドキュメントはこの用途を明示的に挙げています — KataのようなVMコンテナで、イメージレイヤーをvirtiofsや9pの代わりにEROFSでパススルーすれば性能とメモリフットプリントが有利になり、gVisorもEROFSをサポートします。
  • ブロック単位の整合性が要件である場合。dm-verityやfs-verityをレイヤーごとに強制する必要がある規制環境なら、これはoverlayfs snapshotterではうまくいかないことです。ドキュメントが理由を説明していて、FS_IMMUTABLE_FLとfs-verityはサブツリーではなく個別ファイルを保護する手段なので、ファイルが数万個あるoverlayfsレイヤーでは効率が出ません。EROFSはレイヤーがファイル1つなので自然に合います。
  • 書き込みレイヤーにディスククォータをかける必要がある場合。default_sizeで固定サイズのブロックデバイスをoverlayfsのupperとして渡せます。

まだ早い場合

  • 問題がアンパックではなくダウンロードである場合。EROFSネイティブレイヤーはここでは助けにならず、無圧縮ならむしろ有害です。遅延ロードが必要ならeStargz/SOCI/Nydus側を検討するか、単に並列で速く受け取る方向を検討してください。
  • ネイティブEROFSレイヤーを配布したい場合。ビルドパイプラインがなく、media typeが標準ではなく、レジストリ周辺のツール群が理解する保証がありません。
  • ノードのカーネルが6.12未満、あるいはノードイメージにerofs-utilsを入れるのが難しい場合。
  • experimentalの札が負担になる場合。プロダクションでこのラベルの意味は「設定面とオンディスクフォーマットがマイナーリリースで変わりうる」ということです。

一つ付け加えると、EROFS snapshotterを有効にすることとEROFSネイティブレイヤーを受け取ることは別の話です。前者は2.1からあり、今日にでも評価する価値があります。後者が2.3の新しい話であり、まだ実験的です。この二つを混ぜて判断してはいけません。containerdのイメージ処理全般が気になる方はcontainerdイメージ管理の回も参考にしてください。

おわりに

まとめるとこうです。containerd 2.3(2026-04-30、最初の年次LTS)はapplication/vnd.erofs.layer.v1+zstdのmedia typeを追加し、レジストリからEROFSファイルシステムイメージをそのまま受け取ってマウントする経路を開きました。無圧縮のネイティブレイヤーのアンパックはコード上blobコピー1回で終わり、os.featuresによって1つのタグがEROFS対応クライアントと通常のクライアントを同時に提供でき、dm-verityによってレイヤーごとにブロック単位の整合性をかけられます。

その代わり遅延ロードはありません — チャンクテーブルの設計は提案にあり、実装PRは開いたままで、そのPR自身が「テーブルは書くが消費しない」と記しています。media typeはベンダー型で、OCI側の扉は2年以上開いたままです。無圧縮ならワイヤーバイトを多く使い、zstdで取り戻せば展開パスが戻ってきます。カーネル6.12とerofs-utilsが必要で、メンテナーがexperimentalと表記しています。

本稿から一つだけ持ち帰るなら、これであってほしいと思います — コンテナイメージ最適化の話で「速い」という言葉が出てきたら、ダウンロードが速いのか展開が速いのかをまず問うてください。 この5年間、この分野の話題の大半は前者であり、containerd 2.3が実際に船積みしたのは後者です。あなたのノードでどちらが痛いのかは、リリースノートではなくプロファイルが答えます。

参考資料