はじめに
IoT エッジデバイスの長期運用では、eMMC への書き込み量は信頼性と寿命を左右する重要な要素です。
本記事では、アプリケーション側で過剰なfsync()を抑制した状態を前提として、
Linuxカーネルパラメータの調整により、eMMC書き込み量をどこまで削減できるのか
を実測で検証しました。
評価軸は以下の2つです。
| 評価ポイント | 評価軸 | 対象 |
|---|---|---|
| ⓵ | ページキャッシュ制御 |
vm.dirty_background_ratio / vm.dirty_ratio / vm.dirty_writeback_centisecs / vm.dirty_expire_centisecs
|
| ⓶ | ジャーナル制御 | ext4 マウントオプション commit
|
本検証の結果、
- ext4 commitを5秒 → 60秒に延長しても削減率は3.7%
- dirty_writeback最適化では約28%削減
という差を確認しました。
書き込みの流れとチューニングポイント
対策を正しく評価するために、アプリケーションの書き込みが物理ストレージに到達するまでの経路を整理します。ページキャッシュからeMMCへの書き込み経路は 非同期パス と 同期パス の2系統があります。
| パス | 経路 | vm.dirty_writeback の影響 | ext4 commit の影響 |
|---|---|---|---|
| 非同期 | fwrite → fflush → ページキャッシュ → writeback → journal → eMMC | あり | あり |
| 同期 | fsync → ページキャッシュ → 即時フラッシュ → eMMC | なし | なし |
fsync() はページキャッシュ上の変更を同期的に書き出して完了を待つため、writeback による集約(dirty_* のコアレッシング)が効きにくいと考えられます。また ext4 の commit= は「最大遅延」を決めるパラメータであり、fsync() はこのタイマを待たずに永続化を進めるため、commit変更の効果は出にくいです。
2つのチューニングポイントはいずれも非同期パスにのみ作用します。どちらが影響が大きいかを今回の実測で確認します。
用語解説
Dirty(ダーティページ)
ページキャッシュ上にあり、まだブロックデバイスへ書き出されていない変更済みのメモリページです。
アプリが write() を呼ぶと、データは即座に eMMC へは書かれず、まず DRAM 上のページキャッシュに保存されます。この「未書き出し状態」のページが Dirty ページです。
$ grep Dirty /proc/meminfo
Dirty: 2048 kB
Dirty 量が増えすぎると、カーネルは書き込みプロセスをスロットリング(リソースの流量を制限してシステムがパンクするのを防ぐ制御)して書き込みを抑制します。
Writeback(ライトバック)
Dirty ページをブロックデバイスへ書き出すカーネルの非同期処理です。kworker スレッドが担当します。
writeback は以下の条件でトリガーされます。
| トリガー | 条件 |
|---|---|
| 量超過 | Dirty 量が dirty_background_ratio を超えた |
| 定周期 |
dirty_writeback_centisecs タイマー発火 |
| 期限切れ |
dirty_expire_centisecs を超えた古い Dirty ページが存在 |
複数の write() が同じページに対して連続して発生した場合、writeback が発火するまでの間に後の書き込みが前の書き込みを上書きし、最終的に1回の物理 I/O にまとまります。これをコアレッシングといいます。これは例えると、小さな液滴や気泡が互いにくっついて、一つの大きな塊になる現象のようなイメージです。
fflush
C 標準ライブラリ(stdio)が保持するユーザー空間バッファをカーネルのページキャッシュへ転送します。ストレージへの永続化は保証しません。
fflush() : ユーザーバッファ → ページキャッシュ(DRAM 止まり)
fsync
対象ファイルのダーティページを即座にストレージへ書き出すよう強制します。writeback スケジューラを待たず同期的に実行され、完了後はストレージへの永続化が保証されます。
fsync() : ページキャッシュ → eMMC(同期確定)
fsync() を多用すると vm.dirty_* によるコアレッシング効果が失われます。本記事のワークロードでは、アプリケーション側でこの fsync() 呼び出し頻度をあらかじめ最適化済みの状態を前提としています。
vm.dirty_background_ratio
バックグラウンド writeback を開始する Dirty 量の閾値(システム RAM に対する割合)です。
例)RAM 1GB × dirty_background_ratio 10% = 約 100MB
この値を超えると、カーネルがバックグラウンドで非同期に(eMMCへ)書き出しを開始します。これによりアプリの書き込みはブロックされません。
vm.dirty_ratio
アプリの書き込みを同期的に抑制しスロットリングする Dirty 量の上限(RAM に対する割合)です。dirty_background_ratio より大きい値を設定します。
例)RAM 1GB × dirty_ratio 20% = 約 200MB
| パラメータ | 役割 | 超えたときの動作 |
|---|---|---|
| dirty_background_ratio | バックグラウンド writeback 開始ライン | kworker が書き出しを開始(アプリは継続) |
| dirty_ratio | アプリ書き込み抑制ライン | write() がスリープしてスロットリング発生。アプリが急に重くなる現象が発生。 |
vm.dirty_writeback_centisecs
writeback デーモンを起動する周期です。単位は centisecs(1/100 秒)です。
500 → 5秒周期(デフォルト)
3000 → 30秒周期
長くするほど Dirty ページが蓄積されてからまとめて書き出されるため、コアレッシング効果が増します。一方で、電源断時の未書き込みデータ滞留時間も増加します。
vm.dirty_expire_centisecs
Dirty ページを「古い」と見なし、次回 writeback 時に強制的に書き出す期限です。単位は centisecs です。
3000 → 30秒(デフォルト)
10000 → 100秒
この値を延ばすと、同一ページへの複数更新が1回の writeback にまとめられやすくなります。
ext4 commit
ext4 のジャーナル(jbd2)がコミットを行う最大遅延時間(秒)です。マウントオプションとして指定します。
commit=5 → 5秒ごとにジャーナルコミット(デフォルト)
commit=60 → 最大60秒遅延
commit周期を延ばすと、短時間に発生するメタデータ更新をまとめてジャーナルに書けるため、メタデータ I/O 回数を削減できる可能性があります。ただし、コミット前に電源断が発生するとジャーナル上のメタデータが失われるリスクがあります。
実験設計
実験は、ページキャッシュ制御やジャーナル制御によるeMMC書き込み負荷量を評価するため、実験環境の制御パラメータを変えた状態で、アプリケーション評価ツールによるワークロード中のセクタ書き込み量を評価しました。
実験環境
| 項目 | 値 |
|---|---|
| ボード | ARMボード(Armadillo IoT G3) |
| RAM | 1GB |
| eMMC | 4GB |
| ファイルシステム | ext4(data=ordered) |
| カーネル | 6.1系 |
評価ワークロード
今回の検証では、実運用に近い固定ワークロードを再現するために、以下仕様の自作の負荷生成ツールを使用しました。
- 5000個のデータファイル(64KB)を周期更新
- 各データファイルを100スレッドで並列更新
- 各データファイルを30秒周期で書き込みfflush()
- 管理情報ファイル(440KB)のみ30秒周期でfsync()
この構成は、IoTエッジ機器における
- センサデータファイルの周期更新
- 状態ファイルの上書き
- 管理情報の同期保存
を模擬したものです。
この目的は、ワークロードを固定した状態でカーネルパラメータのみを変更して因子を切り分けることです。
データファイルはページキャッシュ経由のため vm.dirty_* の影響を受けます。管理情報ファイル は fsync() のため vm.dirty_* の影響を受けません。
ワークロード生成ツールの詳細実装は以下を参照してください。
計測方法
書き込み量の指標として、/sys/block/mmcblk2/stat の sectors_written を使用しました。
評価ポイントの条件毎に、
- ワークロード実行中に
- 1分間の sectors_written の差分を取得 × 10回繰り返し平均を算出
としました。
1 sector = 512 byteとし、
- MiB/min = sectors / 2048
- GiB/day = MiB/min × 1440 / 1024
で換算しています。
ワークロードは固定として、カーネルパラメータのみを変更因子として評価しました。
実測結果
Default(ベースライン)
# カーネルデフォルト値
vm.dirty_background_ratio = 10
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500 # 5秒
vm.dirty_expire_centisecs = 3000 # 30秒
# ext4 commit=5(デフォルト)
| 指標 | 値 |
|---|---|
| 平均書き込み量 | 62.98 MiB/min |
| 推定 eMMC 書き込み量 | 88.56 GiB/day |
Pattern A — バランス型
writeback 周期と expire を2倍に緩め、dirty_ratio を少し拡大した「バランス型」です。
sysctl -w vm.dirty_background_ratio=10
sysctl -w vm.dirty_ratio=30
sysctl -w vm.dirty_writeback_centisecs=2000 # 20秒
sysctl -w vm.dirty_expire_centisecs=6000 # 60秒
| 指標 | 値 | vs Default |
|---|---|---|
| 平均書き込み量 | 52.22 MiB/min | −17% |
| 推定 eMMC 書き込み量 | 73.47 GiB/day |
writeback 周期を 5秒 → 20秒、expire を 30秒 → 60秒 に延ばすことで、データ ファイルへの複数回書き込みが1回の writeback にまとめられる頻度が増しました。
Pattern B — 集約型
さらにコアレッシングを積極化した「集約型」です。expire を 100秒に延ばし、複数周分の更新をまとめて書き出す設定です。
sysctl -w vm.dirty_background_ratio=15
sysctl -w vm.dirty_ratio=40
sysctl -w vm.dirty_writeback_centisecs=3000 # 30秒
sysctl -w vm.dirty_expire_centisecs=10000 # 100秒
| 指標 | 値 | vs Default |
|---|---|---|
| 平均書き込み量 | 45.19 MiB/min | −28% |
| 推定 eMMC 書き込み量 | 63.54 GiB/day |
dirty_expire_centisecs を長くすると「古いdirtyを強制的に書き出すまでの猶予」が伸びるため、同一ページへの上書きが発生しやすくなり、結果としてコアレッシングが効きやすくなりました。
commit=60(ジャーナル周期のみ変更)
vm.dirty_* はデフォルトのまま、ext4 の commit オプションのみを 5秒 → 60秒 に変更しました。
# 動的に再マウント
mount -o remount,commit=60 /
# /etc/fstab で永続化する場合
# /dev/mmcblkXpY / ext4 defaults,commit=60 0 1
| 指標 | 値 | vs Default |
|---|---|---|
| 平均書き込み量 | 60.63 MiB/min | −3.7% |
| 推定 eMMC 書き込み量 | 85.34 GiB/day |
削減率はわずか 3.7% に留まりました。commit の変更だけではほとんど効果がありません。
Pattern B + commit=60(組み合わせ)
Pattern B に commit=60 を加えた場合です。
| 指標 | 値 | vs Default |
|---|---|---|
| 平均書き込み量 | 45.14 MiB/min | −28% |
| 推定 eMMC 書き込み量 | 63.47 GiB/day |
Pattern B 単体(45.19 MiB/min)と実質同等でした。commit=60 の追加効果はほぼゼロです。
比較表
| パターン | 平均書き込み (MiB/min) | GiB/day | Default比 |
|---|---|---|---|
| Default | 62.98 | 88.56 | — |
| Pattern A(バランス型) | 52.22 | 73.47 | −17% |
| Pattern B(集約型) | 45.19 | 63.54 | −28% |
| commit=60 のみ | 60.63 | 85.34 | −3.7% |
| Pattern B + commit=60 | 45.14 | 63.47 | −28% |
考察
なぜ vm.dirty_* の影響が大きいのか
本ワークロードの書き込みを経路別に整理します。
| ファイル | 書き込み方法 | ページキャッシュ経由 | dirty_* の影響 |
|---|---|---|---|
| データ ファイル(5,000個) |
fflush,fclose() のみ |
○ あり | 大 |
| 管理情報(1個、440KB) |
fsync() あり |
× なし(即時物理 I/O) | なし |
書き込み量の大半を占める データ ファイルはページキャッシュを経由するため、writeback タイミングを制御することでコアレッシング効果が発揮されます。
dirty_expire_centisecs = 100秒(Pattern B)の場合:
データ ファイル更新 1周目(0〜30秒) → 各ページがダーティ化
データ ファイル更新 2周目(30〜60秒) → 同ページを上書き(まだダーティ状態)
データ ファイル更新 3周目(60〜90秒) → 同ページを上書き(まだダーティ状態)
expire 到達(〜100秒後) → 最新データのみ eMMC へ書き出し
↑ 3周分の write が 1回の物理 I/O に集約
dirty_expire_centisecs=10000(100秒)により、データファイルの30秒周期の更新が最大3周分まとめて書き出されるようになりました。
なぜ ext4 commit の影響が小さいのか
ext4 のジャーナル(jbd2)は主にメタデータ(inode、ディレクトリエントリ等)を管理します。data=ordered モード(ext4 デフォルト)では、データブロック自体はジャーナルに書かれず直接 eMMC へ書き出されます。
data=ordered モードでのファイル更新フロー:
1. データブロックを writeback(ページキャッシュ → eMMC)← 大半の I/O
2. ジャーナルにメタデータを記録 ← ごく少量
3. journal commit ← commit 周期が作用するのはここ
本ワークロードはファイルの新規作成・削除ではなく既存ファイルのデータブロック上書きが主体であるため、ジャーナルに記録されるメタデータは少量です。commit 周期を延ばしてもデータブロックの書き込み量は変わりません。削減できるのはメタデータ分のジャーナル書き込みのみであり、全体に対して微小なため約 3.7% の削減に留まりました。
コアレッシング効果の限界
管理情報ファイルへの fsync() は dirty_writeback による制御の外にあります。30秒周期の fsync() は 1分間の計測窓内で2回発生し、1回 440KB を即時書き出します。
管理情報 の書き込み: 440KB × 2回/min = 0.86 MiB/min
データ ファイルの書き込み: 62.98 − 0.86 ≈ 62.12 MiB/min(Default 時)
データファイルが全体の約 98.6% を占めるため、管理情報分はほぼ無視できる規模です。逆に言えば、今回の削減効果(Pattern B で −28%)はほぼすべて データファイルへのコアレッシング改善によるものです。
トレードオフ
dirty_* を緩めることには明確なリスクがあります。それは電源断時に RAM 上の Dirty ページは失われるため、dirty_expire_centisecs が長いほど失われうるデータ量が増加することです。
| パターン | 削減効果 | 電源断時の最大データ損失期間 |
|---|---|---|
| Default | — | 最大 30秒 |
| Pattern A | −17% | 最大 60秒 |
| Pattern B | −28% | 最大 100秒 |
| commit=60 のみ | −3.7% | 最大 60秒(メタデータのみ) |
| Pattern B + commit=60 | −28% | 最大 100秒 |
Pattern B を採用する場合、データ消失リスクを最小化するために以下の電源断・瞬停対策が望ましい。
| 対策 | 内容 |
|---|---|
| UPS(無停電電源装置) | 停電時に電源を維持し、正常シャットダウンで sync を完了させる |
| 瞬停対策(コンデンサ・バッテリー) | 数秒〜数十秒の瞬時電圧低下に対してダーティページのフラッシュ完了まで電源を保持 |
| シャットダウンスクリプト | 電源断検知時に sync コマンドを実行してダーティページを強制フラッシュ |
今回の結論
今回のワークロードでは、eMMC書き込み量に最も影響していたのはext4 commitではなく、dirty_writeback制御でした。
| 観点 | 結論 |
|---|---|
| 影響因子 | vm.dirty_* がジャーナル(ext4 commit)より影響が大きい |
| commit=60 の効果 | 約 3.7%(誤差レベル) |
| 最大削減率 | Pattern B で約 28%削減 |
| 追加組み合わせ効果 | Pattern B + commit=60 はほぼ Pattern B 単体と同等 |
削減効果(Default比)は以下の通りです。
| 優先度 | 施策 | 削減率 |
|---|---|---|
| 1 | vm.dirty_* チューニング | 最大 28% |
| 2 | ext4 commit 延長 | 約 3.7% |
eMMC 寿命試算(本記事のワークロード)
ここでは、eMMC の理論総書き込み許容量(TBW)を 120TB と仮定し、
日次書き込み量(GiB/day)から単純計算で推定寿命を算出しました。
計算式
推定寿命(年) ≒ (122,880 ÷ GiB/day) ÷ 365
※ 計測値はバイナリ単位(MiB/min → GiB/day)のため、1TiB = 1,024GiB として換算
120TiB の eMMC 理論寿命に対して:
| パターン | GiB/day | 推定寿命(日) | 推定寿命(年) |
|---|---|---|---|
| Default | 88.56 | 1,387 | 約 3.8 年 |
| Pattern A | 73.47 | 1,673 | 約 4.6 年 |
| Pattern B | 63.54 | 1,934 | 約 5.3 年 |
アプリケーション最適化済みのワークロードに Pattern B を追加適用することで、推定寿命がさらに約 1.5 年延びる計算になります。
今回わかったこと
- 書き込み削減の主因は「ジャーナル」ではありませんでした。
- データ主体のワークロードでは writeback 制御の方が影響が大きいことが分かりました。
- commit延長は、メタデータ主体のワークロードでない限り効果は限定的と考えられます。
次回確認すること
次回は、今回の結論をさらに深めるために以下を確認したいと思います。
-
fsync()の周期を変えた場合(毎回 / 30秒 / writeback周期と同期)の相互作用 - 書き込みサイズや更新局所性を変えた場合(小サイズ高頻度 / 大サイズ低頻度)
-
nr_dirty/nr_writeback/sectors writtenの時系列可視化による因果の補強
注意事項
- 本記事の設定値・計測値は特定の実験環境での結果であり、環境によって異なります。
- dirty_* パラメータを緩めると電源断耐性が低下するため、UPS・瞬停対策が整った環境での適用を推奨します。
- ext4 commit 延長も同様に、コミット前の電源断でジャーナル上のメタデータが失われるリスクがあります。
