1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WSL2 + ext4 環境で fsync のコストを徹底測定してみた

1
Posted at

毎回 fsync するとどのくらい遅いのか?実測してみた

「毎回 fsync すると遅い」とよく言われます。
しかし、実際にはどの程度遅いのか、具体的な数字で語られることはあまり多くありません。

また、

  • バッチサイズを増やすとどこまで改善するのか
  • O_DIRECT は本当に遅いのか
  • fsyncfdatasync の違いはどれほど影響するのか

これらを、自分の環境で実測して確認してみました。


実験環境

  • OS: WSL2 (Windows 11)
  • Kernel: Linux 6.6
  • Filesystem: ext4 (data=ordered)
  • Disk: Windows 側 NVMe(WSL2 の仮想化レイヤ経由)
  • 言語: Rust(libc を直接呼び出し)

WSL2 はベアメタル環境ではありませんが、相対的な性能比較には十分利用できます。


実験1: 毎回 fdatasync する場合

4KB のデータを書き込み、その都度 fdatasync を呼び出します。

use std::fs::{File, OpenOptions};
use std::io::Write;
use std::os::fd::AsRawFd;
use std::time::Instant;

const FILE_PATH: &str = "/home/user/test.db";
const WRITE_SIZE: usize = 4096;
const TOTAL_WRITES: usize = 100_000;

fn main() -> std::io::Result<()> {
    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(FILE_PATH)?;

    let data = vec![b'a'; WRITE_SIZE];
    let start = Instant::now();

    for _ in 0..TOTAL_WRITES {
        file.write_all(&data)?;
        unsafe { libc::fdatasync(file.as_raw_fd()) };
    }

    let duration = start.elapsed();
    println!("Ops/sec: {}", TOTAL_WRITES as f64 / duration.as_secs_f64());
    Ok(())
}

結果

項目
Total time 134.8 秒
Ops/sec 742
Avg latency 1.35 ms

1 回の write + fdatasync に約 1.35ms かかる計算になります。
スループットは 742 ops/sec です。

この数値が「毎回同期する」場合の基準になります。


実験2: バッチ処理の効果

一定回数ごとに fdatasync を呼び出すように変更します。

const BATCH_SIZE: usize = 10; // 10, 100, 1000 と変更

for i in 0..TOTAL_WRITES {
    file.write_all(&data)?;
    if (i + 1) % BATCH_SIZE == 0 {
        unsafe { libc::fdatasync(file.as_raw_fd()) };
    }
}

結果

Batch Size Ops/sec 加速倍率
1 742 1.0x
10 7,011 9.4x
100 43,541 58.7x
1000 135,901 183x

バッチサイズ 1000 では 180 倍以上 のスループット向上が確認できました。

ただしその代わりに、クラッシュ時には最大 1000 件のデータを失う可能性があります。
スループットと耐障害性の明確なトレードオフが可視化されました。


実験3: O_DIRECT の影響

O_DIRECT を指定し、ページキャッシュをバイパスする構成に変更します。

OpenOptions::new()
    .custom_flags(libc::O_DIRECT)
    .open(FILE_PATH)?

結果

モード Ops/sec
通常(キャッシュあり) 742
O_DIRECT 791

予想に反して、O_DIRECT の方がわずかに高速でした。

通常、O_DIRECT はページキャッシュを回避するためオーバーヘッドが増えることが多いですが、今回の環境では逆の結果となりました。

これは WSL2 の仮想化レイヤに起因する可能性があります。
内部的な I/O パスやキャッシュ構造が、ベアメタルとは異なる挙動を示していると考えられます。

環境依存性の重要さを改めて認識しました。


実験4: fsyncfdatasync の比較

fsync はデータとメタデータの両方をフラッシュし、
fdatasync はデータ部分のみを対象とします。

結果

モード Ops/sec
fdatasync 742
fsync 734

差は約 1% で、実質的に同等でした。

今回のワークロードは単純な追記であり、メタデータ更新がほとんど発生しないため、差が小さくなったと考えられます。


まとめ

実験 結果
毎回同期 約 1.35ms / 742 ops/sec
バッチ処理 最大 183 倍の向上
O_DIRECT WSL2 環境ではわずかに高速
fsync vs fdatasync 追記ワークロードでは差は小さい

得られた示唆

  1. 「遅い」という表現は曖昧である
    実際には 1.35ms、742 ops/sec という具体的なコストが存在する。

  2. バッチ処理は極めて強力
    同期回数を減らすだけで桁違いの改善が得られる。

  3. ストレージ挙動は環境依存
    一般論がそのまま当てはまるとは限らない。


ソースコード

GitHub: https://github.com/otmojo/fsync-cost

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?