サーバーが遅い理由 #3 SSD なのに遅いとき — 書き込み増幅・fsync・キュー深度

読了 7分

HDD を SSD に替えればすべて解決する、という感覚が広まった時代がありました。ところが NVMe SSD を使うサーバーで 1 編 の診断をたどっていくと、iostatawait が数十ミリ秒という数字を目にすることがあります。スペックシートには数十万 IOPS と書いてあるのに、なぜこの SSD はスペックどおりに動かないのでしょうか?

答えの骨格は一つです。スペックシートの数字は特定の条件でだけ出る最大値であり、実際のワークロードはその条件をなかなか満たしません。条件を一つずつ崩す要因が、この記事の主題である fsync、書き込み増幅、キュー深度です。デバイスのスペックの読み方は ハードウェア基礎 #4、実測の方法論は ハードウェア中級 #5 で扱ったので、この記事は「スペックと実測の差はどこから来るのか」に集中します。

fsync — 耐久性を求めた瞬間、まったく別の話になります #

通常の書き込みはページキャッシュに記録すれば終わりなので、メモリ速度で返ってきます。ところがデータベースのコミットやメッセージキューのジャーナルのように「電源が落ちても残らなければならない」書き込みは、fsync でデバイスまで書き込み、完了の確認を待ちます。この経路は SSD 内部のキャッシュも迂回またはフラッシュする必要があるため、同じデバイスで桁の違う遅延が出ます。

特にコンシューマー向け SSD は fsync 経路が極端に遅い製品が多くあります。電源損失保護コンデンサ(PLP)を持つデータセンター向け SSD は、内部キャッシュに記録した瞬間に完了を報告しても安全なので fsync が数十マイクロ秒で終わりますが、PLP のないコンシューマー向けはフラッシュまで実際に記録する必要があり、fsync 一回にミリ秒単位かかることもあります。シーケンシャルリードのベンチマークでは最上位のコンシューマー向け SSD が、データベースを載せた瞬間に期待外れになる理由の大半はここにあります。毎秒数千コミットが必要なワークロードなら、fsync の遅延がそのままコミットの遅延です。

fsync 性能は fio で直接測れます。

fio — fsync 遅延の測定
$ fio --name=fsynctest --rw=write --bs=4k --size=1g --fdatasync=1
  ...
  fsync/fdatasync/sync_file_range:
    sync (usec): min=280, max=18400, avg=1240.52

平均 1.2ms なら、このデバイスの同期コミットの上限は毎秒 800 回前後です。スペックシートの「書き込み 500K IOPS」とは何の関係もない数字です。

書き込み増幅 — 長く使うと遅くなる構造 #

SSD のフラッシュは上書きができません。消去はブロック単位でしかできず、書き込みはそれより小さいページ単位で行われます。そこでコントローラーは空きページに新しく書き、古いページを無効としてマークしておき、あとで有効なページだけを別のブロックへ移してからブロックを消すガベージコレクションを回します。ユーザーが 1 の量を書くとき、内部では引っ越しコストを含めて何倍も書くことになります。これが書き込み増幅(write amplification)です。

増幅が大きくなる条件は空きブロックの不足です。ディスクを満杯近くまで使ったり、TRIM が伝わらずファイルシステムが消した領域を SSD がまだ有効データだと思っていたりすると、GC が移すべき有効ページが増え、書き込みのたびに内部の引っ越しがついて回ります。もう一つ、多くの TLC/QLC SSD は書き込みをまず高速な SLC モード領域で受け取りますが、持続的な書き込みでこのキャッシュが尽きると速度が階段状に落ちます。「コピーの開始直後は速いのに数十秒すると急に落ちる」という症状が典型です。

運用側の対処は三つです。使用量を 80〜90% 以下に保ってコントローラーに余裕ブロックを残すこと、fstrim.timer のような周期的な TRIM が実際に動いているかを確認すること、そして持続書き込み性能が重要なワークロードならスペックシートの瞬間最大値ではなく持続(sustained)書き込みの数値と PLP の有無でデバイスを選ぶことです。

キュー深度 — スペックの IOPS は行列を作ってこそ出ます #

スペックシートの「ランダムリード 1M IOPS」には小さな文字が付いています。QD32、つまりキュー深度 32 で複数のワーカーを立てた条件です。SSD は内部に数十のチャネルを持つ並列デバイスなので、リクエストをたっぷり並ばせてこそ全チャネルが働きます。

ところが実際のアプリケーションのアクセスパターンの多くは QD1 に近いのです。B-tree をたどるインデックス探索、ポインタをたどる参照のように、前の結果を見なければ次のリクエストを出せない依存的なアクセスは行列を作れません。QD1 で性能を決めるのは並列性ではなくリクエスト一つの往復遅延で、この値は最上位の NVMe でも数十マイクロ秒を大きく下回りません。QD1 の 4K ランダムリードに換算すれば、どの SSD も数万 IOPS 程度に収束するという意味です。

だから「スペック 1M IOPS なのに実測 20K」は故障ではなく、ワークロードのキューが浅いだけかもしれません。iostat -xaqu-sz(平均キュー長)が 1 前後なら、デバイスは能力を持て余している状態です。この場合はデバイスをより速いものに替えても利得が小さく、対処はアプリケーション側の並列化やアクセスパターンの改善、あるいはそもそもその参照をキャッシュで吸収する方向です。キュー深度別の実測方法は ハードウェア中級 #5 で扱いました。

クラウドボリューム — 上限はデバイスではなく契約です #

クラウドでは要因がもう一つ加わります。EBS のようなネットワークブロックストレージは、ボリュームとインスタンスのそれぞれに IOPS・スループットの上限が契約として決まっています。gp3 のデフォルトは 3,000 IOPS なので、物理 NVMe の感覚で使うとはるかに手前で await が跳ね上がります。バーストクレジットのあるボリュームタイプ(gp2 など)はクレジットが尽きた瞬間に性能が階段状に落ち、「数日は問題なかったのに突然遅くなった」という症状を作ります。モニタリングでボリュームの IOPS 消費が上限に張り付いていないかをまず確認し、上限ならデバイスのチューニングではなくボリュームスペックの変更が対処です。

診断手順のまとめ #

症状が「SSD なのにディスクがボトルネック」のとき、手順は次のとおりです。

  1. iostat -xawaitaqu-sz、読み書きの比率で症状の形をつかみます。
  2. クラウドならまずボリューム上限 — IOPS・スループットが契約上限やバーストクレジットに引っかかっていないかを確認します。最も多く、最も早く確認できます。
  3. fsync ワークロードか — データベースやジャーナルの書き込みが中心なら fio --fdatasync=1 で同期書き込みの遅延を実測します。コンシューマー向けデバイスなら、PLP のあるデバイスへの交換が対処です。
  4. 持続書き込みで遅くなるか — 序盤だけ速いなら SLC キャッシュの枯渇と書き込み増幅です。容量の余裕と TRIM を点検します。
  5. aqu-sz が 1 前後か — 浅いキューならデバイス交換よりアクセスパターンとキャッシングが対処です。

まとめ #

  • スペックシートの IOPS は深いキューと並列ワーカーという条件での最大値です。実測と違うのは故障ではなく条件の差です。
  • fsync 書き込みはまったく別の話です。PLP のないコンシューマー向け SSD はコミット中心のワークロードで桁違いに遅くなります。
  • 書き込み増幅は空きブロックの不足から大きくなります。容量の余裕、TRIM の動作、持続書き込みのスペックを見ます。
  • 依存的なアクセスは QD1 なので遅延がすべてです。浅いキューではデバイスのアップグレードの利得は小さいのです。
  • クラウドボリュームは契約上限がデバイス性能より先に来ます。チューニングの前に上限を確認します。

次回はネットワークです。帯域は足りているのに遅い理由を、レイテンシ、RTT、再送で追跡します。

X