reflink(3C) on ZFS

FUJIWARA Katsunori <foozy@lares.dti.ne.jp>

man cp(1)

man cp(1) での -z オプションに関する説明:

-z     Fast Copy. cp will reflink the source and
       destination files.  For more information,
       see the reflink(3C) man page.

reflink(3C) のユーザ空間実装

表記上 3C = C Library 分類扱いになっているが、 実際には引数を積んで reflinkat システムコールを呼び出しているだけ

以下の方法で確認済み

  • libc.so 中の両関数の逆アセンブル
  • truss でのシステムコール呼び出し確認

脱線

資料を書いていて、ふと思ったのですが……

Application Binary Interface

引数を「積む」って「スタック渡し」前提な表現ですよね

SPARC は勿論 x64 (AMD64) も一定数以下ならレジスタ渡し

そのうちに引数を「詰める」的な表現になるのかな? (笑)

ところで ARM の ABI ってどうでしたっけ?

富嶽とか次期 Apple 製デスクトップとか

富嶽と言えば

HPCシステムズ株式会社は、 富嶽と同じ ARM V8-A ベースの A64FX CPU (48 core) 搭載スパコン PRIMEHPC FX700 を税別 4,155,300円 の キャンペーン特別価格 で発売中

https://pc.watch.impress.co.jp/docs/news/yajiuma/1261462.html

富士通の PRIMEHPC FX700 (ノードあたり "約 4,000,000 円" ) の販売条件は 2ノードから なので、

https://it.impress.co.jp/articles/-/18836

1 ノード構成で売ってくれるのは安いのかも

いずれは自宅にも?

10 年経過すると SPARC Enterprise M4000 (約6,000,000円) もヤフオクで捨て値状態

あと 10 年待てば PRIMEHPC FX700 も投げ売りされているのかなぁ

そういえば京の一般モデル PRIMEHPC FX10/FX100 は中古の話を聞かないなぁ

と思ったら、そもそも FX10/FX100 は ラック単位の販売 でした……

閑話休題

それはさておき

reflink(3C) を使うための最小実装

int call_reflink(const char* src, const char* dst){
    int preserve = 0;
    if (reflink(src, dst, preserve)){
        return -1
    }
    return 0;
}

単独実行可能な最小実装の、コンパイル可能なコードは以下の通り。

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int
main(int argc,
     const char* argv[])
{
    int preserve = 0;
    if (reflink(argv[1], argv[2], preserve)){
        fprintf(stderr, "%s: %s: from %s to %s",
                argv[0], strerror(errno), argv[1], argv[2]);
        exit(1);
    }
    return 0;
}

reflink(3C) の実現方式

reflink(3C) による CoW 併用複製は ZFS の De-Duplication 機能で実現される

但し、当該 ZFS データセットにおける dedup 機能の有効/無効とは 無関係

reflink(3C) の速度

オンラインマニュアル曰く -zFast Copy のためのオプション

しかし、実際にはそれほど fast ではない

少なくとも 初回 適用時に関しては very slow

reflink(3C) の初回コスト

初回 reflink(3C) のカーネル内処理手順の推測

  1. 複製元ファイルの、最初の割り当てブロックの ハッシュ値を算出
  2. dedup 管理テーブル (DDT) 管理下の 同一内容ブロックの有無を確認
  3. あれば当該ブロックを共有 (⇛ refcnt += 1)
  4. なければ現ブロックを新規登録 (⇛ refcnt = 1)
  5. 複製先ファイルの、最初の割り当てブロックを DDT 経由で設定 (⇛ refcnt += 1)
  6. 複製元ファイルを構成する割り当てブロック 全て に対して上記を実行
  7. 遅いに決まっている!

reflink(3C) の実行速度デモ

reflink(3C)dd(1) での実行速度の比較デモ

参考までに、以前動作確認した際の出力を貼っておきます。

cp_reflink コマンドは、 "単独実行可能な最小実装のコンパイル可能なコード" から生成された実行バイナリです。

以降で例示する性能計測結果は、 いずれも VirtualBox ゲスト上における計測値なので、 傾向は見て取れるものの、 値そのものの厳密性には欠けている点にご注意ください。

$ time dd if=1M_file of=1M_dd bs=1k
1024+0 records in
1024+0 records out

real    0m0.042s
user    0m0.005s
sys     0m0.039s

$ time ./cp_reflink 1M_file 1M_reflink1
real    0m0.218s
user    0m0.001s
sys     0m0.008s

$

性能比較に dd(1) を使う理由

dd(1) は愚直に read/write を繰り返すが -z なしの cp(1) は:

  1. 読み出しモードで open(2) した複製元ファイル 全体mmap(2)
  2. マッピングのみなのでファイル内容に関する I/O なし
  3. 複製先ファイルを書き込みモードで open(2)
  4. 複製元の mmap(2) 領域 全体 を元データ指定して write(2) 実施
  5. 複製元全体を一括指定するので write(2) 発行は 一度のみ
  6. データ転送は全て カーネル内で閉じる
  7. 早いに決まっている!

参考までに、以前動作確認した際の出力を貼っておきます。

$ time dd if=1M_file of=1M_dd bs=1k
1024+0 records in
1024+0 records out

real    0m0.042s
user    0m0.005s
sys     0m0.039s

$ time cp 1M_file 1M_cp
real    0m0.015s
user    0m0.002s
sys     0m0.012s

$

なお対象ファイルサイズが 64MB の場合、 1K バッファでのループによる複製に対してなら、 初回の reflink(3C) 適用でも十分高速であることが確認できます。

$ time dd if=64M_file of=64M_dd bs=1k
65536+0 records in
65536+0 records out

real    0m1.664s
user    0m0.215s
sys     0m1.393s

$ time ./cp_reflink 64M_file 64M_reflink1
real    0m0.925s
user    0m0.001s
sys     0m0.025s

$ time ./cp_reflink 64M_file 64M_reflink2
real    0m0.045s
user    0m0.001s
sys     0m0.011s

$ time cp 64M_file 64M_cp
real    0m0.219s
user    0m0.001s
sys     0m0.216s

$

流石に初回 reflink(3C) こそ mmap(2) 併用で高速化している cp(1) には及びませんが、 2回目以降の reflink(3C)cp(1) よりも大幅に高速に複製できることが確認できます。

reflink(3C) の使いどころ (2)

Oracle Cluster File System (OCFS) の reflink 機能に関するブログエントリ (2009-05-04) 曰く:

The reason we implemented reflink is for Oracle VM

「大容量なベースファイルを元に、小規模改変された多数のファイルが存在」 するようなケースには reflink(3C) がハマりそう。

脱線

SEEK_HOLE について掘り下げてみる

SEEK_HOLE on Solaris

Jeff Bonwick の 2005 年のコラム 曰く:

As part of the ZFS project, we introduced two general extensions to lseek(2): SEEK_HOLE and SEEK_DATA.

(snip)

At this writing, SEEK_HOLE and SEEK_DATA are Solaris-specific. I encourage (implore? beg?) other operating systems to adopt these lseek(2) extensions verbatim (100% tax-free) so that sparse file navigation becomes a ubiquitous feature that every backup and archiving program can rely on. It's long overdue.

SEEK_HOLE on Linux

Linux (4.15.0/Ubuntu 18.04.4 LTS) の man lseek(2) 曰く:

Since version 3.1 (注釈: 2011-10-25 リリース), Linux supports the following additional values (注釈: SEEK_DATASEEK_HOLE のこと) for whence:

(snip)

SEEK_DATA and SEEK_HOLE are nonstandard extensions also present in Solaris, FreeBSD, and DragonFly BSD; they are proposed for inclusion in the next POSIX revision (Issue 8).

ls 本 を書いた 2013 年には Linux でも SEEK_DATASEEK_HOLE が利用可能になっていた筈なのに、 書籍では "Linux は ioctl(FS_IOC_FIEMAP) でブロック割り当て状況取得" と書いてしまってる…… orz

更に脱線

reflink 成立の歴史的経緯に関して掘り下げてみる

閑話休題

ほぼ終わりですが……

reflink(3C) on ZFS まとめ

以下のようなユースケースでは reflink(3C) の利用がハマる筈

reflink(3C) on ZFS まとめ

そんなあなたに reflink(3C) !!!

おわり

ご清聴ありがとうございました