磁盘存储 入门
7 个 section,3 个论断。盘片把一次 4 KB 读的 99.8% 花在等。闪存盘用六七次写回答你的一次写。一块百万 IOPS 的盘,你一次只问一件事,它给你 28,571。下面每一个数字,都从它旁边那张图上读出来。
磁头,和它在等的东西
4 KB 随机读是存储的经典基准。在盘片上它要 8.34 ms, 而其中几乎没有一点是在读。
下面的数字来自 Seagate Exos X18 产品手册:7,200 RPM、平均寻道 4.16 ms、持续 258 MB/s。三个里有两个是盘在干没用的事。
第一个纯粹是几何。就算机械臂已经在对的磁道上,磁头还得等扇区转过来,它还没走完的那段弧就是等待。拖动盘片转一圈:
注意,没有任何办法把它变短。7,200 RPM 下转一圈 8.33 ms, 所以这段等待均匀分布在 0 到 8.33 之间,平均就是半圈。磁头之上的东西都影响不了它 —— 更快的 CPU、更大的缓存、更好的计划:盘片转得一样快。
走到磁道也要钱。把磁头甩过整张盘,从它下面那条曲线上读寻道时间:
注意平均值落在哪儿:全程的三分之一。随机两条磁道平均相距行程的三分之一,所以 0.5 ms 整定加 11 ms 的三分之一是 4.17 ms —— 正是规格书上的 4.16 ms。画的 12 圈,每圈约 4 万条真磁道。
把两段等待加上传输,就是一次读的全部代价。传输是唯一真在搬数据的部分;其余全是磁头没在读。把读从 4 KB 往大了拖,看这两块的比例翻过来:
时,99.8% 的时间在等:120 IOPS、491 KB/s。到 ,等待掉到 20%,传输终于交出 205 MB/s。盘片 258 MB/s 的标称只有在 MB 级 I/O 上够得着,它的 4 KB 数字是那个的百分之一。
所以在这个设备上,摆放比带宽重要。同样 64 个块,按散开的 4 KB 请求读要付 64 次等待;按一条连续段读只付一次。在块数不变的前提下切换摆法:
因为磁头在连续段里根本不动,64 个块合起来 9.34 ms,分开要 534 ms ——同样的字节快 57 倍。每一条预写日志、每一次 LSM 压缩,存在的意义都是把第一种模式变成第二种。
这些数字只有摆在 CPU 本来能干什么旁边时才有意义。把滑块从 L1 命中一路拖到盘片,在每一级读出那个倍数:
值得背下来的一步是 DRAM 到 NVMe:80 ns 到 35 µs,440 倍。盘片是一次 L1 命中的 830 万倍 —— 存储之上每一层缓存存在的理由。
闪存内部
SSD 不是一块把动件拿掉的硬盘。它是一个完全不同的设备,穿着硬盘的接口,而这身伪装是要付钱的。
一个闪存单元就是带绝缘口袋的晶体管,你往口袋里推电子,口袋里的电荷就是存的值。让闪存变便宜的把戏,是把这个范围切成更多档。
可以切的电荷窗口只有固定的一个,所以多存一位并不会多给一个窗口 —— 它拿走的是相邻两档之间的余量的一半。逐个走过 4 种工艺,看这段余量塌下去:
注意这个算术。3 位要 8 档、7 个间隔,所以 TLC 单元的余量是SLC 余量的七分之一,QLC 是十五分之一。这就是 QLC 标 1,000 次编程/擦除而 SLC 标 100,000 的全部原因:氧化层没变,变的只是读出电路允许犯多大的错。
而随着时间过去,读出电路的余地还会更小。每次擦除都把电子推过绝缘层、把它打坏,于是每一档的电荷分布都在变宽。把单元磨过它的标称寿命,看相邻两档碰上:
标称寿命不是一道悬崖。它是分布展宽刚好把余量吃满的那个点。电平重叠,读回来的位就是错的 —— 用 ECC 藏到藏不住,然后把块退休。这也是磨旧的盘变慢的原因:换参考电压重读,每次几十微秒。
同样的漏电在盘断电时照跑,而那时没有任何东西在刷新它。 JEDEC 的 JESD218 定了下限:一块磨到寿终的消费级 SSD 必须在 30 °C 下存住一年。把单元磨旧、把柜子捂热:
因为电荷流失是热激活的,大约每 9 °C 保持时间减半: 30 °C 下的新盘是五年, 55 °C 机架里磨旧的盘不到两个月。拿断电的 SSD 当归档,不叫归档。
单元之上坐着那个塑造了其余一切的不对称。你能编程一页 —— 4 到 16 KB —— 但你只能擦一整块页,而且块内的编程指针只往前走。填一块看看:
这里画了 16 页;真实 TLC 块装 768 到 2,304 页、每页 16 KB,也就是 12 到 36 MB。这一行没有回头路:要在写过的页上放新数据,必须先把整块擦掉。
那主机改写一个已经写过的块时会怎样?设备没法照字面办到。它把新的那份写到别处一页空闲页上,再把老的那页标成死的。两种都试一下:
因为物理位置动了而逻辑地址没动,总得有个东西记住数据去哪儿了。那张表就是闪存转换层。走一遍逻辑地址,看它怎么解析:
注意它有多贵:页级映射每 1 TB NAND 大约要 1 GB 控制器 DRAM, 这就是便宜盘用更粗的映射、或通过 HMB 借主机内存的原因,也是企业盘带电容的原因。那张表就是这块盘。
你没要过的那些写
原地改写做不到,于是覆盖写在活块里留下一地死页。把它们收回来,是 SSD 一辈子的活儿,也是它寿命的去处。
每次覆盖写都在身后留下一页失效页。空间用完不是因为盘满了而是因为盘乱了,而唯一的扫帚是擦除。
所以回收器必须先把还活着的页从块里搬出来,才能擦它,而这些搬运就是没人要过的写。一步一步走完一轮:
注意第 4 步的代价。这次擦除交出 16 页空闲, 但其中 8 页是用8 次搬运换来的 —— 主机从没下过、也永远看不到的写。这个比值就是写放大, 它决定这块盘能活多久。
算术只有一行。如果被回收的块有u 的比例还活着,擦掉它只能换来 1 − u 页空闲,所以主机写一页要花 1 ÷ (1 − u) 次物理写。拖动活页数,从条上读这个代价:
半活的块代价是 2 倍; 16 页里活 15 页是 16 倍。所以回收器的全部工作就是挑最空的块 —— 而它的全部麻烦是:没有余量的盘根本挑不出来。
这正是预留空间买到的东西:永远不告诉主机的那部分闪存,于是搬运总有地方落,回收总有一块快死透的可挑。把预留往上拖,看写放大掉下来:
贪心回收比闭眼回收强,所以实线远在虚线之下:挑最空的块而不是随便挑一块,把 预留下的 15.3 倍压到 6.6 倍, 则压到 1.6 倍。曲线是模拟的不是拟合的 —— 均匀随机 4 KB 覆盖写、贪心挑选回收目标、跑到稳态。
盘只能对它知道已死的页贪心,而文件系统删掉一个文件,如果不说一声,设备那边什么都没变。把discard 指令关掉,看回收器继续搬没人要的数据:
没有 TRIM,删掉的页在设备眼里都还活着, 于是回收器永远在搬它。这就是为什么每周要跑 fstrim、新机器上值得看一眼 lsblk --discard、以及为什么一个把 discard 吞掉的精简卷,是你在自己代码里永远找不到的性能 bug。
回收器在写路径上有个孪生兄弟。TLC 阵列可以按每单元 1 位编程 —— 又快、需要的余量又小 —— 所以盘先把一段突发吸进那个模式的一块区域,之后再折叠下来。往它的尽头之外写:
看 114 GB 处那道坎,那是三星 990 PRO 的动态缓存:突发装得下时 6.9 GB/s, 写到 平均剩2.9 GB/s,写到 剩 1.9。拷 30 GB 文件的评测量的是这条曲线的左边;备份任务量的是右边。
这一切最后都落到寿命预算上。一块 1 TB TLC 盘的单元大约能吸收 3,000 TB 的 NAND 写入;主机拿到的是这个数除以写放大。把写放大调大,看主机那一份缩水:
现在看那条虚线落在哪儿。三星给这块盘保 600 TBW, 而 3,000 ÷ 600 = 5 —— 标称本身就已经假设了大约 5 倍写放大。跑 、每天写 500 GB, 单元2.5 年就用完,而不是预算暗示的 。
第二张账单在盘还健康的时候就来了。搬运是真的设备写,所以回收器和应用抢同一批通道。把写入速率往上推:
看延迟曲线,别看那根条。空闲盘上 35 µs 的读,在回收器把通道占满之后要几毫秒 —— 恰好是应用最忙的时候。p99 在预发好好的、到生产就烂掉,通常就是这个。
那根线,和你一次问多少件事
一个百万 IOPS 的闪存阵列挂在 SATA 线后面,就是一块十万 IOPS 的盘。而一块百万 IOPS 的盘、你一次只问一件事,就是一块两万八千 IOPS 的盘。
闪存是并行的:每通道好几颗 die,每颗都能干不同的请求。你见不见得到这份并行,取决于线能扛多少、以及主机手上有多少个没完成的请求。
先说线,因为它是更简单的那个天花板。接口能扛的量由链路定死,它之上的部分都是主机够不着的闪存。切换接口:
注意 SATA 不是稍微慢一点,它是另一个年代:600 MB/s 对着 PCIe 4.0 x4 的 7.9 GB/s,窄 13 倍,而它后面那堆闪存本来能把更宽的那根喂满。上限之上的那部分,全是主机够不着的闪存。
不过线不是有意思的那个限制。SATA 用的控制器 AHCI, 是为一个物理上一次只能干一件事的设备定的:一个命令队列,32 个槽位。递更多命令看看:
第 33 条命令没地方放,于是它在主机里等着。 NVMe 的答案不是一个更大的队列而是很多个 —— 最多 65,535 个提交队列、每个 65,535 个槽位,实践中每 CPU 核一对,于是两个核提交 I/O 时永远不碰同一条 cache line。
队列只有在有东西可忙时才有用,而确实有:标称除以服务时间告诉你,至少要有 35 个操作同时在进行。把深度往上拖,数在干活的 die:
因为一颗 die 在它那 35 µs 里是全程忙的,只有一个在途请求时40 颗里有 39 颗闲着。这就是下一张图背后的全部机制 —— 天花板不是线也不是控制器,而是你成功叫醒了几块硅。
现在是让人意外的那部分。一块标称百万 IOPS 的盘,只有当每秒真有百万个请求在途时,才交得出那个数。把保持在途的数量往上拖,看到手的和付出的:
看那个拐点在哪儿。一次 4 KB 读大约 35 µs,所以是 28,571 IOPS ——标称的 2.9%。曲线正好在深度 35 跑满,再往上多出来的队列只表现为延迟: 时盘还是百万 IOPS, 只是每次 128 µs。
这个数字不是江湖传说,是 Little 定律。在途请求数等于吞吐乘每次耗时,所以你需要的深度是一个矩形的面积,两条边分别是 IOPS 和延迟。动其中任何一条:
虚线矩形就是规格书 —— 百万 IOPS、35 µs —— 面积 35。「你需要 QD 32」就是从这儿来的,它也告诉你怎么修:一个只报 IOPS、旁边不写延迟的基准,只报了矩形的一条边。
把 35 个请求维持在途是主机的活儿,而阻塞的 pread 干不了:它递一个、等着,而且每 4 KB 要过两次内核边界。一步步走过这条路径,再切到共享环:
开了页表隔离的后 Meltdown 时代 x86-64 上,一次系统调用来回约 1.5 µs, 之前 0.1 µs。百万 IOPS 下这是光切换特权级就烧掉 1.5 个核, 而那些读一个字节都还没碰。io_uring 跟内核共享环:一整批一次系统调用,或者零次。
熬过那块坏掉的盘
每一种冗余方案都在回答同一个问题 —— 坏 N 块盘而不丢字节 —— 并且用容量、用写入代价、或者用故障之后那段窗口的长度来收费。
从摆法开始:一条条带就是横跨一组盘的一行块,变的是多少块装数据、多少块装能重算出来的东西。
从条上读可用比例,从右边读扛得住几块坏。切换级别,再改每种下面的组宽:
注意这里显示的容错是保证值不是走运值。 3 对镜像任何情况下都扛得住坏 1 块,坏第 2 块只有在它避开第 1 块的搭档时才行 —— 按走运值写的 runbook 等于没写。RAID 0 是诚实的极端:每块的容量都能用,而第一块坏掉时整组一起走。
写入代价没那么显眼,咬人更勤。校验是整条带的函数,所以改一个块意味着:在换掉任何一个之前,得先把旧块和旧校验都读出来。走一遍一次小写:
RAID 5 上主机写一次等于设备 4 次 I/O,RAID 6 是 6 次,RAID 10 是 2 次 —— 这就是 OLTP 数据库坐在镜像上、归档坐在校验上的原因。切到 RAID 10,看那两次读消失:有镜像就没有要重算的东西,自然也没有要先读的。
然后是那段窗口。一块盘坏掉时,阵列靠把每块幸存盘从头读到尾来重建它,而且还要限速,好让阵列继续服务流量。把单盘容量往上拖,读重建时间:
重建时间是容量除以一个没跟着容量长的吞吐,所以 的盘按 150 MB/s 要 33 小时 —— 而这 33 小时里幸存盘全在满负荷跑,恰好是半死不活的盘最可能跟着走的时候。在 RAID 5 上,这段窗口里的第二次故障就是全丢。
而第二次故障并不是唯一的丢法。盘的规格是每 1014 位返回一次不可恢复读,也就是每 12.5 TB 一次 —— 而一次重建要读的比这多得多。设好组,读那个概率:
就算只有 4 块 4 TB 盘,按消费级错误率也有72% 的概率。 —— 一个再普通不过的 RAID 5 机架 —— 是 99.996%:重建预期会失败。企业盘规格好 10 倍,63.5% 下照样输掉这次抛硬币。
同一种故障还有个更坏的版本:它根本不报错。如果盘交回错的字节还说成功,校验帮不上忙 —— 阵列不知道哪一份是错的。scrub 一遍,再切换文件系统校验什么:
注意什么变了、什么没变:两种情况下损坏都照样发生。 ZFS 和 btrfs 把一个块的校验和存在它的父块里而不是它旁边,于是 scrub 能发现不匹配并从冗余里重建。 RAID 上的 ext4 没有任何东西可比,于是把坏块端上去,因为在盘看来这次读成功了。
到了机架尺度,同样的算术有个更便宜的答案:把一个对象切成k 个数据分片和m 个校验分片, 用 (k+m)/k 的存储扛住任意 m 个丢失。动其中任何一个:
Backblaze 公开的 Vault 布局是 17 + 3:用 1.18 倍存储扛住同时丢 3 片,对着三副本的 3 倍存储、只扛 2 片。代价是重建烧 CPU、一次读要摸 k 台机器。
「写下去了」到底什么意思
一次成功的 write() 保证的是内核拿到了你的字节。它对介质什么都没保证,而这两者之间的缝隙,就是已提交事务消失的地方。
一个字节走向介质的路上要经过三个断电就没的地方 —— 应用的缓冲区、页缓存、盘自己的 DRAM —— 每个对应的在险数据量都不一样。
沿路径走这次写 —— 拖它,或者一步一步 —— 读出在那个点断电要付什么, 对着什么活下来:
注意只有最后一站才算落盘。在页缓存里 Linux 按自己的节奏回写 ——vm.dirty_expire_centisecs 是 3000,所以脏页可以躺 30 秒 —— 在盘上缓存里,数据离没了只差一颗电容。fsync 把字节送到路径尽头,送到之前不返回。
顺序和时机一样要紧,而它最先坏在两次互相依赖的写上。校验阵列更新一个块和覆盖它的校验是分开的两次写。在这两次之间断电,再切到写时复制:
这就是写洞,注意没有任何东西能发现它:条带自身是自洽的胡话,于是之后的重建重构出从没写过的字节。 ZFS 从不原地更新一条条带 —— 它整条写新的、再切指针 —— 所以根本没有可以被打断的窗口。
同样的危险在上一层也住着。journal、WAL、LSM manifest 都是一个套路:先写数据, 再写一条指向它的记录。让设备乱序,然后断电:
只要提交记录比它指向的数据先到介质,恢复就会重放一个 payload 从没写过的事务 ——静默损坏,发生在一个为了防它才有 journal 的系统里。顺序必须主动要:两者之间插一条 cache flush,或者用 FUA 写。
陷阱在这儿:对的和又快又错的在代码里几乎一样,而快的那个能通过每一个不拔电源的测试。
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);
于是要刷两次 —— 而每一次都按硬件的价钱收费。切换盘上缓存,再把多个事务打包进一次刷盘:
消费级盘上一次刷盘要把缓存推进 NAND:1.1 ms, 于是每秒 909 次落盘提交。企业盘断电后还能把缓存排空,所以 60 µs 就确认 —— 快 18 倍,每次刷盘 32 个事务就是 29,091。
对任何存储栈值得问三个问题:到落盘之前的窗口有多长、谁可以乱序、以及硬件是不是从易失内存里确认的。
速查
三个值得冷启动答出来的问题,以及五个红旗。
为什么 dd if=/dev/zero 是个错的写基准?
因为很多控制器在写之前会压缩或去重,于是一整块 0 根本到不了闪存。把缓冲区里 0 的比例拉上去,看打印出来的数字把 NAND 甩在后面:
过了 77% 是 0 之后,还在限制那个打印数字的只剩 PCIe 链路:报出来 14 GB/s,对着真到闪存的 3.2。用 fio 加 --refill_buffers, 或者拿 /dev/urandom 填种子。
什么时候裸 NVMe 加 O_DIRECT 比文件系统划算?
当工作集已经长过页缓存的时候 —— 那时缓存是在为一个永远达不到的命中率搬字节。把工作集拖过 RAM,比较两条路径:
在 1× RAM 以下缓存无敌 —— 一次命中 100 ns 对着 35 µs。过了这条线两条曲线合拢,双重缓存成了纯成本。这就是 Postgres、ScyllaDB、Oracle ASM 自己管缓冲池的原因,也是除此之外 NVMe 上的 XFS 比你一个季度能重造的东西更快的原因。
为什么新盘跑基准比你部署的那块快?
因为每一块都是擦干净的,回收器根本不跑,写放大正好是 1。把容量整个写一遍,看你本来要上报的那个数字塌下去:
、 —— 同一块盘、差 6.5 倍,中间只隔着一个回收器。
- 拿
--iodepth=1做容量规划。那量的是系统调用开销,不是盘。 - 4 TB 以上还上 RAID 5。18 TB 重建 33 小时,而这段窗口里撞上不可恢复读的概率是 99.996%。
- 因为「是 SSD」就跳过
fsync。介质从来没决定过持久性 —— 任何设备上都有 30 秒在险。 - 假设 discard 到了盘上。精简卷会把 TRIM 吞掉,症状是一个几个月里长起来的 p99。