ディスクストレージ 基礎
7 つのセクション、3 つの主張。プラッタは4 KB の読みの 99.8% を待つことに費やす。フラッシュドライブは 1 回の書き込みに6 回も 7 回もで応える。 100 万 IOPS のドライブは、一度に 1 件しか頼まなければ28,571 しか返さない。以下のすべての数字は、隣の図から読み取れる。
ヘッドと、それが待っているもの
4 KB ランダム読みはストレージの定番ベンチマークだ。プラッタ上では 8.34 ms かかり、そのうち読んでいる時間はほぼない。
以下の数字は Seagate Exos X18 の製品マニュアルのもの:7,200 RPM、平均シーク 4.16 ms、持続 258 MB/s。3 つのうち 2 つはドライブが役に立たないことをしている時間だ。
最初のひとつは幾何学だ。アームが正しいトラックに乗ってもなお、ヘッドはセクタの到着を待たねばならず、残っている弧がその待ちだ。プラッタを回してほしい:
注目してほしいのは、それを短くする手段が存在しないこと。 7,200 RPM で一回転は 8.33 ms、だからこの待ちは 0 から 8.33 に一様に分布し、その平均は半回転そのものだ。ヘッドより上の何もこれに触れない —— 速い CPU も、大きなキャッシュも、良い実行計画も。
トラックまで行くのにも代金がいる。ヘッドをプラッタの端まで振り、その下の曲線からシーク時間を読み取ってほしい:
平均がどこに落ちるかを見てほしい —— 全行程の 3 分の 1 だ。無作為な 2 本のトラックは平均でストロークの 3 分の 1 離れており、整定 0.5 ms に 11 ms の 3 分の 1 を足して 4.17 ms —— データシートの 4.16 ms そのものだ。描いた 12 本のリングは 1 本あたり約 4 万本にあたる。
2 つの待ちに転送を足せば、1 回の読みのコスト全部になる。転送だけがデータを動かしている部分で、それ以外はすべてヘッドが読んでいない時間だ。読みを 4 KB から大きくして、比率がひっくり返るのを見てほしい:
では時間の 99.8% が待ち —— 120 IOPS、491 KB/s。 では待ちが 20% まで落ち、転送がようやく 205 MB/s を出す。 258 MB/s という定格は MB 級の I/O でしか届かず、 4 KB の数字はその 100 分の 1 でしかない。
だからこのデバイスでは配置が帯域に勝つ。同じ 64 ブロックでも、散らばった 4 KB リクエストとして読めば 64 回の待ちを払い、1 本の連続として読めば 1 回で済む。ブロック数はそのままに、配置だけ切り替えてほしい:
連続読みではヘッドが一度も動かないので、 64 ブロックはまとめて 9.34 ms、ばらばらなら 534 ms—— 同じバイト数で 57 倍速い。先行書き込みログも LSM のコンパクションも、 1 つ目のパターンを 2 つ目に変えるために存在している。
これらの数字は、CPU が代わりに何をできたかと並べて初めて意味を持つ。スライダーを L1 ヒットからプラッタまで下ろし、各段でその倍率を読んでほしい:
覚える価値があるのは DRAM から NVMe への一段:80 ns から 35 µs、440 倍。プラッタは L1 ヒットの 830 万倍 —— ストレージより上のあらゆるキャッシュが隠そうとしている溝だ。
フラッシュの中身
SSD は可動部を取り去ったハードディスクではない。ハードディスクのインタフェースをまとった、まったく別のデバイスであり、その変装には代金がかかる。
フラッシュセルは絶縁されたポケットに電子を押し込めるトランジスタで、ポケットの中の電荷が保存された値だ。フラッシュを安くした仕掛けは、その範囲をより多くの段に切ることだった。
切り分けられる電荷ウィンドウは固定の 1 つしかないので、ビットを 1 つ増やしてもウィンドウは増えない —— 増えた分は隣の段との余裕の半分を食う。 4 つの世代を順に切り替えて、その余裕が潰れていくのを見てほしい:
この算術に注目してほしい。3 ビットには 8 段、つまり隙間は 7 つ。だから TLC セルの余裕はSLC の余裕の 7 分の 1、 QLC は 15 分の 1 だ。QLC が 1,000 回、SLC が 100,000 回と定格される理由はこれがすべてで、酸化膜は何も変わっていない。変わったのは読み出し回路が間違ってよい幅だけだ。
しかもその幅は時間とともに減る。消去のたびに電子が絶縁層を通り抜けてそれを傷つけるので、各段の電荷分布は広がる。定格を超えてセルを摩耗させ、隣どうしが出会うのを見てほしい:
定格は崖ではない。広がりがちょうど余裕を埋め切る点のことだ。段が重なり、読み出しは誤ったビットを返す —— 限界まで ECC で隠され、限界が来ればブロックは引退する。摩耗したドライブが遅くなるのもこれで、参照電圧をずらした読み直しは 1 回ごとにマイクロ秒を払う。
同じ漏れは電源を切っている間も進み、そのときは何も refresh していない。 JEDEC の JESD218 が下限を決めている —— 摩耗したクライアント SSD は30 °C で 1 年データを保たねばならない。セルを摩耗させ、棚を暖めてほしい:
電荷の喪失は熱で活性化されるので、保持期間はおよそ 9 °C ごとに半分になる —— 30 °C の新品なら5 年、 55 °C のラックの摩耗品なら2 か月未満だ。電源を切った SSD に置いたアーカイブは、アーカイブではない。
セルの上には、残りすべてを形づくる非対称がある。プログラムできるのは1 ページ —— 4 から 16 KB —— だが、消せるのはページ 1 ブロックまるごとで、しかもブロック内のプログラムポインタは前にしか進まない。1 つ埋めてみてほしい:
ここでは 16 ページ。実際の TLC ブロックは 16 KB のページを 768 から 2,304 枚、つまり 12 から 36 MB だ。この列に戻り道はない —— すでに書いたページへ新しいデータを置くには、ブロックまるごとを先に消すしかない。
ではホストがすでに書いたブロックを書き直したら?デバイスはそれを文字どおりには果たせない。新しい方をどこか別の空きページに書き、古い方を死んだものとして印をつける。両方試してほしい:
物理的な位置が動き、論理アドレスが動かなかったので、データの行き先を覚えている何かが要る。その表がフラッシュ変換層だ。論理アドレスを歩いて、それが解決されるのを見てほしい:
これがどれだけ高くつくかに注目してほしい。ページ単位の対応表は NAND 1 TB あたりコントローラ DRAM 約 1 GB を要求する。安価なドライブが粗い対応表を使い HMB でホストのメモリを借りる理由であり、企業向けドライブがコンデンサを積む理由だ。その表こそがドライブなのだから。
頼んでいない書き込み
その場での書き直しは不可能なので、上書きは生きたブロックの中に死んだページを取り残す。それを回収することに、 SSD は一生と寿命を費やす。
上書きのたびに無効ページが 1 枚残る。空き容量が尽きるのは満杯だからではなく散らかっているからで、箒は消去しかない。
だからコレクタは消去の前にまだ生きているページをブロックの外へ運び出さねばならず、その退避が誰も頼んでいない書き込みになる。 1 周を 1 手ずつ辿ってほしい:
4 手目の代金に注目してほしい。この消去は空きページを 16 枚生んだが、そのうち 8 枚は8 回の退避で買ったものだ —— ホストが一度も発行せず、今後も見ることのない書き込みで。この比が書き込み増幅であり、ドライブの寿命を決める。
算術は 1 行で済む。回収されるブロックのu の割合が生きているなら、消去して得られる空きは 1 − u だけ。だからホストの 1 ページは物理書き込み 1 ÷ (1 − u) 回を要する。生存ページ数を動かして、バーからその代金を読んでほしい:
半分生きたブロックなら代金は 2 倍、 16 枚中 15 枚なら 16 倍。だからコレクタの仕事は最も空のブロックを選ぶことに尽き、その悩みは、余裕のないドライブには選ぶべきものが存在しないことに尽きる。
予備領域が買うのはまさにそれだ —— ホストに決して知らされないフラッシュがあれば、退避の置き場も、回収すべき死にかけのブロックも、常にある。予備を上げて、増幅が落ちるのを見てほしい:
貪欲な回収は盲目な回収より強い。だから実線は破線のはるか下を通る —— 最も空のブロックを選ぶだけで、予備 での 15.3 倍が 6.6 倍まで落ち、 なら 1.6 倍になる。曲線は当てはめではなくシミュレーションだ —— 一様ランダムな 4 KB 上書き、貪欲な victim 選択、定常状態まで実行。
ドライブが貪欲になれるのは「死んでいると知っているページ」に対してだけで、ファイルシステムがファイルを消しても、そう言わない限りデバイス側では何も変わらない。discard 命令を切って、コレクタが誰も欲しがらないデータを運び続けるのを見てほしい:
TRIM がなければ消したページも生きて見えるので、コレクタは永久に退避し続ける。毎週 fstrim を走らせる理由、新しいホストでlsblk --discard を確かめる価値がある理由、そして discard を飲み込むシンプロビジョニングのボリュームが自分のコードでは見つからない性能バグである理由がこれだ。
コレクタには書き込み経路にも双子がいる。TLC アレイは 1 セル 1 ビットでプログラムできる —— 速く、必要な余裕もずっと小さい —— ので、ドライブはバーストをそのモードの領域に吸い込み、後で畳み下ろす。その終わりの向こうまで書いてほしい:
114 GB の崖を見てほしい。Samsung 990 PRO の動的キャッシュだ —— バーストが収まるうちは 6.9 GB/s、 では平均2.9 GB/s、 なら 1.9。 30 GB のファイルをコピーするレビューはこの曲線の左側を、バックアップジョブは右側を測っている。
すべては寿命の予算に着地する。1 TB の TLC ドライブのセルが吸収できるのは NAND 書き込みでおよそ 3,000 TB。ホストが受け取るのはそれを増幅で割った量だ。増幅を上げて、ホストの取り分が縮むのを見てほしい:
破線の印がどこにあるかを見てほしい。 Samsung はこの部品を 600 TBW で保証しており、3,000 ÷ 600 は 5 —— 定格そのものがおよそ 5 倍の増幅を前提にしている。で 1 日 500 GB なら、セルは予算が示唆する ではなく2.5 年で尽きる。
2 枚目の請求書は、ドライブがまだ健康なうちに届く。退避は本物のデバイス書き込みなので、コレクタはアプリケーションと同じチャネルを奪い合う。書き込み速度を上げてほしい:
バーではなくレイテンシの曲線を見てほしい。遊休なら 35 µs の読みが、コレクタがチャネルを飽和させた途端にミリ秒になる —— アプリケーションが最も忙しいときに。ステージングでは問題ない p99 が本番でひどいのはたいていこれだ。
配線と、一度に何件頼むか
100 万 IOPS のフラッシュアレイも SATA ケーブルの後ろにあれば 10 万 IOPS のドライブになる。そして 100 万 IOPS のドライブも、一度に 1 件しか頼まなければ 2 万 8 千 IOPS のドライブになる。
フラッシュは並列だ。チャネルごとに複数のダイが別々のリクエストを処理できる。それが見えるかどうかは配線が何を運べるかとホストが未完了のリクエストを何件抱えているかで決まる。
まず配線、こちらのほうが単純な天井だから。インタフェースが運べる量はリンクで固定され、それより上はホストが届かないフラッシュだ。インタフェースを切り替えてほしい:
SATA が少し遅いのではなく別の時代だということに注目してほしい。 PCIe 4.0 x4 の 7.9 GB/s に対して 600 MB/s、13 分の 1 の幅しかなく、その後ろのアレイは広いほうを飽和させられる。天井より上はすべてホストが届かないフラッシュだ。
とはいえ配線は面白いほうの限界ではない。SATA が使うコントローラ AHCI は、物理的に一度に 1 件しか処理できないデバイスのために規定された —— コマンドキュー 1 本、深さ 32。それより多くのコマンドを差し出してほしい:
33 番目のコマンドを置く場所がない。だからそれはホスト側で待つ。 NVMe の答えはより大きなキューではなく、たくさんのキューだった —— 最大 65,535 本の提出キュー、各 65,535 段。実際には CPU コアごとに 1 対で、 2 つのコアが I/O を投入しても同じキャッシュラインに触れない。
キューは忙しくさせる相手がいて初めて役に立つ。そして相手はいる —— 定格を処理時間で割れば、少なくとも 35 の操作が同時に進んでいなければならない。深度を上げて処理中のダイを数えてほしい:
ダイはその 35 µs のあいだずっと塞がっているので、実行中が 1 件なら40 個のうち 39 個が遊んでいる。次の図の背後にある仕組みはこれがすべてだ —— 天井は配線でもコントローラでもなく、何枚のシリコンを起こせたかで決まる。
ここからが意外な部分だ。100 万 IOPS 定格のドライブがその数を出すのは、毎秒 100 万件が実際に実行中であるときだけだ。実行中に保つ数を上げて、手に入るものとその代金を見てほしい:
膝がどこにあるかを見てほしい。4 KB の読みは約 35 µs なので、なら 28,571 IOPS ——定格の 2.9% だ。曲線はちょうど深度 35 で飽和し、それより先では余った深さがレイテンシとしてしか現れない。 でも 100 万 IOPS だが、 1 件あたり 128 µs になる。
この数字は言い伝えではなくリトルの法則だ。実行中の件数はスループットに 1 件あたりの所要時間を掛けたものなので、必要な深度はIOPS とレイテンシ を辺に持つ長方形の面積になる。どちらでも動かしてほしい:
破線の長方形はデータシート —— 100 万 IOPS、35 µs —— 面積は 35 だ。「QD 32 が要る」はここから来ており、直し方も同時に分かる ——レイテンシを添えずにIOPS だけを報告したベンチマークは、長方形の辺を 1 本しか報告していない。
35 件を実行中に保つのはホストの仕事で、ブロッキングな pread にはそれができない。1 件投入して待ち、しかも 4 KB ごとにカーネル境界を 2 回またぐ。経路を辿ってから、共有リングに切り替えてほしい:
ページテーブル分離を有効にした Meltdown 以後の x86-64 では、システムコールの往復は約 1.5 µs、以前は 0.1 µs だった。100 万 IOPS では切り替えだけで CPU 1.5 コアを焼く —— まだ 1 バイトも読まないうちに。io_uring はリングを共有し、バッチ 1 回、SQPOLL なら 0 回だ。
死ぬドライブを生き延びる
あらゆる冗長化はひとつの問いに答える —— N 台の故障をバイトを失わずに生き延びること —— そして容量か、書き込みコストか、故障後の窓の長さで代金を取る。
まず配置から。ストライプはドライブ群をまたぐブロックの 1 行で、変わるのは何枚がデータを持ち、何枚が再計算できるものを持つかだけだ。
バーから使える割合を、右から耐えられる故障台数を読んでほしい。レベルを切り替え、それぞれでグループの幅も変えてほしい:
ここに出ている耐性が保証される数であって運がよければの数ではないことに注目してほしい。3 対のミラーは常に 1 台に耐え、 2 台目は最初の相方を外したときだけ耐える —— 運のよい数で書かれた runbook は書かれていないのと同じだ。RAID 0 は正直な極端で、全台分の容量が使え、最初の 1 台とともに死ぬ。
書き込みコストは目立たないぶん、より頻繁に噛みつく。パリティはストライプ全体の関数なので、1 ブロックを変えるには、どちらかを置き換える前に古いブロックと古いパリティを読まねばならない。小さな書き込みを 1 回、順に辿ってほしい:
RAID 5 ではホストの 1 回の書き込みがデバイス I/O 4 回、RAID 6 は 6 回、 RAID 10 は 2 回 —— OLTP データベースがミラーに、アーカイブがパリティに座る理由だ。RAID 10 に切り替えて2 回の読みが消えるのを見てほしい。ミラーには再計算するものがないので、先に読むものもない。
そして窓がある。1 台が死ぬと、アレイは生き残った全ドライブを端から端まで読んでそれを再構築する。しかもアレイがトラフィックを捌き続けられるよう絞りながらだ。 1 台の容量を上げて、再構築時間を読んでほしい:
再構築時間は容量を「容量に合わせて伸びなかったスループット」で割ったものだ。だから は 150 MB/s で 33 時間 —— その 33 時間ずっと生き残りは 100% で回る。弱ったドライブが 1 台目に続く確率が最も高い瞬間そのものだ。 RAID 5 では、その窓の中の 2 台目の故障が全損になる。
しかも 2 台目の故障だけが失い方ではない。ドライブは 1014 ビットに 1 回の回復不能読み取りを返すと規定されている —— つまり 12.5 TB に 1 回 —— そして再構築はそれよりはるかに多くを読む。グループを設定して、その確率を読んでほしい:
4 TB × 4 台でさえ、コンシューマの誤り率では72% だ。 —— ごく普通の RAID 5 の棚 —— は 99.996%、つまり再構築は失敗すると期待される。エンタープライズ品は 10 倍良い規定でも 63.5% で負ける。
同じ故障のより悪い版は、エラーをまったく返さない。誤ったバイトを返して成功と報告されたら、パリティは助けにならない —— どちらの複製が誤りなのかを知らないからだ。スクラブして、ファイルシステムが何を検査するかを切り替えてほしい:
何が変わって何が変わらないかを見てほしい —— 破損はどちらでも起きている。 ZFS と btrfs はブロックのチェックサムをその隣ではなく親に置くので、スクラブが不一致を検出し、冗長性から作り直す。 RAID 上の ext4 には比べる相手がないので壊れたブロックをそのまま渡す —— ドライブにしてみれば、その読みは成功したのだから。
ラック規模では、同じ算術にもっと安い答えがある —— オブジェクトをk 個のデータシャードとm 個のパリティシャードに割れば、ストレージ (k+m)/k で任意の m 個の喪失に耐える。どちらでも動かしてほしい:
Backblaze が公開する Vault の構成は 17 + 3 —— ストレージ 1.18 倍で同時 3 シャードの喪失に耐える。三重複製は 3 倍で 2 つまでだ。代金は再構築の CPU と、k 台に触れる読み。
「書けた」とは何のことか
成功した write() が約束するのは、カーネルがバイトを受け取ったということだけだ。媒体については何ひとつ約束していない。その隙間こそ、コミット済みのトランザクションが消える場所だ。
媒体へ向かう 1 バイトは、電源が落ちれば中身を失う場所を 3 つ通る —— アプリのバッファ、ページキャッシュ、ドライブ自身の DRAM —— それぞれ危険にさらされるデータ量が違う。
書き込みを経路に沿って動かし —— ドラッグでも 1 段ずつでも ——その地点で電源が落ちたときの代金を、生き延びるものと対で読んでほしい:
永続なのは最後の 1 地点だけだ。ページキャッシュでは Linux が自分の都合で書き戻す ——vm.dirty_expire_centisecs は 3000、つまりダーティページは 30 秒居座りうる —— ドライブのキャッシュでは、データはコンデンサ 1 つ分だけ消滅から離れている。fsync はそのバイトを終点まで連れていき、着くまで返らない。
順序はタイミングと同じだけ効き、互いに依存する 2 つの書き込みで最初に壊れる。パリティアレイはブロックとそれを覆うパリティを別々に更新する。その間で電源を落とし、それからコピーオンライトに切り替えてほしい:
これがライトホールだ。誰もそれを検出しないことに注目してほしい —— ストライプはそれ自体としては整合した無意味であり、後の再構築は一度も書かれなかったバイトを組み立てる。 ZFS はストライプをその場で更新しない —— 丸ごと新しく書いてポインタを差し替える —— ので、割り込まれる窓がそもそも存在しない。
同じ危険は 1 つ上の層にも住んでいる。ジャーナルも WAL も LSM のマニフェストも同じ手口だ —— データを書き、それからそれを指すレコードを書く。デバイスに並べ替えを許し、電源を落としてほしい:
コミットレコードがそれが指すデータより先に届いた瞬間、復旧は本体の書かれていないトランザクションを再生する ——それを防ぐジャーナルを持つシステムでの静かな破損だ。順序は明示的に要求するしかない。
罠はここだ。正しい版と速くて誤っている版はほぼ同じ形で、速いほうは電源を抜かないテストをすべて通る。
write(data_fd, payload, n); write(log_fd, commit_rec, m); # 到達順は任意 write(data_fd, payload, n); fdatasync(data_fd); # payload は媒体上 write(log_fd, commit_rec, m); # ここで初めて許される fdatasync(log_fd);
つまりフラッシュは 2 回、それぞれハードウェアの言い値だ。キャッシュを切り替え、トランザクションを 1 回にまとめてほしい:
コンシューマ機ではフラッシュがキャッシュを NAND へ押し込む —— 1.1 ms、つまり毎秒 909 件の永続コミットが上限だ。エンタープライズ品は電源断の後でもキャッシュを空にできるので 60 µs —— 18 倍速く、1 回に 32 件なら 29,091 件になる。
どんなストレージスタックにも問う価値のある問いは 3 つだ ——永続になるまでの窓はどれだけ長いか、誰が並べ替えてよいか、ハードウェアは揮発メモリから応答していないか。
クイックリファレンス
何も見ずに答えられる価値のある 3 つの問いと、5 つの赤旗。
dd if=/dev/zero が書き込みベンチマークとして誤りなのはなぜか?
多くのコントローラが書く前に圧縮または重複排除するので、ゼロで埋めたバッファはフラッシュに届かないからだ。バッファのうちゼロの割合を上げて、表示される数字が NAND を置き去りにするのを見てほしい:
ゼロが 77% を超えると、表示される数字を制限しているのは PCIe リンクだけになる —— 報告は 14 GB/s、フラッシュに届いたのは 3.2。fio に --refill_buffers を付けるか、/dev/urandom を種にすること。
生の NVMe と O_DIRECT がファイルシステムに勝つのはいつか?
ワーキングセットがページキャッシュを追い越したときだ —— そのときキャッシュは、決して届かないヒット率のためにバイトを写している。ワーキングセットを RAM の向こうへ動かし、2 つの経路を比べてほしい:
RAM の 1 倍より下ではキャッシュに勝てない —— ヒットは 35 µs に対して 100 ns。越えれば 2 本の曲線は寄り、二重キャッシュは純粋なコストになる。 Postgres も ScyllaDB も Oracle ASM も自前のバッファプールを持つ理由であり、それ以外では NVMe 上の XFS が四半期で作り直せるものに勝つ理由だ。
新品のドライブが、配備するドライブより速く測れるのはなぜか?
全ブロックが消去済みでコレクタが一度も走らず、書き込み増幅がちょうど 1 だからだ。容量を 1 周書き込んで、報告するはずだった数字が崩れるのを見てほしい:
、 —— 同じドライブ、あいだにいるのはコレクタだけだ。
--iodepth=1で容量計画をする。測っているのはシステムコールのオーバーヘッドでドライブではない。- 4 TB を超える RAID 5。18 TB で再構築 33 時間、その窓で回復不能読み取りに当たる確率 99.996%。
- 「SSD だから」と
fsyncを省く。永続性を決めたのは媒体ではない —— どのデバイスでも 30 秒が危険にさらされる。 - discard が届いていると仮定する。シンボリュームが TRIM を飲み込み、症状は数か月で育つ p99 だ。