毎回 fsync するとどのくらい遅いのか?実測してみた
「毎回 fsync すると遅い」とよく言われます。
しかし、実際にはどの程度遅いのか、具体的な数字で語られることはあまり多くありません。
また、
- バッチサイズを増やすとどこまで改善するのか
-
O_DIRECTは本当に遅いのか -
fsyncとfdatasyncの違いはどれほど影響するのか
これらを、自分の環境で実測して確認してみました。
実験環境
- 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: fsync と fdatasync の比較
fsync はデータとメタデータの両方をフラッシュし、
fdatasync はデータ部分のみを対象とします。
結果
| モード | Ops/sec |
|---|---|
| fdatasync | 742 |
| fsync | 734 |
差は約 1% で、実質的に同等でした。
今回のワークロードは単純な追記であり、メタデータ更新がほとんど発生しないため、差が小さくなったと考えられます。
まとめ
| 実験 | 結果 |
|---|---|
| 毎回同期 | 約 1.35ms / 742 ops/sec |
| バッチ処理 | 最大 183 倍の向上 |
| O_DIRECT | WSL2 環境ではわずかに高速 |
| fsync vs fdatasync | 追記ワークロードでは差は小さい |
得られた示唆
-
「遅い」という表現は曖昧である
実際には 1.35ms、742 ops/sec という具体的なコストが存在する。 -
バッチ処理は極めて強力
同期回数を減らすだけで桁違いの改善が得られる。 -
ストレージ挙動は環境依存
一般論がそのまま当てはまるとは限らない。