关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

数据库慢先别急着加CPU:4K随机写、队列深度和fsync怎么判断磁盘到顶

发布时间:2026-09-21

CPU 闲着这笔账不对:请求在排队,但没人告诉你在排谁的队

订单高峰刚开始,监控上突然冒出一串报警:下单接口的平均响应时间从二十毫秒量级爬到了八百毫秒量级,慢请求占比一路走高,主从延迟从零点几秒涨到几十秒。值班第一反应通常是登机器看资源,然后就卡住了——CPU 使用率不到三成,iowait 只在个别核上有点波动,空闲内存还剩好几个 G,没有 swap 交换,连接数也没打满。

这时候最容易出的一句话是"是不是 CPU 不够了,加点核"。这句话几乎一定是错的。加完核,CPU 更闲了,延迟一点没降,因为请求根本不是在计算上排队,它们排在了往存储落盘的路上。CPU 空转恰恰是磁盘瓶颈的典型伴生现象:事务在等 fsync 返回,线程阻塞,线程不做算术,CPU 自然没活干。你要证明的不是"CPU 忙不忙",而是"请求有没有在某一层的队列里等着"。

这篇要解决的问题很具体:在没有专业存储团队、也没有 fancy 的可观测平台的情况下,怎么用几行内置命令把"磁盘到底到顶了没有"这件事判明白,以及判明白之后先改哪一处。我会先把最容易误读的指标拆掉,再给判据,然后讲那些看起来像磁盘问题、其实是被文件系统和刷盘策略放大的假瓶颈。

先摆结论,后面逐条拆:

%util 冲到 100% 不等于磁盘到顶。它统计的是"设备队列非空的时间占比",不是带宽占用率,对多队列设备和虚出来的云盘盘符,这个数字失真到几乎没有参考价值。

真正的排队信号藏在 await 和服务时间的比例里。await 是总耗时(含排队),服务时间是纯粹的介质操作时间,两者差出来的那一段就是排队时间。

深队列会掩盖延迟。队列越深,吞吐曲线越平滑,你看 IOPS 稳稳当当甚至还在涨,实际单次请求已经在几十毫秒外排队了。

平均值会骗人,要看 P99。大量命中缓存、被合并的请求会把平均值拉到很好看,业务体感却由尾巴上的那一小撮决定。

很多"磁盘慢"根本不是盘慢。文件系统日志、双写缓冲每秒一次 fsync 的事务配额,这些写放大换一块更快的盘不会消失。

一、%util 到 100% 到底在说什么:把统计口径拆开看

看指标之前,先把命令固定下来,全文都以这一条为准:

iostat -xmt 1 20

-x 是扩展统计,-m 用 MB 显示吞吐,-t 打时间戳,1 是采样间隔(秒),20 是次数。别用不带参数的 iostat,那种输出只有 CPU 和裸串号的简单统计,什么都判断不了。还有一件很朴素的事:等到出事那天再装 sysstat 就来不及了,平时就该开着 sar -d 采集,故障时才有"健康基线和故障现场"两份数据对比。

输出里那个 %util,官方文档的说法是可以粗略理解为"设备带宽利用率",但这句话只在"设备一次只能处理一个请求"的前提下成立。它的实际算法是拿 /proc/diskstats 的第 13 个字段(设备进行 I/O 的毫秒数)除以采样间隔(毫秒)。也就是说,它数的是"队列里有请求的时间",不是"介质忙不忙"

区别在哪?一块单队列的机械盘,一次一个命令,寻道加旋转,队列非空的那段时间介质基本都在动,这两个口径接近。但换到今天的设备上,前提就崩了:

NVMe 有多条硬件提交队列和完成队列,可以同时承载几十个请求在不同队列里并行推进;virtio-blk 这种虚拟化磁盘设备,请求提交到宿主又是一次映射;带缓存的硬件 RAID,逻辑盘只有在真要往介质落的时候才动。结果就是:只要有请求在队列里,%util 就往上走,很快就贴住 100%,而设备可能还有大量处理能力空着。这就是为什么很多 NVMe 机器在非常轻的负载下 %util 也能长期显示的很高,它压根不表示到顶。

云盘更麻烦一层。宿主机里看到的 sdX 是一块虚拟盘,你在 guest 里读到的 %util、aqu-sz,全部反映的是虚拟块设备这一层的排队,和后端真实物理盘的忙闲没有直接对应关系。租用这类实例时,很多厂商会在产品说明里给出 IOPS 和吞吐的配额口径,那个配额才是真正的约束边界,而它是 guest 内部的工具看不见的。所以别指望在云主机里用 %util 推断宿主的情况。

还有一种常见误读来自多层块设备的重复统计。做了 LVM 逻辑卷、mdadm 软 RAID、或者 dm-crypt 加密层的机器,iostat 里会同时出现 sdX 和 dm-N 两组设备。看物理层还是看逻辑层,结论能差出一倍:逻辑层的统计建立在物理层之上,你既不能简单相加,也不能只看其中一层。建议固定看数据库实际挂载点这一层的同时,也把底层 sdX 拉出来对照,把"读写请求有没有被条带化打散、有没有某一块底层盘特别忙"看清楚。

二、三个能用的判据:await 与服务时间的比例、队列深度、写延迟 P99

既然 %util 不可信,那就得换一组信号。下面这三条是能真正把"到顶"这件事判定出来的,而且互相印证,单看任意一条都容易误判。

判据一:await 比服务时间多出来的那一段,才是排队

await 是从请求下发给设备驱动开始,到它彻底完成回来为止的总时间,它天然包含了在队列里等待的那一整段。而"服务时间"特指设备真正处理这个请求的时间,也就是介质操作本身的耗时。

老一点的教程会让你看 iostat -x 里的 svctm 列当服务时间。这条路现在基本走不通了:sysstat 从 11.x 起就把 svctm 标记为不建议使用,并明确写了"不要再相信这个字段",因为它背后那套单队列估算模型在今天的多队列设备上算不准,经常出现比 await 还大的荒谬值。所以现实的做法是用别的手段拿到真正的服务时间,再去和 await 比:

fio -name=randwrite -ioengine=libaio -direct=1 -rw=randwrite -bs=4k -iodepth=1 -filename=/dev/sdX -runtime=60 -time_based -group_reporting

这条用 队列深度 1 去测,等于抹掉了排队的影响,输出里 lat (usec) 那一行就是这块盘在当前状态下的单请求服务时间量级。注意这是往fo上跑,会消耗容量并产生写脏数据,只能在维护窗口、或者在配置相同的备用盘/闲置分区上做,千万别拿它去怼生产数据盘。拿到这个值之后,再拿生产时段 iostat 的 r_await / w_await 去比:

比值接近 1,说明请求基本来了就被服务,没排队。比值上到几倍甚至十几倍,说明绝大部分时间花在等待上,这就是排队,排队才是"磁盘到顶"的正式定义

关于服务时间的绝对值,给一组行业常见量级作为参考坐标(经验值,随型号、容量、磨损程度、队列设置差异很大,不是本站实测):企业级机械盘 4K 随机写的单请求服务时间通常在毫秒量级,受寻道与旋转延迟支配;SATA 固态通常在几十到几百微秒量级;NVMe 固态在几十微秒到一百多微秒量级。拿这个坐标系去套,你至少能判断出自己那块盘的 await 是不是已经离谱。

判据二:队列深度怎么读,以及它为什么会掩盖延迟

iostat -x 的 aqu-sz 列(老版本叫 avgqu-sz)就是平均队列长度:采样瞬间在设备上正在服务的请求加上还在等待的请求的平均数量。这个数字有个很实用的搭档算式——利特尔法则(Little's Law,L = λ × W)

aqu-sz ≈ IOPS × await

举例说明(纯数学示例,非实测):某刻 IOPS 是 2000,await 是 10 毫秒,那么队列里平均就有 2000 × 0.01 = 20 个请求。这个换算能用来反向校验数据是否自洽:如果看到 await 很高而 IOPS 很低,算出来的队列深度却极小,那说明平均值被少数几个卡到几百毫秒的请求拉偏了,这时候要去看分布而不是平均值。

队列深度为什么重要?因为它是延迟的直接乘数。请求被排在第几个决定了它要等前面几个被处理完。吞吐量 = 队列深度 ÷ 延迟,这个关系决定了:当设备本身处理不了更多请求时,把并发(也就是往里塞请求的力度)加倍,吞吐几乎不变,延迟却近似翻倍。

这就是为什么"深队列会掩盖延迟"——你在监控上看到的是一条漂亮平稳甚至还在上涨的 IOPS 曲线,实际每个请求的等待时间正在线性膨胀。等到触发了应用层超时,曲线才会突然塌下去。所以看 IOPS 绝对值毫无意义,一定要和同时刻的 await 配对看。

具体可以从两层去读真实并发。设备层:

cat /sys/block/sdX/queue/nr_requests(每个请求队列能排多少个请求)

cat /sys/block/sdX/queue/scheduler(当前 I/O 调度器)

数据库层:以 MySQL/InnoDB 为例,innodb_io_capacity 与 innodb_io_capacity_max 直接决定了后台刷脏能用多大的 IOPS 预算去写;innodb_write_io_threads / innodb_read_io_threads 决定异步 I/O 的并发线程数;innodb_page_cleaners 决定刷脏并行度。如果 innodb_io_capacity 填的是一个远高于盘真实能力的值,后台线程会以盘处理不过来的速率灌 write,脏页永远刷不完,checkpoint 积压,最后某一刻突然触发剧烈刷脏峰值,表现就是周期性写入尖刺。这个参数设定错了,比盘慢本身更常见。

还得留意读被写饿死的情况。IOFQ 公平排队、ionice、cgroup blkio 限制都是在资源紧张时避免某类请求独占的手段。账务类库上,一次大批量对账跑批的写能轻易把在线交易的读打到几百毫秒

判据三:写延迟必须看分布,平均值在这里基本无用

假设一块盘在六十秒里处理了十万个请求,其中九万九千个是命中设备缓存、几十微秒就返回的,另外一千个因为撞上垃圾回收和擦除块回收卡到了几十毫秒。算出来的平均值可能非常好看,而业务侧体验是每六十秒就有一千次请求的用户体感是"卡了一下"。订单提交、支付回调、账务入账这类操作,用户对单次峰值的敏感度远高于对均值的敏感度。

所以要看 P99、P99.9,甚至最大值。取法:

用 fio 压测时,输出里的 lat (usec) 行会给出 min / max / mean / stddev,配合 lat_percentiles=1 参数会打出百分位(percentile)分布表,能明确看到 99.00、99.90、99.99 各档的延迟。

线上抓则用 eBPF 工具(BCC 工具包):

biolatency -D 10 30(按设备维度输出块设备延迟直方图,单位通常是毫秒,对数刻度)

biolatency -F 10 30(额外按读写方向拆分,读延迟与写延迟分开统计,对数据库特别有用)

看直方图形状比看一个数字有用得多。订单/账务类库写入抖动最常见的形状是"双峰":主峰在亚毫秒到几毫秒,另一个小峰在几十毫秒甚至上百毫秒。后一个小峰通常对应某种周期性动作——后台刷脏水位触顶、redo 日志组写满切换、binlog 轮转、SSD 的垃圾回收、或者某一批跑批任务集中写入。找到小峰的周期,基本就找到了原因。

补充一条针对主从延迟的提醒:主从延迟突然变大,第一嫌疑通常不是磁盘。从库回放的并行度、大事务、主库上的并发写入方式都可能造成。可以先把 binlog side 的堆积量(写进 relay log 的速度)与 applier side(回放完成的速度)分开看,判断延迟卡在传输还是回放;回放侧的并行 worker 数量、以及表上有没有主键(无主键表在行模式下回放会退化成全表扫描),影响往往比磁盘大得多。别一上来就怀疑 I/O。

三、4K 随机写和顺序写,差价不止一个数量级

这里说的 4K,指的是和 NAND 闪存页粒度、文件系统块大小大致相当的小块单元。它和数据库的关系是:InnoDB 默认页大小是 16K,落到文件系统上通常对应若干个 4K 块;而 redo/undo/二级索引的更新天然是散落在文件各处的小块写。只要写入没有对齐到最小单元、一次写不完一个完整单元,就会引发读改写,写的数据量比逻辑上想写的多。

给一组行业常见量级参考(经验值,非本站实测,不同型号差距极大):7200 转企业级机械盘的 4K 随机写通常在一百到几百 IOPS 量级,因为每次写都要付出寻道加旋转的代价,顺序写则能跑到一百多到两百 MB/s 量级;SATA 接口的固态,随机写常见在数万 IOPS 量级;走 PCIe 的 NVMe 固态,在足够深的队列下可以到十万甚至更高 IOPS 量级。看明白这组对比就知道,"这块盘顺序写很快"和"这块盘跑数据库很快"是两件完全不相干的事

再往下是 SSD 自身的写放大。闪存的最小写入单位是页、最小擦除单位是块(通常由上百个页组成),改一页往往要先把整个块读出来、改写、擦除、再写回,这就是写放大系数(WAF)的来源。盘越接近写满、剩余预留空间(OP)越少、垃圾回收越吃紧,WAF 越难看,延迟尾巴也就越长。这条直接引出一个很反常识但很实用的运维建议:数据盘不要用到八成五以上,留点空间给垃圾回收。这不是玄学,是闪存物理决定的。

数据库侧自己的写放大部分,主要是这几处:

双写缓冲(doublewrite buffer),InnoDB 为了防止部分写(partial page write)导致页损坏,会把脏页先写一份到双写区再落数据文件,等于这部分数据写两遍。原子写能力(atomic write)的设备或文件系统(比如支持 16K 原子写的配置)可以关闭它,这是实打实能省一半的量。

redo/undo/binlog 三套日志叠加,同一笔事务要在多个位置产生写入。

二级索引和 B+ 树页分裂,一次看似简单的插入可能触发多级页分裂,每级都是随机写。

相比之下,binlog 和 WAL 是顺序追加写,这是全部写路径里最便宜的一种。这也是为什么"能不能让变化的部分尽量落到顺序写"是优化的核心思路。

四、很多"磁盘慢"是刷盘策略造成的写放大,换盘治不好

这一节是全文最省钱的部分。因为这一节的结论是:你可能不用花钱。

fsync() 的语义只有一个——把这个文件的数据和元数据真正刷到持久介质上,调用返回后,即使整机掉电,数据也不该丢。数据库的事务持久性(ACID 里的 D)就是靠它在提交路径上反复调用换来的。

以 MySQL 为例,两个参数决定这笔开销:

innodb_flush_log_at_trx_commit:值为 1,每次事务提交都写 redo 并 fsync;值为 2,每次提交写 redo 但只写到操作系统页缓存,由后台每秒刷一次;值为 0,连写都不保证每次做,交给后台每秒一次。

sync_binlog:值为 1,每次事务提交都对 binlog 做一次 fsync;值为 N,攒够 N 次提交才 fsync 一次;值为 0,完全交给文件系统决定。

两个都设成 1 就是业内说的"双一",它是崩溃后绝不丢已提交事务、且主从绝对一致的配置。代价是每笔事务至少两次 fsync。在一块没有掉电保护缓存的机械盘或普通云盘上,这两次 fsync 能把写入吞吐压到极低水平,而且会把 await 直接拉到十几毫秒往上——因为它要等介质真正转完/落完。

但请先别急着关。关闭 fsync 的风险不是"性能会抖",而是"崩溃时会丢数据,而且你不知道丢了多少、丢的是哪几笔"。对订单和账务库,最坏的结果是主库崩了重启,binlog 里记录了事务、redo 里却没落地,主库回滚了一部分已返回成功的事务;如果从库已经同步走了这部分 binlog,主从数据就永久性地对不上,而且 difference 是静默的,可能几个月后对账才爆发。账务系统上,这种事故的代价远超多买几块盘的钱。

PostgreSQL 侧同理,synchronous_commit(on / remote_write / local / off)控制的是提交时 WAL 刷写到哪一步。在此基础上,commit_delay 配合 commit_siblings 可以延迟一点点让多个事务攒成一组一起刷(组提交),用极小的延迟代价换 batch。

降 fsync 成本的正规路子有这几条,按推荐度排:

组提交。MySQL 的 binlog_group_commit_sync_delay(微秒级等待)和 binlog_group_commit_sync_no_delay_count(攒够多少事务就不再等)配合,把大批小事务的 fsync 合并成一次。同样的持久性保证,fsync 次数可能降一到两个数量级。高并发小事务场景下这是最有效的一招,而且不牺牲任何安全性。

把 redo/binlog/WAL 挪到带掉电保护的低延迟设备上。这是经典的"日志与数据分开",前提是底层必须是两块不同的物理设备——同一块盘上划两个逻辑卷毫无意义。

文件系统层面的选择。ext4 默认 data=ordered 只保证元数据日志,日志还是会带来元数据的额外写入;改成 data=writeback 能少一次,但代价是崩溃后可能出现陈旧数据。PostgreSQL 社区更偏好 XFS(多年的实践共识),部分原因就是它在大量小文件和大文件场景下的成熟度。给 ext4 配一块独立的外部日志设备(挂载时指定 journal 设备)也是一条老而有效的路。

barrier 相关选项要非常谨慎。barrier 的作用是阻止 I/O 重排序越过日志提交点,保障崩溃一致性。nobarrier 只有在你确认底层设备有可靠的掉电保护(阵列卡的电池/超级电容、或企业级固态自带的电容)时才可以考虑,否则一次掉电就可能连文件系统都挂载不上。这一条宁保守。

还有一处容易被忽略的双重写:缓存层级重复。数据库自己有缓冲池,操作系统还有一层页缓存,同一份数据内存里放两份。innodb_flush_method 设成 O_DIRECT 可以绕过操作系统页缓存,既省内存又避免双重管理。但要留意变体 O_DIRECT_NO_FSYNC 在不同版本、不同文件系统组合下的行为差异并不完全一致,用它之前务必查对应版本的官方文档,别凭印象设。

五、RAID 卡和云盘缓存:要的是性能还是掉电不丢

硬件 RAID 卡上的两个模式必须分清:

WriteBack(回写):数据写进卡的缓存就立刻返回,卡负责后续落盘。延迟低、速度快,但掉电会丢缓存里还没落下去的那些,所以必须配 BBU 电池或者超级电容(CacheVault 之类)。

WriteThrough(透写):必须真正写到介质后才返回。安全,但 fsync 的延迟直接等于介质延迟,跑数据库体感会非常糟。

这里有个非常经典的现场故障形态:昨天还好好的,今天突然全面变慢,而且慢得毫无征兆。查到最后是 RAID 卡的电池老化、或进入了充放电校准周期,卡自动把缓存策略从 WriteBack 降级成 WriteThrough。保护逻辑没错,错的是没人盯着电池状态。定期查一下卡的健康与当前缓存策略(各家厂商的管理工具不同),能把这类'玄学变慢'消灭在发生之前。

云盘这边要搞清楚三件事:

一,fsync 到底刷到了哪一层。托管块的写入落到宿主、落到分布式副本、还是落到实际的持久层,语义不同,可用的持久性保证也不同。选型时这一点要问清楚,而不是默认等同于本地盘。

二,IOPS 和吞吐通常是配额制,而且经常与容量挂钩——买的盘越大,拿到的 IOPS 额度越高。只按容量、不按性能挑云盘,是云上数据库最常见的选型错误。还有一点容易踩:低 IOPS 配额的盘往往还带突发能力(burst),短时间内能超额,突发额度用完后会被压回基线,于是压测一切正常、跑到几分钟后性能断崖,这就是典型的突发桶耗尽。

三,带有本地 NVMe 的实例盘性能好、fsync 飞快,但它随实例的生命周期走,实例释放或被重建时数据一同消失(各厂商条款不同,以该产品说明为准)。本地 NVMe 适合放 redo、临时表、队列缓冲这类可重建的数据,账务主数据不要单独托付给它

六、确认之后按顺序改:先看这三张和三档在一起的表

把上面这些指标串起来,日常排查时可以直接照这张表往下走。表格里的阈值是工程上的经验区间、用来做判断起点,不是普适标准,更不是本站实测数据,请结合自己业务的健康基线使用。

指标 怎么看 什么数值值得怀疑 可能原因 下一步
%util iostat -xmt 1 的最后一列 长期贴住 100%,但吞吐和 IOPS 都不高 该字段统计的是队列非空时间占比,多队列设备与虚拟盘会失真;也可能是深队列把 io_ticks 撑满 不当判据使用,改用下面几行交叉验证;必要时核对厂商给出的 IOPS/吞吐配额
r_await / w_await iostat -xmt 1,读写分开看 写 await 明显高于同盘用队列深度 1 测出的服务时间,经验上超过几倍就要警觉 介质本身到顶、云盘突发额度耗尽、RAID 卡电池降级为透写、阵列降级重建中 队列深度 1 跑一次 fio 拿到服务时间基线做对比(维护窗口、非生产数据盘)
aqu-sz(旧版 avgqu-sz) iostat -xmt 1;可与 IOPS × await 互算校验 持续远大于设备能力对应的合理并发(经验上长期两位数以上且延迟同步上涨) 数据库侧灌写速率超过盘的处理能力;innodb_io_capacity 等参数设定过高 下调后台刷脏预算,限制大批量写入的 I/O 优先级,给在线读写留出余量
写延迟 P99 / P99.9 fio 配 lat_percentiles=1;线上用 biolatency -D / -F 出直方图 P99 与均值相差一个数量级以上,或出现毫秒到几十毫秒的第二峰 SSD 垃圾回收与写放大、磁盘接近写满、日志切换、周期性刷脏峰值 先看第二峰的周期是否对应某种后台动作;同时检查剩余空间是否过低
svctm(服务时间) iostat -xmt 1 中的一列,sysstat 11.x 起已标注不建议使用 出现大于 await、或与其他列不自洽的值 底层估算模型基于单队列假设,多队列设备上算不准 弃用该列,改用 fio 队列深度 1 或 eBPF 直方图获取真实服务时间
r/s、w/s 与 rMB/s、wMB/s iostat -xmt 1 IOPS 很高但吞吐很低,说明单次请求块很小 随机小写主导(redo、undo、索引页分裂);是正常负载形态还是异常要看业务 检查是否存在可合并的写入、是否有缺失索引导致回表放大读
底层 sdX 与逻辑 dm-N 对照 同一条 iostat 输出里两组设备一起看 其中一块底层盘的 await 明显高于同组其他盘 阵列成员盘性能不一致、个别盘亚健康、条带未均匀分布 查 SMART 与阵列状态,确认是否有降级重建;规划更换异常成员盘

七、确认之后具体改什么:从不要钱的一路排到要花钱的

第一步,改参数(零成本,先做)

把 innodb_io_capacity 调到与盘真实能力匹配的量级,别照抄默认值,更别想当然往大填。填太大等于允许后台以盘处理不过来的速率灌写,脏页永远追不上。innodb_io_capacity_max 作为突发上限,innodb_flush_neighbors 在固态上通常建议关闭(固态没有相邻页寻道的收益,反而多写了多余页)。PostgreSQL 侧对应的思路是检查 checkpoint 相关参数、WAL 相关缓冲与 autovacuum 的 I/O 限流。

把刷脏线程、异步读写线程的并发与实际存储能力对齐;不要用高倍的默认值去冲击本身能力就很有限的设备。

该 batch 的地方 batch:批量插入合并成更大的事务,能显著摊薄每次提交的 fsync 成本。代价是锁持有时间变长,需要按场景折。

第二步,改文件系统与挂载项(改动小,需重启或重挂)

选型上,开源社区多年实践里 PostgreSQL 偏向 XFS;MySQL 在 ext4 与 XFS 上都有大量部署,两者都可以,关键是别用默认参数裸奔。挂载项里值得关注的是日志与 barrier 相关的几项(前文已说明取舍);顺带把 noatime / nodiratime 加上,能白拿一笔元数据写入的减免——每次读文件都要更新 atime 是纯粹的浪费。

如果预期会有大量小文件删除、单套文件系统要承受大量小文件删除的压力,选 XFS 通常在元数据与大规模目录的表现上更稳。绝大多数参数在改动前后都要留下对比测度,不要同时改三处,那样你永远不知道是哪一处起了作用,甚至不知道有没有起作用。

第三步,改架构分层(这才是真正治本的地方)

把顺序追加写的那一段单独掏出来:把 redo / binlog / WAL 挪到单独的低延迟设备,前提已在前面强调过:必须是两块不同的物理设备。这一步在很多场景下能单独把 fsync 延迟降到原来的几分之一。

异步化写入路径:能放进消息队列的请求就别让它同步落到库里。账务类场景做不到完全异步,但可以把非核心的旁路逻辑(统计、通知、日志)全部搬出去,把同步路径上的写压到最小。

削峰:热点账户、热点商品的串行化写是另一个层面的问题(锁等待伪装成 I/O 慢),看清锁等待与 I/O 等待的差别,别混为一谈。

读扩展:把报表、查询、跑批挪到独立从库,别让跑批的写挤压在线交易的时间片。很多所谓的"磁盘瓶颈"其实是"一台机器什么都干"。

八、什么情况下答案是:就该换盘了

把参数、文件系统、架构三层都过了一遍,下面这几条若全部成立,那确实该掏钱:

其一,队列深度 1 的服务时间本身就处在机械盘量级(毫秒级),而业务需要的单请求延迟在亚毫秒级。这是介质物理属性决定的,任何参数都救不回来。

其二,确认是随机小写为主的业务形态,且当前盘是机械盘。这种负载下机械盘与固态之间是一条鸿沟,不是优化能填补的差距。

其三,在保留 fsync 语义(innodb_flush_log_at_trx_commit=1、sync_binlog=1)的前提下,单盘的 fsync 能力就是撑不住事务量。注意前提——用牺牲持久性换来的流畅不算解决问题。

其四,健康对比下来,云盘的 IOPS 配额本身已经到了限制,且突发额度已经耗尽。云上有时直接换个更高性能的盘档比调任何参数都有效,因为瓶颈就是厂商定的那个额度。

换什么?如果 I/O 4K 随机写能力强、掉电保护可靠是企业级固态的核心。选型时优先看是否与其它实例共享同一个 NVMe 设备——若共享同一硬件队列,还是会遇到邻居吵闹的问题(noisy neighbor)——以及是否提供足够的 IOPS 配额。

在具体产品上,一万网络深耕 IDC 19 年(成立于 2007 年),裸金属与物理机线的 E5-2620 32G/1T 规格 ¥999/月起、E5-2698v4×2 32G/1T 规格 ¥3999/月起(官网价,以官网实时报价为准),这一类无虚拟化开销的独占机型在 I/O 确定性上天然优于共享实例;其高配方案里也能见到多块大容量 NVMe 的整机配置。选之前先把前面的判据跑一遍,把真正的紧约束项(随机写能力、可用 IOPS、还是持久性语义)确定了再去对配置表,不然钱很容易花在不需要的地方。还有一项要提前确认:若要在 BIOS/阵列卡层面调整缓存策略,需要确认服务商是否提供对应的带外管理能力,这一项通常不是自己能在系统里改的。

磁盘瓶颈排查里最容易被问住的五个问题

iostat 里 %util 到 100% 就一定说明磁盘到顶了吗?

完全不是,这也是全文最想纠正的一条误解。这个字段由 /proc/diskstats 的第 13 个字段(设备进行 I/O 的毫秒数)除以采样间隔算出,它统计的是"队列非空的时间",不是"带宽被占满的时间"。单队列机械盘上这两个口径接近,所以早年的经验还能用;但 NVMe、virtio-blk、带缓存的硬件 RAID 都能同时处理多个请求,只要有请求在队列里,这个数字就噌噌上到 100%,此时介质可能还很闲。云盘上更糟,guest 里读到的是虚拟块设备一层的情况,和后端物理盘无关,实际约束是厂商给的那个 IOPS/吞吐配额。正确的用法是把它当作"这台设备最近确实在受理请求"的提示,真正的判据交给 await 与真实服务时间的对比、队列深度、以及 P99 分布三项。

4K 随机写到底多少 IOPS 算够?

没有任何一个放之四海皆准的数字,因为"够不够"取决于业务的事务模型,不取决于盘。给出的是行业常见量级参考坐标(经验值,非本站实测):机械盘 4K 随机写在一百到几百 IOPS 量级,SATA 固态在数万 IOPS 量级,NVMe 固态在深队列下可达十万以上 IOPS 量级。真正该做的是把估算做出来:统计业务高峰每秒的写入页数与日志写入量,换算成每秒需要的随机写 I/O 次数,再和设备的真实能力对比。别忘了把单位统一——一次 16K 的 InnoDB 页写在 4K 块的文件系统上可能对应四次设备 I/O,这个换算漏了,估出来会差好几倍。

把 fsync 关掉能不能提速?

能,而且效果往往立竿见影,因为提交路径上最贵的一步就是等介质真正落成。但这是一笔非常糟糕的交易:你换来的是吞吐,交出去的是崩溃一致性。一旦掉电或异常重启,主库可能回滚掉一批已经向客户端返回成功的事务,而从库如果已经拿走了这部分 binlog,主从就永久性地不一致了,而且这个不一致是静默的,可能要到几周后的对账才暴露。对订单与账务类业务,这个代价远远超过多加几块盘的钱,我的立场很明确:不值得。真正该走的是组提交这条合并路子(批量把很多次 fsync 并成一次),它不牺牲任何持久性保证,却能把 fsync 次数降一到两个数量级。另有一条中间选项是把日志放到带掉电保护的低延迟设备上,让 fsync 变便宜而不是变没有。

云盘的 IOPS 和物理 SSD 怎么比?

不能直接比数字,因为两者的约束模型完全不同。物理盘的约束是介质能力加总线带宽,你买到的是一个上限;云盘的约束是配额——厂商给你定了一个 IOPS 值和吞吐值,还会附带突发额度,额度用完就掉回基线。这就是为什么很多人在云上压测前两分钟一切美好,之后性能断崖式下跌。托管 API 中还应留意几点:一是 fsync 究竟保证到哪一层(落到宿主缓存、落到分布式副本、还是落到持久介质);二是你买的容量往往同时决定了 IOPS 额度,只按容量挑盘、不按性能挑盘,是云上数据库最常见的选型错误。判断方法不变:同样是看等待时间的构成,await 里绝大部分不是在介质上,而是在排队。

主从延迟大是不是一定是磁盘问题?

大概率不是,这是被冤枉次数最多的一个节点。真正正确的排查顺序应该是:先看延迟卡在哪一段——是 binlog 没能及时传到从库(网络、主库 dump 线程瓶颈),还是从库回放跟不上(applier 侧瓶颈)。回放侧的常见成因包括回放并行度不足、碰上了单个大事务(一个大事务只能由一个 worker 串行执行,其他 worker 只能干等)、以及表上没有主键(在 row 格式下,无主键表的每一行匹配都可能退化成一次全表扫描,回放速度能慢到令人崩溃)。相比之下,从库磁盘 I/O 到顶虽然确实会导致回放变慢,但它通常伴有明显的本地 await 升高,一查就知道。别一上来就给从库换盘,先把每秒产生的 binlog 量与每秒回放完的量分开看清楚。

加内存能不能缓解磁盘压力?

能,但要清楚它缓解的是读压力,不是写压力。更大的缓冲池意味着热数据更容易命中内存,物理读下降,随机读 I/O 显著减少,这一条非常有效,是我愿意优先推荐的手段之一。但写入这块,缓冲再大也改变不了一个事实:事务提交要落盘,checkpoint 要把脏页刷出去,脏页量只会因为缓冲更大而积得更多——缓冲池越大,一次剧烈刷脏造成的延迟尖峰反而可能越明显,因为要刷的量变大了。所以加内存的正确期待是"读 I/O 下降、写 I/O 形态可能需要重新调优",配一次 innodb_io_capacity 与刷脏线程数的复核。指望加内存治好写延迟抖动,多半会失望。

这些判断依据从哪来,边界在哪

本文涉及的判断方法来自操作系统与数据库的公开文档口径:iostat / /proc/diskstats 字段定义与 svctm 字段的弃用说明见 sysstat 官方文档;InnoDB 刷盘参数、组提交选项的外文说明见对应数据库版本的官方手册;各种 SSD 行为特性的描述来自公开的 NAND 基础原理。文中出现的所有数值,凡涉及 IOPS、延迟、服务时间,一律是行业常见量级或经验区间的表述,用于建立判断坐标,不是本站的实测数据,不同介质型号、配置与实际负载下差异极大,请勿直接套用到自己的设备上。涉及报价的部分,一万网络裸金属 E5-2620 32G/1T ¥999/月起、E5-2698v4×2 32G/1T ¥3999/月起为官网价,具体以签约时最新报价与合同为准。


上一篇:服务器疑似被入侵以后该按什么顺序处置:隔离、取证、重装和数据回溯

下一篇:厄瓜多尔云2核4G的硬盘反而更小:太平洋侧收款站点怎么找真正的紧约束