低速なイメージの中身　〜Dockerの『中間イメージ』と『レイヤー』の違い〜

イメージレイヤーとはまた別にレイヤーがあるそうです。

>レイヤーと中間イメージ
>
>レイヤーと中間イメージは別物です。
Dockerfile上でコマンド( EXPOSE , ENV , COPY , etc)を実行するたびに中間イメージが作成されます。
レイヤーは RUN , COPY , ADD の実行時に増やし、UnisonFileSystemへの影響があります。
基本的に、中間イメージではなくレイヤーを増やさないこと意識するのが良いでしょう。
>
> [イメージの仕組み - 入門Docker](https://y-ohgi.com/introduction-docker/3_production/image/#_1)

つまり、イメージレイヤーではないレイヤーを考慮しよう、というのがこの記事の趣旨です。

# 忙しい人のための結論

- 中間イメージは、Dockerのイメージの履歴。
- レイヤーはUnion FileSystemの履歴。

パフォーマンスに直接影響するのはレイヤーの方です。
中間イメージはイメージサイズに関係してきます。

:::note warn
なお、イメージサイズを軽視してよいということにはなりません。
イメージサイズはポータビリティや起動速度に関係し、CI/CDなどのシーンにおいて重要なファクターになります。
:::

:::note info
UnionFSにおいて無駄なレイヤーが低速化を招くことは事実ですが、DockerのUnionFSはキャッシュが働くため、必ずしもそうとは言い切れないようです。
:::

## 中間イメージとは

一般に、Dockerに代表されるコンテナシステムは軽量で高速であるよう設計されています。
その仕組の一つに、イメージキャッシュの仕組みがあります。
![Dockerイメージイメージ図 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72316/1f873e22-1646-f2ab-d482-3d453c318f02.png)
Dockerfile上でコマンドを実行するたびに、差分がキャッシュされ階層上に積み重なります。
この階層の一つ一つが中間イメージになります。
変更を加える際は既存のイメージを流用し、差分を積み重ねていきます。
キャッシュから最低限の差分変更のみでビルドできるため、非常に高速になります。
この階層を見るコマンドとして`docker-history`があります。

## Union FileSystem とは

ファイルシステムへの変更をレイヤー状に管理するシステムのことです。
ファイルシステムの一種であり、Docker特有のものではありません。
UnionFSと表現されることが多いようです。

ベースとなる状態に対して、各レイヤーのファイルの変更、削除だけを記録していきます。
そして全てのレイヤーを重ねて現在の状態として見せます。
レイヤーを削除すれば前の状態に戻すこともできます。
![Union FileSystemイメージ図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72316/0c48b28c-c87c-9a76-eb79-0466d39ca910.png)
<br>


UnionFSの世界では、ファイルを参照する場合はレイヤーを一つ一つ遡って該当のファイルを探します。
一方で書き込み時は、書き込み可能な最新の層にそのファイルを書き込みます。
この仕組みを **Copy on Write** と呼びます。
読んで字のごとく、書き込み時に初めてコピーされます。
![Union FileSystemイメージ図1 (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72316/d0c05852-5ca4-3cd2-9592-f478a21a80c4.png)


## Dockerの中間イメージとレイヤーの関係

Dockerの中間イメージは、Dockerfile上のコマンドごとに作成されますが、
UnionFSのレイヤーは`RUN`、`COPY`、`ADD`時に作成されます。
![Dockerイメージイメージ図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72316/4a89fd43-b105-ea3f-92b0-ac081de9ab56.png)

このように、中間イメージの一部とレイヤーは対応しています。

ファイルシステムへ変更を加えると、全ファイルを保存するのではなく
その差分だけがレイヤーに保存されるため、Dockerイメージは軽量なまま維持されます。

逆に言うと、大量の変更を加えたレイヤーを追加し、
その変更を取り消すレイヤーをさらに追加しても、イメージは軽くなりません。
最初のベースイメージの軽量さが最終的なイメージサイズに大きく影響すると言えます。


## 本題：無駄なレイヤーの積み重ねが低速なイメージを作る

基本的にイメージはRead Onlyです。
イメージからコンテナを起動してもこれは変わりません。

コンテナを起動した場合、Read Onlyなイメージ階層の上に
書込み可能なコンテナレイヤーが追加されます。
コンテナ上の変更は、全てこのコンテナレイヤーに書き込まれます。
![Dockerイメージ-UnionFS2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72316/8955e272-b169-586d-e770-e4db8502ad8b.png)

さて、UnionFSの世界では、
ファイル参照はレイヤーを順に遡る形で探索され、
ファイル変更時は対象のファイルを過去のレイヤーからコピーして書き込みます。

読み書きのたびにこのような操作が実施されるのであれば
積み重なったレイヤーがIOに大きな影響を及ぼすことでしょう。
> ファイルの更新がかかる度に捜査を実行するため、ログのように書き込みの激しいパスはDataVolumeを使用してUnison FileSystemを回避すると良いでしょう。
>
>[イメージの仕組み - 入門Docker](https://y-ohgi.com/introduction-docker/3_production/image/#_1)

しかし、これは一般的なUnionFSの話であり、
Dockerにはファイル操作の際に、一度読み込んだファイルをキャッシュする仕組みがあります。
そのため読み書きのオーバーヘッドは最小限になるように工夫されています。[^1]
[^1]: [コピーによりコンテナーを効率的に - Docker-docs-ja](https://docs.docker.jp/engine/userguide/storagedriver/imagesandcontainers.html#copying-makes-containers-efficient)

![Dockerイメージ-UnionFS3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72316/a20e69e9-d020-907b-e90d-447e55133f18.png)

一度読み込んだファイルは、キャッシュから読み書きを実施するため、
キャッシュしきれないほどの大量のファイルを操作するのでなければ、
神経質になる必要はないかもしれません。

:::note info
頻繁に書き込みが加えられる場合は、Docker volumeの使用を検討してください。
データ永続化の必要がなかったとしても、より効率的な入出力やイメージサイズ増加の抑制などのメリットを享受できます。
:::

## 結論

Dockerが構成するイメージレイヤーとUnionFSレイヤーとパフォーマンスの関係について解説しました。

Dockerはコマンドのたびに中間イメージを作成しますが、
UnionFSレイヤーの方がよりパフォーマンスに影響する可能性があります。

またパフォーマンス以外にも、ファイルシステムの変更を管理している関係上、
UnionFSレイヤーは、最終的なイメージサイズに大きく影響します。

`RUN`、`COPY`、`ADD`のコマンドが及ぼす影響は、覚えておいて損はないでしょう。