很多人把编程里的“存储”想得很简单——不就是一个硬盘放文件嘛。但真当你要设计一个数据库引擎、做一个高性能消息队列、或者调优一套存储系统的IO路径时,你会发现,物理存储层的那点事儿,决定了你上层所有逻辑的“地基稳不稳”。我记得自己刚接触这块的时候,翻了很多教程,全是大词儿:“柱面”“磁头”“扇区”“磨损均衡”“写放大”,名词倒背如流,可一落地就懵——这些跟我写的代码到底有什么关系?直到自己亲手压测、看着iostat里的await和svctm发愣,又亲手把机械盘换固态、跑通一遍block层的IO模型,才算真正把这层窗户纸捅破。
这一讲是“核心体系架构-物理存储(中)”,咱们不绕弯子,就聚焦三件事:物理存储介质究竟怎么组织数据、操作系统和硬件之间怎么对齐、IO栈上每一层又是怎么影响你最终看到的性能数字。内容直接照着实际项目里用得上的深度来,适合正在做存储相关开发、或者想搞明白“为什么我的磁盘这么慢”的工程师。
1. 物理存储这堂课到底在讲什么
1.1 为什么“物理存储”是体系架构的承重墙
物理存储不是硬盘厂商的说明书,而是整个数据世界的底层逻辑。你写一行代码,调用一个write()函数,这行数据从用户态到内核态,从页缓存刷到块设备,最后落在盘面的某个磁道上——中间每一个环节都受物理存储特性的制约。选型的时候如果忽略它,后面出的问题几乎都是“结构性”的,不是改两行配置能救回来的。
举一个最直白的例子:为什么机械硬盘时代,数据库DBA们天天念叨“随机IO是魔鬼”?本质上是机械臂的物理运动周期决定的。磁头从一个磁道移动到另一个磁道,是典型的机械动作,以ms为单位;而CPU/内存操作是以ns为单位计算,两者差了几个数量级。而到了SSD时代,随机IO性能暴涨,不是因为软件变聪明了,而是NAND闪存的“寻址”不再需要物理移动,电子迁移的速度比机械运动快几个数量级。可见,介质的物理特性变了,上层体系架构的整个性能模型也跟着变。
所以我一直觉得,学体系架构不先吃透物理存储,等于盖楼不打地基。而你一旦掌握了物理层的行为模式,再看上层的页缓存、IO调度器、文件系统,很多时候都能“未卜先知”地猜出它们为什么要那样设计。
1.2 一次读写请求在物理层经历了什么
为了让大家有直观体感,我们先从头到尾跟一个读请求走一遭。假设你写了一个程序去读文件/var/lib/mysql/data/user.ibd的某段内容:
- 应用通过
read()系统调用进入内核,在虚拟文件系统(VFS)层定位到具体文件系统和inode; - 文件系统把文件内偏移转换为主机可见的“逻辑块地址”(LBA);
- 通用块层(Generic Block Layer)以
bio结构体封装请求,交给IO调度器排队; - IO调度器按策略(如NOOP、Deadline、mq-deadline)合并、排序请求,最终派发给设备驱动;
- 设备驱动把LBA翻译成物理存储介质内部的三维/多维地址,比如机械盘的柱面-磁头-扇区(CHS),或SSD的通道-芯片-块-页;
- 硬件真正执行动作——磁头寻道+盘片旋转到目标扇区,或者闪存控制器从目标Page读取电荷状态;
- 数据从设备寄存器/缓存通过DMA搬入内存,再copy_to_user返回应用。
每一次读请求,都要走完这整条链路。看到没有,物理存储的每个环节都嵌在这条链路里。这也就是为什么优化不能只看单一层——有时候你觉得是磁盘慢,其实问题在IO调度器,或在文件系统预读策略。不过别急,这些后面会一一展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 机械硬盘的物理组织:柱面、磁头与扇区的真实关系
2.1 盘片上的基础结构:磁道、扇区、柱面到底怎么数
机械硬盘(HDD)靠盘片旋转和磁头移动来读写数据。盘片一般用铝合金或玻璃做基底,表面涂覆磁性介质。每个盘片有两个面,每面配一个磁头,磁头悬浮在盘面上方几十纳米的高度,通过感应磁场变化来读取数据,或者通过磁场改变磁化方向来写入数据。
盘面上,磁头从外圈到内圈,会在无数个同心圆上移动,每一圈就叫一个“磁道”(Track)。磁道不是连续的线,而是被切分为一段一段的弧,每一段叫一个“扇区”(Sector)。传统扇区大小是512字节,后来随着容量增大和纠错需求,逐步演进到4K高级格式化扇区(4096字节)。
那“柱面”是什么呢?一块硬盘如果有多张盘片叠在一起,不同盘面上相同半径的磁道,在垂直方向上会形成一个“圆柱面”。假设盘片A的第100号磁道和盘片B的第100号磁道,它们到主轴的距离完全一致,从三维角度来看,这些等半径磁道的集合,就是柱面。
柱面的概念在CHS寻址时代非常重要。因为磁头臂是所有盘片共用的,如果数据文件“顺着柱面方向排列”,也就是先在柱面0的各个磁头间依次移动,再切到柱面1,那么寻道时的机械移动距离会被压缩到最小,性能自然更好。这也是早期文件系统做“柱面组”设计的基本动机。
2.2 从CHS寻址到LBA逻辑块地址:一次跨越式的简化
老硬盘用CHS寻址,也就是靠“柱面号+磁头号+扇区号”三元组来定位一个扇区。听起来很直观,但实际用起来极其麻烦,因为不同硬盘的柱面数、磁头数、每磁道扇区数都不固定,而且BIOS和操作系统对这些字段的长度限制还不一样,寻址范围被卡得死死的。
后来业界引入LBA(Logical Block Address),硬盘被抽象成一个连续的“逻辑扇区数组”,从0开始编号,操作系统只需要告诉硬盘“我要第1024号逻辑扇区”,设备内部自己负责把LBA翻译成实际物理位置。这就好比把你家楼下的街道门牌号全部重编成1号、2号、3号,快递员不需要知道你具体住哪栋楼哪一单元了——他只要看着门牌号找就行。
从架构角度来说,LBA是一次漂亮的“解耦”。它把上层对存储的使用方式从物理细节中解放出来,文件系统不再关心盘片几何结构,只要能按块号读写即可。这也是为什么后来的SSD也能平稳接入——不管底层是什么介质,反正我只要LBA对的块号,给我数据就完事了。
2.3 寻道时间与旋转延迟:机械硬盘性能模型的手算推导
机械硬盘的两个关键性能指标,都被物理规律锁死。
第一是寻道时间(Seek Time)。磁头从一个磁道移动到另一个磁道的耗时,由移动距离决定,平均寻道时间通常标称为9ms左右(比如常见的企业级硬盘)。注意这只是一个统计平均值,实际上从最内圈到最外圈的寻道可能需要15-20ms,而相邻磁道之间只需要1-2ms。
第二是旋转延迟(Rotational Latency)。盘片转速决定它转半圈需要多久。以7200RPM为例,一转眼就是120转/秒,也就是每转耗时约8.33ms。平均旋转延迟取半圈,就是4.17ms。
那么一个随机读请求的物理耗时大约是多少?假设盘内平均寻道9ms,再加平均旋转延迟4.17ms,这已经是13.17ms。你再算一下IOPS(每秒IO操作次数):1000ms / 13.17ms ≈ 76。这就是为什么机械盘随机读的IOPS通常很难突破150,因为它无论怎么优化,物理动作摆在那里,总得给磁头留出移动和等待的时间。
很多文章喜欢说“顺序IO和随机IO差一个数量级”,我觉得实际可能更悬殊。顺序读场景下,磁头几乎不需要大跨度寻道,连续读取完全依赖盘片旋转,顺序读吞吐量可以做到150MB/s以上;而随机读可能掉到几个MB/s。差距不是十倍,是几十倍。知道了这个物理模型,你再去配置数据库、日志系统时,自然会往“尽量让IO变顺序”这个方向使劲。
3. SSD的物理世界:NAND闪存单元、Page与Block的生命周期
3.1 NAND闪存的最小存储单元与电荷捕获差异
SSD没有磁头、没有盘片,它用的是NAND闪存颗粒。闪存的基本存储单元是浮栅晶体管(Floating Gate Transistor)或电荷捕获晶体管(Charge Trap Transistor)。每个单元通过控制栅极施加电压,往浮栅里注入或移除电荷,来代表一个二进制状态。
关键点来了:这个单元的“多电平”能力,决定了SSD的容量和寿命档次。
- SLC(Single-Level Cell):每个单元只存1bit,只有0和1两种状态,写入快、寿命最长,通常在10万次擦写以上;
- MLC(Multi-Level Cell):每个单元存2bit,有4种电荷状态,寿命通常在1万-3万次;
- TLC(Triple-Level Cell):每个单元存3bit,有8种状态,寿命3000次左右;
- QLC(Quad-Level Cell):每个单元存4bit,有16种状态,寿命可能只有1000次上下。
状态越多,意味着在同一个物理尺寸里要分辨更精细的电荷量,读写时对电压窗口的要求更严格,也就更容易出错。这也是为什么TLC/QLC的写入延迟比SLC高,寿命更短,需要更强的纠错算法。
理解这一点,你就能明白为什么SSD的“标称容量”和“实际可用容量”之间总是有差距——一部分空间要留给坏块替换、预留空间(Over-Provisioning)和FTL映射表管理。不是厂商偷偷抠你容量,而是物理特性逼着控制器必须留后手。
3.2 Page与Block的层级关系:为什么读以页为单位、擦除以块为单位
闪存有一个非常反直觉的特点:写入以页(Page)为单位,但擦除必须按块(Block)来。这个“撕裂”决定了SSD主控的整个工作逻辑。
假设你修改了一个页里面的1个字节——你不能像机械盘那样原地覆盖写。对于NAND闪存,要改数据,得先把整个块擦掉(把所有单元恢复成1状态),然后在块里重新写入。因为块是擦除的最小单位,一个块通常包含128个或256个页,比如每个页4KB,一个块就有512KB甚至1MB。
这就带来了一连串连锁反应:
- 因为“改一个小数据”实际牵动的是整个块的操作,性能必然比逻辑上看要昂贵;
- 为了减少这种高成本操作,闪存控制器会引入“异地更新”(Out-of-Place Update)——新的数据先写到空闲页,旧页标记为“无效”,后续靠垃圾回收(GC)统一收拾;
- 闪存每个块的擦除次数有限,控制器还必须尽量让每个块被擦除次数均匀,否则有些块提前“报废”,整颗盘寿命崩溃,这就是“磨损均衡”(Wear Leveling)。
此外,NAND里的读写还存在并行单位“Plane”的概念。一个Die内部可以分多个Plane,控制器可以同时操作多个Plane来提升吞吐。你在标称参数上看到的“4通道”“8通道”,指的也是主控和闪存颗粒之间的并行通道数——通道越多,可以同时喂到颗粒的数据越宽,性能越高。
3.3 写入放大与磨损均衡:物理特性驱动的主控策略
“写入放大”(Write Amplification)是SSD领域绕不开的指标,定义是:实际写入闪存的数据量除以主机请求写入的数据量。
比如,主机写入4KB数据,但SSD因为要在块内做垃圾回收、把有效页搬运到新块、再擦除老块,实际可能要写20KB数据到物理介质。这个倍率可能是2倍、3倍甚至几十倍。写入放大系数越高,意味着对闪存寿命的消耗越大,同时性能也越差,因为大量的内部搬移动作挤占了带宽。
为了压低写入放大,主控会想尽办法:
- 维持足够的预留空间(OP),让它有地方做搬移和整理;
- 合理设计垃圾回收策略,尽量把数据搬移放在空闲的时候做,而不是阻塞用户的写请求;
- 使用TRIM命令感知上层已删除的页,提前把它们标记为无效,避免垃圾回收时再做无效搬运。
这些策略的核心目标,都是把“主机不可见的内部擦写量”降下来。所以你会看到,同品牌同容量的SSD,企业级往往比消费级预留空间更大、随机写入寿命更稳——贵的不是没道理。
4. 物理存储与逻辑存储之间的映射机制
4.1 文件系统的块与物理扇区:一次4K对齐问题的警钟
如果你管理过云主机或者自己组装过磁盘阵列,大概率遇到过“4K对齐”这个词。它看着像是Windows 7时代的老古董,实际上在今天依然非常重要。
文件系统把空间划分成“块”(Block)来管理,默认常见大小是4KB。而机械硬盘从传统512字节扇区过渡到4K高级格式化扇区(4Kn),也是4KB一个扇区。SSD的Page大小往往也是4KB、8KB或16KB,但FTL的映射粒度也通常以4KB为基准。
如果你把文件系统,恰好从物理扇区的“中间”开始划分块,那么一个4KB的逻辑块可能横跨两个物理扇区,读一次需要访问两次物理扇区,写一次也要碰两个扇区。这就是所谓的“未对齐”。
实测下来,未对齐时随机读写性能可以下降20%~50%(视具体情况),因为每一个IO请求都被硬件拆成了两半。更麻烦的是,某些文件系统在创建时若没有对齐分区边界,后续想改只能重新分区格式化,代价极高。
所以实操里,分区时最好用现代工具(如parted、fdisk)确认起始扇区能被8整除(因为4K扇区=8个传统扇区),或者直接采用系统默认对齐值。如果是在云环境创建云盘,厂商一般已经默认处理好对齐策略,但物理裸机场景还是要手工确认一下,用fdisk -l看分区起始扇区是否对齐到8的倍数即可。
4.2 SSD的FTL层:逻辑块到物理页的映射是怎么做的
在机械盘上,逻辑块号(LBA)和物理位置的关系比较固定,基本可以按数学模型推算。但在SSD上,LBA和物理Page之间没有一个固定公式——因为闪存需要擦除后才能写,数据会被到处搬移。处理这个问题的就是“闪存转换层”(Flash Translation Layer,FTL)。
FTL是SSD主控里的固件逻辑,它维护一张映射表:逻辑块地址→物理闪存页地址。当主机写入一个逻辑页时,FTL会找一个已经擦除好的空白页来放数据,同时更新映射表。当某块需要垃圾回收时,FTL要把有效页的数据搬移到新块,再更新映射表。
这也是为什么SSD容量会被“吃掉”一部分——映射表本身要占DRAM或者专门的存储区域,映射表越大,元数据开销越高。如果用随机写模式,映射表还会频繁变化,DRAM的带宽和处理压力也随之上来。
从架构视角来看,FTL的存在让SSD成为一门“带翻译的存储设备”。上层不需要知道数据到底被搬到哪个物理Page了,只要告诉它“我写第N个逻辑块”,它负责在你的眼皮下完成所有抽屉的重新整理。
4.3 文件系统如何感知物理特性:discard/TRIM与预留空间
文件系统不光往存储设备上写数据,还要想办法让SSD活得久一点。Linux上的fstrim命令、文件系统挂载参数里的discard,都是来配合SSD的TRIM指令的。
TRIM指令的作用是:当文件系统删除一个文件时,它会告诉SSD“这些LBA对应的数据都不再需要了”。SSD拿到消息后,把相应的物理页标记为“无效”,这样等垃圾回收扫到这些块时,有价值的数据量少,搬移动作少,写放大自然降低。相反,如果不发TRIM,SSD根本不知道这些数据已经“死了”,垃圾回收时还要傻乎乎地把它们搬到新块,白白消耗寿命和性能。
我之前遇到过一台服务器,长期不做trim,SSD的写入寿命和性能肉眼可见地往下掉,加上监控数据是删除频繁的业务,写放大明显偏高。后来在crontab里加了定期fstrim -av,情况好转不少。
此外,文件系统还可能考虑“预留空间”。企业级SSD普遍出厂就有10%~28%的OP空间,但如果你用的是消费级SSD做某些存储节点,也可以考虑在分区时故意“少分一点”,比如把标称480G的盘分区成400G来用,多出来的空间留给主控做GC缓冲,实际写性能往往比“满容量使用”更稳定。
5. I/O路径上的性能瓶颈与实测优化
5.1 从应用到磁盘的完整I/O链路:每一层都可能在拖后腿
有了前面的基础,现在我们能完整画出一次IO在“核心体系架构”里走的路。它就像一条物流运输链,任何一个环节堵塞,整条链路的“交付时间”都会变差。
- 应用层:业务代码发起读/写请求;
- 系统调用层:陷入内核态,进行参数检查和复制数据;
- 页缓存/缓冲层:读请求先看页缓存有没有命中,命中就直接返回,完全不用触盘;写请求先写缓存,后续由pdflush后台任务刷盘;
- 文件系统层:解析文件路径,管理inode和块分配,把文件偏移换算成块设备偏移;
- 通用块层:生成
bio请求,往上屏蔽不同设备差异; - IO调度器:对请求做合并和排序,影响的是先处理哪些、后处理哪些;
- 设备驱动:翻译指令,和硬件交互;
- 硬件设备:机械盘或SSD最终完成物理IO。
每一层都有自己擅长的“降速”方式。比如页缓存命中率低,你就得频繁触盘;IO调度器适合机械盘的排序,在NVMe SSD上反而可能成为瓶颈;设备本身如果是SATA接口,带宽也被协议限制在6Gbps(实际约550MB/s)左右。
做性能分析时,我建议从最靠近硬件的工具看起,iostat -x 1能看到r/s、w/s、await、%util等指标;再用blktrace查看块层请求分布;最后用perf看内核热点。很多时候问题并不在设备,而在上面的不合理的IO模式。
5.2 队列深度、随机读写与顺序读写的实测差异
我有一块SATA SSD和一块7200转机械硬盘,简单用fio做过对比测试,结果可以作为参考:
| 测试项 | 机械硬盘 | SATA SSD |
|---|---|---|
| 顺序读 | 约180MB/s | 约550MB/s |
| 顺序写 | 约160MB/s | 约500MB/s |
| 随机读(4K) | 约1.2MB/s(约300 IOPS) | 约180MB/s(约45k IOPS) |
| 随机写(4K) | 约0.8MB/s | 约160MB/s(约40k IOPS) |
注意机械盘的随机读IOPS算下来不到300,和前面手算的76 IOPS有差异,是因为企业级硬盘可能有提前缓存、命令合并等机制,但数量级基本吻合——百级。
随机写方面,SSD虽然看着很强,但如果你用全盘写满后再持续随机写,会发现吞吐掉落——这就是写放大、垃圾回收和预留空间在起作用。这也是为什么测SSD性能不能只测空盘,要测稳态性能(Steady State)才更有参考价值。
另一个关键参数是队列深度(Queue Depth)。机械硬盘时代,一次只能服务一个命令,队列深度再高也没用;NVMe SSD支持多队列、每队列深度可到65535,这时候高队列深度才能把SSD的多通道、多Die并行能力“喂饱”。所以用fio时别固定iodepth=1,建议跑几个深度梯度,就能看到SSD从低并发到高并发性能的爬坡曲线。
5.3 针对物理特性的实践优化建议
复盘一下,当你了解了物理存储的工作原理和IO栈的每一层行为,你就有了一套判断依据,能根据业务形态、硬件型号、IO模式做合理优化。
- 顺序写优先:如果业务允许,尽量把随机写转化为顺序写。典型做法是日志结构合并树(LSM-Tree)的设计思想,数据先顺序写入WAL,再异步整理;机械盘和SSD都吃这一套,只是收益机制不同。
- 读多写少场景:加大页缓存,调高预读(如
blockdev --setra),用内存换磁盘IO。 - 减少不必要的写放大:SSD上尽量保持足够剩余空间,定期TRIM,避免极端满盘运行。
- 选择合适的文件系统挂载参数:比如
noatime减少访问时间更新带来的写IO;discard看场景选fstrim定时执行,避免在线discard影响性能。 - 利用并行性:给NVMe SSD配置多个硬件队列,同时在应用层用多线程/多进程发出IO,别让请求都挤在一个命令队列里排队。
- 对齐和块大小:文件系统块大小与底层物理页/扇区对齐,可以查一下
mkfs.ext4 -b和stripe_width等参数,针对RAID阵列或SSD做对齐。
这些优化看着零散,但原理上都指向同一个方向:让你发给存储设备的请求,符合它物理动作的“偏好”。机械盘喜欢顺序、讨厌寻道;SSD喜欢批量、讨厌频繁小块随机写;NVMe喜欢高并发队列,但也不能让队列堆积到失控。摸清你家设备的性格,再顺着性格去调。
6. 物理层之外:如何用监控与实测校验你的认知
6.1 从iostat到fio:常用工具怎么读才准确
理论讲了一堆,最终都要落到实测量化上。Linux老牌工具iostat -x 1是我最常用的入门项:
r/s/w/s:每秒读/写请求数;rkB/s/wkB/s:每秒读/写字节数;await:请求处理等待时间(含队列等待+设备处理);svctm:设备自身服务时间(现代内核已经不更新这个字段了,部分版本看服务时间用aqu-sz等);%util:设备忙的百分比,但注意这个指标对SSD并不完全可靠,因为SSD是多通道并行,可能一个队列空但另一个还在忙。
fio是压测利器,建议先测“纯净”的性能基线:fio -name=randread -rw=randread -bs=4k -size=8G -iodepth=32 -direct=1 -numjobs=1,来看随机读稳态。然后改-rw=randwrite、-bs=128k等参数,交叉对比不同块大小、不同读写比例下的表现。记录时要分顺序/随机、读/写、单队列/多队列几个维度,之后再做调优才有对照。
6.2 实测中容易踩的几个认知陷阱
- “SSD不需要关注队列深度”:错,低队列深度下NVMe盘只能发挥很小比例性能,尤其在多核场景下,单队列会把所有请求串行化。
- “4K对齐只对机械盘重要”:错,SSD的Page也是4K或更大粒度,逻辑块跨页一样会有额外开销。
- “盘写入量越少就越省寿命”:这个话只对一半,不管业务写入多少,垃圾回收本身也在消耗擦写次数,关键看写放大系数,而不是只看主机写入量。
- “满盘用的SSD没问题”:前面说了,预留空间被压缩后,GC频繁,性能会陡降。挂片盘保留20%以上空闲是相对稳的做法。
这些坑我基本都踩过。有一段时间线上数据库实例的延迟曲线像毛毛虫一样,找了半天发现是消费级SSD在满磁盘状态下跑随机写,最后把容量降一点+定期TRIM,才把尾巴收回来。
6.3 从“物理存储认知”到“体系架构优化”的迁移
学完物理存储这块,最大的收益不是会背几个名词,而是你在做上层架构决策时,多了一把“物理尺子”。比如:
- 设计对象存储存储池时,你是用机械盘做冷数据层,还是用QLC SSD做温数据层?要按成本和性能模型权衡;
- 数据库的
innodb_io_capacity该设多少?要用SSD的稳态随机IOPS来估; - 消息队列的Page Cache写缓冲设多大?要结合磁盘吞吐和刷盘频率算;
- 分布式存储的副本放置策略,要不要考虑跨节点的寻道和争抢?物理层行为会影响你选的机架拓扑。
这些决策没有一个能脱离底层物理特性凭空拍脑袋。体系架构的“体系”二字,就是要求你把从应用到硬件每一层的成本和能力看成一个整体,而不是各调各的。物理存储是这串链路里最“硬”的一环,它不会因为你用软件技巧变聪明就改变物理极限,但理解它的脾气,你就能在上层做最优适应。
人这一路下来,说真的,很多“性能优化”最后拼的不是奇技淫巧,而是对底层物理机制的尊重。把这个地基打扎实了,后面的章节学起来会顺很多。
