Skip to content

cpコマンドとreflink - ディスクにコピーしないcpの話

  • GNU coreutils v9.0以降の cp は、デフォルトでreflinkを使う(使えるなら)
  • reflinkとはデータブロックをファイル間で共有する仕組み
  • 書き込み時はCoW(Copy-on-Write)でブロック単位に差分だけコピーされる
  • Dockerのbtrfsドライバーも同じ仕組みを使っている

ファイルシステムの構造(前提知識)

Section titled “ファイルシステムの構造(前提知識)”

Linuxのファイルシステムでは「ファイル」は3層に分かれている。

ファイル名(ディレクトリエントリ)
↓ 指す
inode(メタデータ)← パーミッション、タイムスタンプ、サイズなど
↓ 指す
データブロック(実際の中身)← ディスク上のデータ

ファイル名はinodeへの「ポインタ」に過ぎない。


別々のファイルのinodeが、同じデータブロックを参照できる仕組み。

foo → inode #100 ─┐
├→ データブロックA
bar → inode #200 ─┘
  • ディスク使用量は1ファイル分で済む
  • inodeは別なのでメタデータは独立

サポートしているファイルシステム

Section titled “サポートしているファイルシステム”
FSreflink
ext4
XFS(kernel v4.9以降)✅(mkfs時に有効化)
Btrfs

Ubuntuなど多くのディストリビューションではXFSのreflinkがデフォルト有効。


同じく「データを共有する」仕組みに見えるが根本的に異なる。

ハードリンクreflink
inodeの共有✅ 同じinodeを指す❌ inodeは別々
メタデータの共有される(同じinodeなので)されない
削除時の挙動inodeの参照カウントが減るだけinodeは消えるがデータブロックは残る
参照カウント管理inodeレベル(リンクカウント)データブロックレベル(refcount btree)
Terminal window
# ハードリンク:foo のパーミッションを変えると bar も変わる
ln foo bar
chmod 755 foo # → bar も 755 になる
# reflink:inodeが別なので独立
cp --reflink=always foo bar
chmod 755 foo # → bar は変わらない

GNU coreutils v9.0以降、cp はデフォルトで --reflink=auto モード。

Terminal window
# 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..2097343
xfs_io -c fiemap bar # → 0: [0..2097151]: 192..2097343(同じ!)
挙動
auto(デフォルト)使えるなら使う
always必ず使う(使えなければ失敗)
never必ずデータコピーする

--reflink=never が必要なケース:

  • cp でバックアップを実現している場合(ディスクビット化けが両ファイルに影響するため)
  • データコピーの性能を測定したい場合

reflinkで共有されているデータに書き込んだとき、もう片方に影響しないようにする仕組み。

bar の一部に書き込もうとする
→ 「このブロックは共有されている」と検知
→ 書き込む部分だけ新しいブロックにコピーしてから書く
→ bar の参照先だけ新しいブロックに切り替える

ブロック単位で差分管理される

Section titled “ブロック単位で差分管理される”

ファイルシステムは4KB単位(XFSのデフォルト)でデータを管理しているため、1バイト書き換えでも「丸ごとコピー」にはならない。

1GBファイルの先頭1バイトだけ書き換えた場合:
- コピーされるのは先頭4KBだけ
- 残りの約1GBはまだ共有されたまま

ディスク使用量の変化:

状態使用量
cp直後1GB(全共有)
先頭1バイト書き換え後1GB + 4KB
全部書き換え後2GB(全部別ブロック)

mkfs時に変更可能(デフォルト4KB)。マウント後の変更は不可。

Terminal window
mkfs.xfs -b size=4096 /dev/sdb # 512〜65536の範囲で指定可能
ブロックサイズ特性
小さいCoWの粒度が細かい・小ファイルに有利・管理コスト増
大きいシーケンシャルアクセスが速い・CoWのコストが荒い・管理コスト減

DockerイメージもCoWと同じ「差分だけ持つ」発想で構成されている。

Layer 4: アプリのコード追加 ← 差分
Layer 3: pip install した結果 ← 差分
Layer 2: python インストール ← 差分
Layer 1: Ubuntu ベース ← ベース
ドライバー差分の粒度仕組み
overlayfs(現デフォルト)ファイル単位ファイルシステムを重ねる
btrfsブロック単位reflinkのCoWをそのまま使う
devicemapperブロック単位LVMシン・プロビジョニング

btrfsドライバーがreflinkと直結しているため、Dockerのレイヤーを理解するとreflinkも自然に理解できる(逆も然り)。


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そのもの。