Dev Study
← 仮想化とクラウドの土台
レッスン 6 / 10·約 9 分·第8期 仮想化とクラウドの土台

イメージはレイヤ — copy-on-write と内容アドレス

アプリと DB の下に何が積まれているか(左が下、右が上)
  1. 物理機リージョン/AZ
  2. ハイパーバイザVM exit で捕まえる
  3. VMゲストOS
  4. コンテナnamespace / cgroup今ここ
  5. プロセスアプリ / DB

コンテナの中身(ファイル)はどう積まれているか。

まず結論

イメージは1枚の塊ではなく、**読み取り専用の差分(レイヤ)の積み重ね**です。OCI image-spec はレイヤを「変更集合」と定義し、各レイヤを**中身のハッシュ(digest)で名指し**します。

帰結は2つ。書き込みは下を書き換えず、上へ**丸ごとコピーしてから**行われること。削除も「消した」印を上に足すだけなので、**消したはずの中身は下のレイヤに残る**ことです。

? 読む前に予測

Dockerfile で `RUN echo secret > /s` の次に `RUN rm /s` と書いてイメージを作りました。配った相手は secret を読めるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

イメージは差分の積み重ねで、各段は中身のハッシュで呼ばれる

OCI image-spec は、イメージを manifest・image index(任意)・レイヤの集合・configuration と定義します。レイヤについて layer.md はこう書きます。

This document describes how to serialize a filesystem and filesystem changes like removed files into a blob called a layer. One or more layers are applied on top of each other to create a complete filesystem.

レイヤは完成品ではなく変更集合です。 変更は追加・変更・削除の3種だけ。追加と変更は tar の中で同じ形ですが、削除だけは違います。

Removals are represented using "whiteout" file entries

whiteout は「この名前は消えている」と示す空ファイルで、名前は .wh. に消したい名前をつないだもの。削除もまた1枚レイヤを足す操作であって、下を削る操作ではありません。

もう1つの軸が名前の付け方。manifest.md は第一の目標を content-addressable images と述べ、descriptor.md は digest を定義します。

The digest property of a Descriptor acts as a content identifier, enabling content addressability. It uniquely identifies content by taking a collision-resistant hash of the bytes.

値は sha256:6c3c624b58dbbcd3… のように アルゴリズム:符号化された値 の形。中身が名前を決めるので、同じ名前は同じ中身です。

イメージの層と書き込み(下が古い)
書き込み層起動時に足される
書くと copy_up
ここから下は読み取り専用
レイヤ3RUN rm /s → .wh.s(0バイト)
レイヤ2RUN echo secret > /s → s(7バイト)
中身は残る
レイヤ1土台のファイル群
他イメージと共有されうる
  • ・各レイヤは digest で名指しされ、同じ digest なら同じ中身
  • ・削除はレイヤを減らさず、上に印を1枚足す
具体実物

実機: rm したファイルは配布物に残っている

3行の Dockerfile をビルドして開きます。

Dockerfile(macOS 上の Docker Desktop 4.47.0 / Engine 28.4.0)設定ファイル
FROM alpine
RUN echo secret > /s
RUN rm /s

積まれた層を見ます。

$ docker history vc-d2-layersコマンド出力
IMAGE CREATED CREATED BY SIZE COMMENT
4693b3a34e4b Less than a second ago RUN /bin/sh -c rm /s # buildkit 4.1kB buildkit.dockerfile.v0
<missing> Less than a second ago RUN /bin/sh -c echo secret > /s # buildkit 8.19kB buildkit.dockerfile.v0
<missing> 3 months ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0
<missing> 3 months ago ADD alpine-minirootfs-3.24.1-aarch64.tar.gz … 9.31MB buildkit.dockerfile.v0

rm の行が 0B ではなく 4.1kB です。 削除がレイヤを1枚増やしています。中身は docker save で取り出して開きます。

$ docker save の各レイヤ tar を tar -tvf(レイヤ1は先頭5行)コマンド出力
--- blobs/sha256/5de55e5ef9c033997441461efe7ba23a986db059c0bb78b38f84ee0d72b99167
drwxr-xr-x 0 0 0 0 Jun 14 01:39 bin/
lrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/arch -> /bin/busybox
lrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/ash -> /bin/busybox
lrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/base64 -> /bin/busybox
lrwxrwxrwx 0 0 0 0 Jun 14 01:39 bin/bbconfig -> /bin/busybox
--- blobs/sha256/4c02f3d296199d54a837123f4ea9bf8c71fe00c381d235ea6936c70bd6170828
-rw-r--r-- 0 0 0 7 Sep 16 14:09 s
--- blobs/sha256/277cc3b360df047b717b4f9bf3034c63306f4a877e9e45e3ff5860e9187e5151
---------- 0 0 0 0 Jan 1 1970 .wh.s

2枚目に 7 バイトの s、3枚目に 0 バイトの .wh.s。s の中身は secret のままです。一方、起動して同じパスを見ると——

$ docker run --rm vc-d2-layers ls -l /sコマンド出力
ls: /s: No such file or directory
exit=1

実行時に見えないことと、配布物に入っていないことは別です。 whiteout は名前を隠す印で、下のレイヤを書き換えません。Docker の storage drivers の解説も、rm されたディレクトリは前のレイヤで依然として利用可能でイメージの総サイズに加算される、と書きます。

✓ ここまでの確認

`RUN rm /s` が作るレイヤのサイズは、実機でどうなっていましたか。

抽象原理

書き込みは「先にコピーしてから」行われる

なぜ下は変わらないのか。コンテナのファイルシステムを組み立てる overlayfs について、カーネルの文書はこう説明します。

An overlay filesystem combines two filesystems - an 'upper' filesystem and a 'lower' filesystem. When a name exists in both filesystems, the object in the 'upper' filesystem is visible while the object in the 'lower' filesystem is either hidden or, in the case of directories, merged with the 'upper' object.

同じ名前が両方にあれば上が勝ち、下は隠れる。lower は書き込み可能である必要すらありません。では書き込みが来たら——

When a file in the lower filesystem is accessed in a way that requires write-access, such as opening for write access, changing some metadata etc., the file is first copied from the lower filesystem to the upper filesystem (copy_up).

先にコピーしてから書く。 これが copy-on-write の中身です。削除も同じで、「lower を変更せずに rm と rmdir を支えるため」に upper へ削除の事実を記録する、と文書は述べます。これが overlayfs 側の whiteout です。

粒度も押さえます。overlayfs はファイル単位で働くので、1GB のファイルの 1 バイトを書き換えてもファイル全体が upper へ複製されます。カーネル文書が metacopy を「ファイル全体ではなくメタデータだけをコピーする」機能として別に定義するのも、既定がファイル全体だからです。

別の場所へ書いて、古い方を無効の印で覆う。 第11週『SSDはなぜ書き込みが複雑なのか — 消せる単位が大きすぎる』と同じ形です。あちらでは上書きができないので、別の空きページへ書いて古いページを「無効」と記録していました。

具体実物

対比: 中身で名指しし、古いものを残す仕組み

Git の内部構造コース『ハッシュ値とblobの対応 — Gitはファイル名ではなく中身を保存する』で同じ骨格を見ました。Git が保存するのは「ファイル」ではなく「中身」で、名前が違っても中身が同じなら blob は1個しか作られません。これが content-addressable と呼ばれる理由でした。

注記はこう続きます。古い blob は書き換えられずそのまま残る、Git のオブジェクトは一度作られたら変更されない(追記のみ)、だから過去のどの時点の中身も取り出せる、と。

削除をコミットしても blob は消えません。 消えるのは新しい木がそれを指さなくなることだけ。イメージの RUN rm と同じ形です。

✓ ここまでの確認

overlayfs 上のコンテナで、下のレイヤにある 1GB のファイルの 1 バイトだけを書き換えました。何が起きますか。

⇄ 2つを見比べる
具体例 A

イメージのレイヤ(OCI + overlayfs)

各レイヤは digest で名指しされた読み取り専用の変更集合。削除は whiteout の印を上に足す操作で、下の中身は残ります。書き込みは copy_up で上へ複製してから行います。

rm した後のイメージ(実機)表
見る場所 /s の状態
コンテナ内 ls -l /s No such file or directory
レイヤ2の tar s (7バイト、中身 secret)
レイヤ3の tar .wh.s (0バイト)
具体例 B

Git のオブジェクト(Git の内部構造コース)

blob は中身とヘッダだけから機械的に決まるハッシュで名指しされ、一度作られたら変更されません。削除をコミットしても過去の blob は残ります。

削除をコミットした後表
見る場所 そのファイルの状態
作業ツリー 存在しない
最新コミットの木 指していない
過去コミットの blob 中身のまま残る

Git の履歴から中身が消えないことと、下のレイヤから中身が消えないこと。共通する構造は何でしょうか。

「削除」が既存の記録に何をしていないかを考えてください。

この章のまとめ

  • OCI image-spec はレイヤを変更集合と定義する。変更は追加・変更・削除の3種で、削除だけが whiteout で表される。
  • 各レイヤは digest で名指しされる。中身のハッシュなので 同じ名前は同じ中身。手元で計算し直せば改変を検査できる。
  • 削除はレイヤを減らさず1枚増やす。 実機では RUN rm /s の層が 4.1kB、2枚目の層の tar に 7 バイトの s(中身 secret)が残った。実行時に見えないことと、配布物に入っていないことは別。
  • overlayfs の書き込みは copy_up。下のファイルを上へファイル単位で丸ごと複製してから書くので、下は変わらない。
  • OCI の whiteout(.wh. 付き空ファイル)と overlayfs の whiteout(0/0 のキャラクタデバイス等)は別表現。
  • タグは manifest への人間可読なポインタで、1つの digest に0個・1個・複数付く。名札は中身を決めない。

次は視点を上げ、これらが載る機械がどこにあるのかを見ます。手を動かすなら、クイズ群「仮想化とコンテナ隔離の原理」の**仮想化V(コンテナランタイムとOCI)と仮想化VI(イメージ層と隔離の限界)**がこの章をそのまま問います。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

OCI image-spec で「レイヤ」はどう定義されていますか。

用語集(4語)
レイヤ(layer)
OCI image-spec が定める、ファイルシステムの変更集合を直列化した blob。1枚以上を互いの上に適用して完全なファイルシステムになる。
digest
中身のバイト列の衝突耐性のあるハッシュから作られる内容識別子。中身が同じなら誰が計算しても同じ値になる。
whiteout
「この名前は削除されている」という印。OCI のレイヤ tar では `.wh.` 付きの空ファイル、overlayfs では 0/0 のキャラクタデバイスまたは xattr 付きの空ファイル。同じ語でも表現が違う。
copy_up
overlayfs で、下(lower)のファイルが書き込みを要する形で開かれたとき、上(upper)へ複製する処理。ファイル単位で行われる。