🐳

DockerのLayerFSとキャッシュの仕組みを理解する

2024/11/04に公開

はじめに

Dockerは、効率的な開発環境を提供するために、レイヤー化されたファイルシステム(LayerFS)とキャッシュ機能を活用しています。今回、Dockerイメージの最適化を考える中で、LayerFSについて調査する機会がありました。そのため、ここではLayerFSやキャッシュの仕組み、ファイル操作がディレクトリの状態にどのような影響を及ぼすか、パフォーマンス向上のためのベストプラクティス、LayerFSのデバッグ方法についてまとめています。

対象読者

  • Dockerの基礎を理解しており、LayerFSの詳細に興味があるエンジニア
  • Dockerイメージのビルド効率や、レイヤー構造の最適化を目指している方

1. DockerのLayerFSとは?

LayerFS(Layered File System)は、Dockerがイメージをレイヤー単位で管理するファイルシステムとして使用されています。Dockerはoverlay2ドライバを活用し、各レイヤーに変更があった場合のみ新しいレイヤーを追加する仕組みです。このアプローチにより、ビルド効率が向上し、ディスクスペースも節約されるというメリットがあります。

LayerFSの構造には以下のディレクトリが含まれます。

  • LowerDir: 読み取り専用のイメージレイヤーで、イメージの各ステップがLowerDirとして積み上げられ、基本的なファイルや基盤構造が含まれるディレクトリです。
  • UpperDir: 書き込み可能なコンテナレイヤーで、コンテナ内で行われた変更がここに記録されます。
  • MergedDir: LowerDirとUpperDirを統合した結果を表示する仮想的なファイルシステムで、コンテナ内からはこのディレクトリを介してファイルシステム全体を確認することができます。
  • WorkDir: MergedDirの構築に必要な一時ファイルが保存される作業ディレクトリです。

以下の図は、LayerFSのディレクトリ構造と各レイヤーの関係を示しています。

├── LowerDir
│   ├── 基盤ファイル
├── UpperDir
│   ├── 変更されたファイル
├── MergedDir
│   ├── LowerDirとUpperDirの統合結果
└── WorkDir
    └── 作業ディレクトリ

LayerFS構造の図
図: LayerFS構造図(出典:Docker公式ドキュメント


2. 環境別のLayerFSの確認方法

DockerはLinux環境で最も効率的に動作しますが、WindowsやMacの場合は仮想化を通じてLinuxコンテナが稼働しています。そのため、overlay2ドライバの詳細を確認する際には、Linux環境が適していると言えます。MacやWindowsの場合、以下の手順で仮想環境にアクセスし、overlay2の構造を確認することが可能です。

  1. MacやWindowsから仮想環境へのアクセス:

    docker run --rm -it --privileged --pid=host justincormack/nsenter1
    

    このコマンドを実行すると、仮想環境に入ってDockerのファイルシステムにアクセスできるようになります。

  2. Linux環境でのドライバ確認:
    Linux環境では、Dockerがoverlay2ストレージドライバを使用しているかどうかを確認するため、次のコマンドを使用します。

    docker info | grep "Storage Driver"
    

    出力例:

    Storage Driver: overlay2
    

3. DockerのLayerFSにおけるファイルやディレクトリの変更

LayerFSではファイルやディレクトリに対する変更に関して具体的な動作パターンが存在し、LowerDirUpperDirに基づいて動作が異なります。以下に、代表的なケースごとにLayerFSがどのように動作するかを整理しています。

1. ファイルの初回書き込み(copy-up)

ファイルが初めて変更される場合、そのファイルがLowerDirに存在していれば、UpperDirにそのファイルがコピーされ、そこに変更が加えられます。この「copy-up」操作は初回のみ発生し、それ以降はUpperDir内のファイルが参照されるようになります。

: 設定ファイル/etc/config.yamlを変更する場合の動作

LowerDir: /etc/config.yaml (元のファイル)
UpperDir: /etc/config.yaml (copy-upされたファイル)
  • 変更前: LowerDir内のconfig.yamlが参照されています。
  • 初回変更時: config.yamlがUpperDirにコピーされ、以降はUpperDirのファイルに変更が反映されます。

2. ファイルの削除

LowerDirにあるファイルを削除すると、直接削除されるのではなく、UpperDirに「ホワイトアウトファイル」と呼ばれる特別なファイルが作成されます。これにより、LowerDirのファイルは削除されたように扱われ、MergedDirからは見えなくなります。

: /etc/config.yamlを削除する場合

UpperDir: /etc/config.yaml(ホワイトアウトファイルとしてマーク)
  • 削除前: config.yamlはLowerDirに存在し、MergedDirで参照可能。
  • 削除後: UpperDirにホワイトアウトファイルが作成され、MergedDirにはconfig.yamlが存在しないように見えます。

3. ディレクトリのリネーム(名前変更)

LayerFSではLowerDir内のディレクトリを直接リネームすることはできません。LowerDirが読み取り専用であるため、リネーム操作は以下のように処理されます。

  • ディレクトリのコピーと再配置
    リネーム対象のディレクトリがLowerDirにある場合、UpperDirに新しい名前でコピーされます。これにより、UpperDir内でのみ「新しい名前」で参照可能となります。

  • 元のディレクトリの隠蔽(ホワイトアウトファイル)
    元のディレクトリを見えなくするために、UpperDirにホワイトアウトファイルが作成され、元のディレクトリが削除されたように見えます。

: /var/logs/var/old_logsにリネームする場合

  1. リネーム前の状態

    • LowerDirに/var/logsが存在。
    • UpperDirにはまだ存在しません。
  2. リネーム操作の実行
    コンテナ内で以下のコマンドを実行します。

    mv /var/logs /var/old_logs
    
  3. リネーム処理の内部動作

    • logsディレクトリがold_logsとしてUpperDirにコピー。
    • UpperDirにホワイトアウトファイルが作成され、LowerDirのlogsディレクトリが削除されたように見えます。
  4. リネーム後の状態

    • コンテナ内からは/var/old_logsのみが表示され、/var/logsは削除されたように見えます。
    • 内部的にはUpperDirに新しいディレクトリとホワイトアウトファイルが作成され、リネーム後の状態を再現しています。

4. DockerのLayerFSにおけるファイル操作によるディレクトリ状態の変化

Dockerコンテナ内でファイル操作を行うと、UpperDirMergedDirがどのように変化するのかについて、Upper Layerの作成および削除のタイミングを踏まえながら、以下のステップごとに確認しています。

1. コンテナの起動とLower Layerの確認

まず、コンテナを起動し、初期状態のディレクトリ構成を確認します。

docker run -it --name demo-container イメージ名 /bin/bash

この時点で、LowerDirにはイメージレイヤーのファイルが含まれ、UpperDirにはまだ何も変更がありません。

2. Upper Layerの作成確認とUpperDirパスの特定

次に、コンテナ内で新しいファイルを作成します。この操作により、Upper Layerに変更内容が記録されます。

touch /tmp/samplefile

別のターミナルでdocker diffコマンドを使用して変更内容がUpperDirに保存されたことを確認します。

docker diff demo-container

出力例:

A /tmp/samplefile

この出力から、/tmp/samplefileがUpper Layerに保存されたことが確認できます。次に、Upper Layerの保存場所も特定してみます。

  1. docker inspectコマンドでUpperDirのパスを取得します。

    docker inspect demo-container | grep -i "UpperDir"
    

    出力例:

    "UpperDir": "/var/lib/docker/overlay2/<layer-ID>/diff"
    
  2. 上記で取得したパスにアクセスし、samplefileがUpper Layer内に保存されていることを確認します。

    ls /var/lib/docker/overlay2/<layer-ID>/diff/tmp
    

    出力例:

    samplefile
    

3. Upper Layerの削除確認

コンテナを停止・削除すると、Upper Layerも削除され、UpperDir内のファイルはすべて消去されます。

docker stop demo-container
docker rm demo-container

この後、再度UpperDirのパスを確認すると、ファイルが削除されていることが確認できます。


5. Docker Build後のLower Layer内容確認

Dockerイメージを構築後、Lower Layerに保存される内容を確認する方法を紹介します。

  1. Dockerイメージの構築
    新しいイメージを作成し、不要なレイヤーがある場合はキャッシュを無効化します。

    docker build --no-cache -t layerfs-demo .
    
  2. Dockerイメージ内のLower Layer確認
    ビルド後にdocker historyコマンドを使用し、各レイヤーの状態を確認します。

    docker history --no-trunc layerfs-demo
    
  3. Lower Layer内のファイル詳細
    Lower Layerのファイル内容を確認するには、次のコマンドで詳細を確認します。

    docker inspect <layer-ID> | grep -i "LowerDir"
    

6. Dockerイメージ間のレイヤー共有確認

Dockerでは、同じベースイメージを使用するイメージ間でレイヤーを共有することで、ディスクスペースを効率的に使用しています。これを確認するための手順は以下の通りです。

  1. 同じベースイメージを使用した複数のDockerイメージの作成

    Dockerfile-ADockerfile-Bを使用して、それぞれ異なるイメージを作成しますが、共通のベースイメージとしてubuntu:latestを使います。

    Dockerfile-A
    FROM ubuntu:latest
    RUN apt-get update && apt-get install -y curl
    
    Dockerfile-B
    FROM ubuntu:latest
    RUN apt-get update && apt-get install -y git
    
  2. ベースレイヤー共有の確認
    同じベースイメージを共有しているかを確認するには、以下の手順で最下層レイヤーのIDを確認します。

    docker inspect --format='{{.RootFS.Layers}}' image-A
    docker inspect --format='{{.RootFS.Layers}}' image-B
    

    これにより、各イメージのレイヤーIDのリストが表示されます。最下層のレイヤーIDが一致していれば、同じベースイメージを共有していることが確認できます。

7. パフォーマンス向上のためのベストプラクティス

LayerFSのパフォーマンスを最大限に引き出すため、以下の具体例に基づいた方法があります。

  1. ページキャッシュの共有
    大きなファイルをLower Layerに含めると、複数のコンテナでページキャッシュが共有され、メモリ使用量が抑えられます。これにより、ファイルの読み込み速度も向上します。Dockerはキャッシュ共有を自動で行うため、特別な設定は不要です。

  2. 書き込みワークロードの最適化
    頻繁に書き込みが発生する場面では、ボリュームを活用してレイヤーのコピーアップを回避することで、効率的なデータ保存が可能です。

    • : ログファイルやデータベースキャッシュの書き込みにボリュームを使用することで、パフォーマンスが向上します。
  3. 高速なストレージの利用
    SSDなどの高速ストレージを使用すると、レイヤーのコピーアップが高速化され、特に初回の書き込み操作が多い場面で有効です。


8. レイヤーの削除とディスクスペースの管理

Dockerではキャッシュやレイヤーが蓄積し、ディスクスペースが増加します。不要なリソースは以下のコマンドで削除できます。

  1. 不要なイメージレイヤーの削除:

    docker image prune
    

    このコマンドにより、タグが付いていない「dangling」イメージが削除され、ディスクスペースが確保されます。

  2. すべての不要リソースの一括削除:

    docker system prune
    

    このコマンドにより、以下のリソースが削除されます。

    • 停止中のコンテナ
    • 未使用のネットワーク
    • ダングリングイメージ
    • ビルドキャッシュ

9. OverlayFSの互換性と制約

OverlayFSは便利なファイルシステムですが、いくつかの制約が存在します。

  1. サポートされるファイルシステムの種類: ext4xfsのみが対応しています。
  2. NFSとの互換性: NFS上ではOverlayFSは動作しません。
  3. レイヤー数の制限: 最大で128レイヤーまでしかサポートされていません。

これらの制約は、たとえばCI/CD環境で大量のレイヤーを持つビルドが発生する場合に問題になる可能性があります。LayerFSの構成が制限に達するとビルドが失敗する場合もあるため、ビルドパイプライン内で定期的にキャッシュをクリーンアップすることが推奨されます。


10. OverlayFSのデータ保存先の変更手順

DockerのLayerFSで使用されるoverlay2ディレクトリは、デフォルトで/var/lib/docker以下に保存されています。このデータ保存先を別の場所に変更する手順を以下に示します。

1. Dockerのサービスを停止する

まず、データの移行を行うためにDockerサービスを停止します。

sudo systemctl stop docker

2. 現在のデータのバックアップ

現在のDockerデータディレクトリ(通常は/var/lib/docker)を新しい保存先にコピーします。ここでは、追加購入したSSDを/mnt/docker-dataにマウントし、保存先として指定する例を示しています。

sudo rsync -aP /var/lib/docker /mnt/docker-data

3. Dockerの設定ファイルを更新する

Dockerがデータ保存先として新しいディレクトリを参照するように、設定ファイルを変更します。

  1. /etc/docker/daemon.jsonファイルを開きます(ファイルがない場合は新規作成します)。

    sudo vim /etc/docker/daemon.json
    
  2. 次の内容を追加して、データディレクトリを/mnt/docker-data/dockerに変更します。

    {
      "data-root": "/mnt/docker-data/docker"
    }
    

4. Dockerサービスの再起動

設定が完了したら、Dockerサービスを再起動します。

sudo systemctl start docker

11. LayerFSのデバッグ方法

LayerFSのデバッグには、docker diffdocker inspectといったコマンドが有効です。以下の手順で各レイヤーの状態を確認し、レイヤー内のファイル追加、変更、削除の詳細を把握することができます。

docker diff <コンテナ名またはID>
docker inspect <コンテナ名またはID> | grep -i "UpperDir\|LowerDir\|MergedDir"

docker diffコマンドの詳細な見方

docker diffコマンドは、指定したコンテナ内で行われたファイルやディレクトリの変更をリストします。出力には、変更内容を示すプレフィックス(A、C、D)が付加されており、それぞれ以下の意味を持ちます。

  • A (Added): コンテナ内に新しく追加されたファイルやディレクトリ。
  • C (Changed): コンテナ内で内容が変更されたファイルやディレクトリ。
  • D (Deleted): コンテナ内で削除されたファイルやディレクトリ。

これらのプレフィックスは、どのような種類の変更が発生したかを簡潔に示しており、デバッグの際に非常に役立ちます。

docker diff出力例

例えば、以下のような出力が得られるとします。

C /tmp
A /tmp/samplefile
D /var/log/example.log

この出力は以下の内容を表しています。

  • C /tmp: /tmpディレクトリに変更が加えられたことを示します。これは、/tmpディレクトリにファイルが追加されたり、既存のファイルが変更された場合に表示されます。
  • A /tmp/samplefile: /tmpディレクトリ内にsamplefileというファイルが新たに追加されたことを意味します。
  • D /var/log/example.log: /var/logディレクトリ内にあったexample.logというファイルが削除されたことを示しています。

docker diffとLayerFSの関係

docker diffで検出される変更内容は、LayerFSのUpperDirに記録されます。コンテナ内で行われたファイルの追加、変更、削除といった操作はすべてUpperDirに反映されるため、デバッグ時にはdocker diffの出力を参考にしつつ、docker inspectコマンドでUpperDirのパスを確認し、変更内容を追跡することが可能です。

LayerFSの構造確認手順

docker inspectコマンドでUpperDirLowerDirのパスを確認することで、コンテナのファイルシステム状態や、どのレイヤーで変更が行われたかをさらに詳しく調べられます。

docker inspect <コンテナ名またはID> | grep -i "UpperDir\|LowerDir\|MergedDir"

おわりに

DockerのLayerFSに関する構造やパフォーマンス向上の方法について、自分が調査した内容をまとめてみました。この結果が少しでも役に立っていれば幸いです。

参考

Discussion