cpコマンドとreflink - ディスクにコピーしないcpの話
- GNU coreutils v9.0以降の
cpは、デフォルトでreflinkを使う(使えるなら) - reflinkとはデータブロックをファイル間で共有する仕組み
- 書き込み時はCoW(Copy-on-Write)でブロック単位に差分だけコピーされる
- Dockerのbtrfsドライバーも同じ仕組みを使っている
ファイルシステムの構造(前提知識)
Section titled “ファイルシステムの構造(前提知識)”Linuxのファイルシステムでは「ファイル」は3層に分かれている。
ファイル名(ディレクトリエントリ) ↓ 指すinode(メタデータ)← パーミッション、タイムスタンプ、サイズなど ↓ 指すデータブロック(実際の中身)← ディスク上のデータファイル名はinodeへの「ポインタ」に過ぎない。
reflinkとは
Section titled “reflinkとは”別々のファイルのinodeが、同じデータブロックを参照できる仕組み。
foo → inode #100 ─┐ ├→ データブロックAbar → inode #200 ─┘- ディスク使用量は1ファイル分で済む
- inodeは別なのでメタデータは独立
サポートしているファイルシステム
Section titled “サポートしているファイルシステム”| FS | reflink |
|---|---|
| ext4 | ❌ |
| XFS(kernel v4.9以降) | ✅(mkfs時に有効化) |
| Btrfs | ✅ |
Ubuntuなど多くのディストリビューションではXFSのreflinkがデフォルト有効。
ハードリンクとの違い
Section titled “ハードリンクとの違い”同じく「データを共有する」仕組みに見えるが根本的に異なる。
| ハードリンク | reflink | |
|---|---|---|
| inodeの共有 | ✅ 同じinodeを指す | ❌ inodeは別々 |
| メタデータの共有 | される(同じinodeなので) | されない |
| 削除時の挙動 | inodeの参照カウントが減るだけ | inodeは消えるがデータブロックは残る |
| 参照カウント管理 | inodeレベル(リンクカウント) | データブロックレベル(refcount btree) |
# ハードリンク:foo のパーミッションを変えると bar も変わるln foo barchmod 755 foo # → bar も 755 になる
# reflink:inodeが別なので独立cp --reflink=always foo barchmod 755 foo # → bar は変わらないcpとreflinkの関係
Section titled “cpとreflinkの関係”GNU coreutils v9.0以降、cp はデフォルトで --reflink=auto モード。
# XFS上でのcp:一瞬で終わるtime sh -c 'cp foo bar && xfs_io -c fsync bar'# real 0m0.016s ← データコピーが発生していない証拠
# fiemapで確認するとfooとbarが同じディスク領域を指しているxfs_io -c fiemap foo # → 0: [0..2097151]: 192..2097343xfs_io -c fiemap bar # → 0: [0..2097151]: 192..2097343(同じ!)–reflinkオプション
Section titled “–reflinkオプション”| 値 | 挙動 |
|---|---|
auto(デフォルト) | 使えるなら使う |
always | 必ず使う(使えなければ失敗) |
never | 必ずデータコピーする |
--reflink=never が必要なケース:
cpでバックアップを実現している場合(ディスクビット化けが両ファイルに影響するため)- データコピーの性能を測定したい場合
CoW(Copy-on-Write)
Section titled “CoW(Copy-on-Write)”reflinkで共有されているデータに書き込んだとき、もう片方に影響しないようにする仕組み。
bar の一部に書き込もうとする→ 「このブロックは共有されている」と検知→ 書き込む部分だけ新しいブロックにコピーしてから書く→ bar の参照先だけ新しいブロックに切り替えるブロック単位で差分管理される
Section titled “ブロック単位で差分管理される”ファイルシステムは4KB単位(XFSのデフォルト)でデータを管理しているため、1バイト書き換えでも「丸ごとコピー」にはならない。
1GBファイルの先頭1バイトだけ書き換えた場合:- コピーされるのは先頭4KBだけ- 残りの約1GBはまだ共有されたままディスク使用量の変化:
| 状態 | 使用量 |
|---|---|
| cp直後 | 1GB(全共有) |
| 先頭1バイト書き換え後 | 1GB + 4KB |
| 全部書き換え後 | 2GB(全部別ブロック) |
ブロックサイズの設定
Section titled “ブロックサイズの設定”mkfs時に変更可能(デフォルト4KB)。マウント後の変更は不可。
mkfs.xfs -b size=4096 /dev/sdb # 512〜65536の範囲で指定可能| ブロックサイズ | 特性 |
|---|---|
| 小さい | CoWの粒度が細かい・小ファイルに有利・管理コスト増 |
| 大きい | シーケンシャルアクセスが速い・CoWのコストが荒い・管理コスト減 |
DockerのレイヤーとCoWの関係
Section titled “DockerのレイヤーとCoWの関係”DockerイメージもCoWと同じ「差分だけ持つ」発想で構成されている。
Layer 4: アプリのコード追加 ← 差分Layer 3: pip install した結果 ← 差分Layer 2: python インストール ← 差分Layer 1: Ubuntu ベース ← ベースストレージドライバーの違い
Section titled “ストレージドライバーの違い”| ドライバー | 差分の粒度 | 仕組み |
|---|---|---|
| overlayfs(現デフォルト) | ファイル単位 | ファイルシステムを重ねる |
| btrfs | ブロック単位 | reflinkのCoWをそのまま使う |
| devicemapper | ブロック単位 | LVMシン・プロビジョニング |
btrfsドライバーがreflinkと直結しているため、Dockerのレイヤーを理解するとreflinkも自然に理解できる(逆も然り)。
まとめ:繋がりの全体像
Section titled “まとめ:繋がりの全体像”cp の挙動└→ reflink(ブロック共有) ├→ ハードリンクとの違い(inodeが別か同じか) └→ CoW(書き込み時だけコピー) ├→ ブロック単位で差分管理 └→ Dockerのbtrfsドライバーも同じ仕組み学習の軌跡:気になったこと・深掘りの流れ
Section titled “学習の軌跡:気になったこと・深掘りの流れ”記事を読んで疑問に思ったことと、それを掘り下げた流れのメモ。
Q1. reflinkってハードリンクと同じじゃないの?
Section titled “Q1. reflinkってハードリンクと同じじゃないの?”「データを共有する」という点で同じに見えた。
→ 違いはinodeを共有するかどうか。ハードリンクはinodeごと共有するのでメタデータも同一になるが、reflinkはinodeが別なので独立している。
Q2. ファイルを削除したときreflinkはどうなるの?
Section titled “Q2. ファイルを削除したときreflinkはどうなるの?”ハードリンクは「参照カウントが0になったらinodeが消える」のはわかっていたが、reflinkはinodeが消えてもデータブロックはどうなるのか疑問だった。
→ データブロックレベルでも参照カウント(XFSではrefcount btree)が管理されていて、全inodeから参照されなくなって初めてデータブロックが消える。ハードリンクと同じ発想がブロックレベルで起きている。
Q3. 書き込んだら両方のファイルが変わってしまうのでは?
Section titled “Q3. 書き込んだら両方のファイルが変わってしまうのでは?”共有しているデータブロックに書き込んだら、もう一方のファイルにも影響するのではと思った。
→ CoW(Copy-on-Write)という仕組みで解決している。書き込みが発生したブロックだけ新しい領域にコピーしてから書くため、もう一方には影響しない。
Q4. CoWは「丸ごとコピー」が発生するの?
Section titled “Q4. CoWは「丸ごとコピー」が発生するの?”先頭1バイトだけ書き換えた場合、1GBのファイルが丸ごとコピーされてディスク使用量が2倍になるのかと思った。
→ ブロック単位(XFSのデフォルト4KB)で管理されているため、書き換えたブロックだけがコピーされる。1バイトの書き換えでも増加するのは4KBだけ。
Q5. ブロックサイズは変えられるの?
Section titled “Q5. ブロックサイズは変えられるの?”CoWの粒度がブロックサイズに依存するなら、設定できるのか気になった。
→ mkfs時に指定可能(512〜65536)。ただしマウント後の変更は不可。小さいほど粒度が細かくなるが管理コストが増える。
Q6. Dockerのbtrfsドライバーがなぜreflinkとつながるの?
Section titled “Q6. Dockerのbtrfsドライバーがなぜreflinkとつながるの?”Dockerのレイヤーが「差分だけ持つ」のは知っていたが、ストレージドライバーにbtrfsが出てくる理由がピンときていなかった。
→ btrfsドライバーはreflinkのCoWをそのまま使ってレイヤーの差分管理を実現しているため。overlayfsはファイル単位の差分、btrfsはブロック単位の差分という違いがあり、btrfsの仕組みはまさに今回学んだreflinkそのもの。