Linux服务器读写性能调优实战:从基准测试到内核参数
2026/9/7 16:03:33 网站建设 项目流程

机器变慢了这件事,十次里有七八次是被存储读写拖住的。做“机器调优”最常碰到的情况是:CPU 占用不高、内存也没满,但应用就是卡,慢 SQL 一个接一个,页面转圈,日志写入像挤牙膏。这时候性能问题的焦点大概率就在读写性能上。这篇内容不绕弯子,直接把我在服务器读写性能调优这条路上用过的工具、踩过的坑、验证过的参数、以及一套可以照着做的排查流程整理出来,希望能帮大家少走弯路。

先把调优对象说清楚。这里的读写性能,指的是操作系统和存储子系统处理数据读写的效率,包含磁盘设备的吞吐能力、随机读写 IOPS、单次 IO 的延迟、文件系统与内核块设备层的处理效率,以及上层数据库或者应用对 IO 的利用方式。很多人在这一层绕了很久,调了半天 sysctl 发现没效果,原因是问题根本不在参数,而在对瓶颈的判断从一开始就错了。

1. 调优前先定位:这台机器的读写瓶颈到底在哪

1.1 吞吐、IOPS、延迟,三者不能混为一谈

聊读写性能,得先把三个基础指标分清楚。吞吐量(Throughput)是单位时间能搬多少数据,单位通常是 MB/s,适合衡量大文件顺序读写。IOPS 是每秒能处理多少个 IO 请求,适合衡量大量小文件随机读写。延迟(Latency)则是单个请求从发出到返回的时间,通常以毫秒或微秒为单位。很多人习惯只盯着某一块测,但这三个指标互相制约,同一块硬盘顺序读可能轻松跑到几百 MB/s,随机小文件读写却可能只有几十 IOPS,两者对调优方向的要求完全不同。

举个容易理解的说法:吞吐像一条高速公路能同时过多少辆车,IOPS 像收费站能处理多少辆车的缴费,延迟则是最前面那辆车从到站到通过花了多久。你要优化的是“大车队平稳通过”还是“小车流快速通行”,方案思路天差地别。平时调优如果拿顺序读的带宽去评估一个小文件随机写为主的系统,等于拿货车速度去评价市区通勤体验,没有任何参考意义。

所以我在真正动手之前,一定会先明确当前机器的负载类型。是日志型追加写,还是数据库事务型随机写?是视频转码那种连续大块读,还是搜索引擎倒排索引那种小块随机读?只有搞清楚了主要矛盾,后面读出来的测试数据才能指导调优方向。

1.2 先用这组命令定位是不是磁盘拖后腿

调优的第一步不是改参数,是判断“慢”到底发生在哪一环。我习惯用一个很朴素的顺序:先看 CPU 和负载,再看内存,最后把目光放到 IO 上,用 iostat 和 iotop 这类工具缩小范围。

# 查看系统整体负载和 CPU 等待 IO 的比例 top # 如果看到 wa(I/O wait)经常偏高,说明大量进程在等 IO 完成 # 实时查看每块磁盘的 IO 情况,间隔 1 秒刷新 iostat -x 1 # 找到是哪些进程在打磁盘 iotop -o -P

iostat 输出里值得重点看的是这几个字段:%util 表示设备忙的时间比例;await 表示 IO 请求平均等待时间,包含排队和服务时间;r_await、w_await 分别是读、写各自的等待;aqu-sz 是平均队列长度。很多人看到 %util 接近 100% 就认为磁盘满了,其实这需要分情况讨论。

%util 对于机械盘比较接近真实繁忙度,但对 SSD 和 NVMe,因为支持并行处理大量请求,即使 %util 已经成为 100%,设备可能仍然有余量,IOPS 还在增长。真正判定瓶颈时我更看重 await 和队列深度的组合:如果读写请求的延迟在明显飙升,同时队列长度持续高居不下,说明设备端确实吃满了。反之如果 %util 高但延迟稳定、没有积压,那只是说明设备在高效地持续干活,未必到了性能极限。

还有一个小细节:iostat 默认的采样周期对偶发尖刺不敏感。比如数据库每秒有一个较大的刷盘动作,你用 1 秒间隔可能看到的平均等待并不高,但实际那一瞬间的延迟非常高。所以我通常会把间隔设小一点,例如 0.5 秒,连续观察几分钟,留意有没有周期性延迟尖峰;或者直接用 pidstat -d 1 看单个进程的 IO 情况,定位是哪个应用在制造问题。

2. 用基准测试给磁盘“体检”:读写性能数值怎么读

2.1 用 FIO 给底层磁盘做一轮摸底

判断系统的读写性能到底处于什么水平,与其靠“感觉”,不如直接用工具压一轮。FIO 是这类测试里最常用的工具,开源免费,语法清晰,可以用来模拟几乎任何 IO 模式。

需要先强调一点:基准测试必须绕过文件系统缓存,否则读的是内存缓存,测的就不是磁盘了。所以 FIO 测试时一定要加 --direct=1,绕过 page cache,让 IO 请求直接下发到存储设备。同时配合 --ioengine=libaio 使用异步 IO,这更贴近真实高并发应用的请求模型。

# 随机读,4K 块大小,队列深度 64,测试 60 秒 fio --name=randread --filename=/data/fio_test --rw=randread \ --bs=4k --iodepth=64 --direct=1 --ioengine=libaio \ --numjobs=1 --runtime=60 --time_based --group_reporting # 随机写,4K 块大小,队列深度 64 fio --name=randwrite --filename=/data/fio_test --rw=randwrite \ --bs=4k --iodepth=64 --direct=1 --ioengine=libaio \ --numjobs=1 --runtime=60 --time_based --group_reporting # 顺序读,1M 块大小 fio --name=seqread --filename=/data/fio_test --rw=read \ --bs=1m --iodepth=32 --direct=1 --ioengine=libaio \ --numjobs=1 --runtime=60 --time_based --group_reporting # 顺序写,1M 块大小 fio --name=seqwrite --filename=/data/fio_test --rw=write \ --bs=1m --iodepth=32 --direct=1 --ioengine=libaio \ --numjobs=1 --runtime=60 --time_based --group_reporting

测试完之后记得清理测试文件,这种大文件留在线上磁盘里本身就是一种空间浪费和额外的 GC 负担。

2.2 从测试结果反推业务场景

我用一块普通 SATA SSD 举例说明怎么解读 FIO 输出。一般券商的 OLTP 数据库服务器使用的企业级 SSD,4K 随机读 IOPS 可能在一两万到十万不等,顺序读带宽能到 500MB/s 以上;如果是 NVMe SSD,4K 随机读 IOPS 可以轻松冲破几十万,顺序吞吐则奔着 GB/s 去。而一块 7200 转的机械盘,4K 随机读可能只有几百 IOPS,顺序读写也就 150~200MB/s 左右。两者数量级的差异,直接决定了你的系统架构选型——如果业务是随机小 IO 为主,机械盘根本扛不住,参数再怎么调也只是在矮子里拔高个。

下面是我常用的一组关键测试组合,作为一个比较完整的定位矩阵:

测试目的块大小IO 模式队列深度关注结果
模拟 OLTP 小事务随机读写4K / 8Krandread / randwrite16~64随机 IOPS、时延 P99
模拟日志顺序追加写4K~64Kwrite16~64写带宽、写延迟
模拟大文件扫描/报表查询1Mread16~32顺序读吞吐
模拟高并发小文件读4Krandread128IOPS 上限与延迟拐点

光测出数值还不够,要结合业务环境去判断合理范围。线上数据库一般还会配合压测工具(例如 sysbench 或 HammerDB)做整体吞吐验证,FIO 这类底层测试只负责确认“地基”有没有问题。如果 FIO 测出来底层磁盘本身没问题,业务还是慢,那问题基本就不在存储硬件层,而在文件系统、内核参数或者应用自身的使用方式了,这也正好引到下一层的调优动作。

3. 文件系统与内核参数:影响读写性能的几个隐藏开关

3.1 IO 调度器该不该换,要看场景

Linux 内核里的 IO 调度器,负责决定块设备层的请求以什么顺序来处理。过去常用的调度器有 noop、deadline、cfq,新内核里 cfq 逐步退出历史舞台,比较常见的是 none、mq-deadline 和 bfq 这些多队列调度器。每个调度器的思路不一样,noop/none 基本不排序,请求来了就发下去;deadline 保证每个请求在截止时间之前得到处理,避免某个请求被饿死;cfq 按进程公平分配 IO 带宽,适合桌面环境避免单个进程抢占。

检查当前磁盘使用的调度器很容易:

cat /sys/block/sda/queue/scheduler

输出中括号括起来的那个就是当前生效的调度器。比如输出 [mq-deadline] none,说明现在使用的是 mq-deadline。

从实践角度看,这台机器如果用 NVMe SSD,我一般建议用 none,也就是不做调度,尽量把请求快速下发给设备,让 SSD 内部的 FTL 层自己处理排序与合并。如果用的是机械盘,尤其是存储系统里有大量顺序或者半顺序写请求,mq-deadline 通常更合适,它能减少磁头来回寻道的浪费。bfq 适合个人桌面环境或者多租户共享 IO 的场景,需要保证进程间的 IO 公平性,但对追求极限吞吐的服务器来说并不占优。

调度器配置要写进 udev 规则或者内核引导参数才能持久化,否则重启以后 udev 会根据设备类型自动设置一个新的默认值。我自己就踩过这个坑:调整完调度器之后明明测试结果不错,结果一重启设备,调度器又被 udev 的默认规则给覆盖回去了。正确做法是把规则写到 /etc/udev/rules.d/ 目录下,比如 60-io-scheduler.rules,让设备创建时自动应用你的设定。

3.2 脏页回写参数:写性能的幕后总开关

Linux 用 page cache 缓存磁盘内容,写入操作先落内存,再异步写回磁盘,这部分尚未写回磁盘的缓存页就是脏页。vm.dirty_ratio 和 vm.dirty_background_ratio 这两个内核参数决定了脏页允许占内存的比例,直接决定了写 IO 是平滑地落盘还是瞬间猛刷一波。

举个例子。默认配置下,如果系统有 64GB 内存,dirty_background_ratio 默认约 10%,意味着脏页到了约 6.4GB 就会后台开始回写;dirty_ratio 默认 20%,到了约 12.8GB 时,再发起写入的进程会被强制同步等待脏页落盘。这会造成一个现象:平时写入很平滑,突然某一刻写入延迟飙升,应用卡顿明显,因为后台回写没能及时跟上,最终触发了前台进程的同步写等待。

调优时我不会盲目把 dirty_background_ratio 调高,也不会建议无脑调低。如果业务有大量短暂的写入突变,例如日志采集代理批量写缓存,较高的 background 阈值反而可以让写入集中在内存中一次性刷出,提高吞吐并减少磁盘寻道次数。反过来,如果业务是延迟敏感型的小事务提交,比如数据库线上交易系统,我倾向于把 dirty_background_ratio 调低一些,让脏页更早开始回写,避免积累过大以后出现明显的周期性抖动。

# 查看当前脏页参数 sysctl vm.dirty_ratio vm.dirty_background_ratio # 临时调整,例如内存大的写缓存型服务器 sysctl -w vm.dirty_background_ratio=5 sysctl -w vm.dirty_ratio=15

要永久生效的话,写到 /etc/sysctl.conf,或者更规范一点,放到 /etc/sysctl.d/99-io-tuning.conf 文件里。这个参数的调整要特别留意应用的数据安全要求:脏页允许的比例越高,意味着异常掉电时可能丢失的未写盘数据窗口越大。生产环境如果对数据安全极其敏感,要把值调得保守一点;如果只是缓存类数据、丢一点能接受,才可以把阈值放宽去换写入吞吐。

3.3 挂载选项和文件系统的取舍

读写性能调优很容易忽略文件系统层。简单来说,不同的文件系统有不同的数据布局与日志机制,对读写的表现差异很明显。

ext4 是经典老将,兼容性好,维护工具丰富;xfs 则在大文件、高并发场景下表现更稳,经常成为数据库等重 IO 应用的默认选择。如果你建的是一个全新的存储目录,且没有特殊兼容需求,我一般会优先推荐 xfs。它在并发写入上的锁竞争更小,动态 inode 分配也省去了 ext4 预分配 inode 的空间浪费问题。当然 ext4 本身并不差,只要用对场景,两者在吞吐数据上未必有毁灭性差距。

挂载参数的影响也很大。noatime 可以减少每次文件访问时产生的元数据写入,这是最基础也最推荐的一项。如果没有程序依赖 atime(访问时间),建议直接开启 noatime,顺带解决“读取操作还会触发写 IO”的诡异问题。nodiratime 同样可以降低目录访问的写负载。

还有几个挂载参数容易引起误解。barrier 是日志文件系统为确保顺序性而启用的屏障,强行关闭可以在极端情况下提升吞吐,但日志文件系统的一致性保障会变弱,系统异常断电时文件损坏的概率会提高,这个我不建议随意关闭。SSD 的 discard 挂载参数启用后,每次文件删除都会向 SSD 发起 TRIM 指令,帮助 SSD 回收无用块,但也会带来额外的命令开销。如果担心性能和寿命平衡问题,可以不用挂载 discard,而是定期用 fstrim 做批量回收。这在大多数场景下更友好。

块设备层的对齐问题也要注意。老式机械盘分区时容易遇到起始扇区没有对齐到 4KB 边界的问题,对 4K 扇区盘和 SSD 来说,未对齐意味着一次 IO 被拆成两次物理写入,性能折损极其明显。新系统安装时默认会对齐,但如果是从老机器克隆或迁移过来的分区,最好检查一下起始扇区。

4. 存储场景化调优:拿数据库读写场景举例

4.1 为什么数据库缓存永远比磁盘更快:读优化思路

读写性能调优放到数据库这类具体应用上,会有更明确的优化路线。拿最常见的 MySQL 来剖析,读性能的优化核心在于减少真正到达磁盘的读请求。

MySQL 的 InnoDB 引擎把数据按 16KB 页面组织,有自己的 buffer pool 缓存热数据页。理论上如果 buffer pool 足够大,所有热数据都能留在内存里,走主键查询甚至可能完全不需要磁盘读,这时磁盘读 IOPS 接近 0。听起来很理想,但现实是有些机器内存不够大,或者冷数据量大,缓存命中率低,直接把压力交到了磁盘上。

判断数据库的缓存命中情况很简单:

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; -- 命中率 = read_requests / (read_requests + reads) * 100%

我曾经遇到过一台 MySQL 服务器,慢查询频繁出现,查看磁盘 iostat 发现读延迟很高,但是 CPU 和内存看着都还“有余量”。后来查询状态发现 buffer pool 命中率只有 90% 左右,这对一套以读为主的业务来说意味着大约 10% 的读请求要落到磁盘。问题的根源其实不是底层磁盘不够好,而是内存缓存太小。把 innodb_buffer_pool_size 从 4GB 提高到 16GB,命中率升到 99% 以上之后,磁盘读 IOPS 直接下降了 90%,用户侧响应速度立刻改善。

这说明一个调优的基本原则:在给硬件扩容、调内核参数之前,先检查应用自身的缓存策略是否合理。缓存命中率不高的情况下,再聪明的内核参数也挡不住大量真实磁盘读请求,内存永远比任何存储介质都快好几个数量级。

4.2 数据库落盘参数对“写抖动”的影响

写性能调优和读不完全一样。数据库写操作要保证事务持久性,不能只写内存就返回成功,必须要落盘才放心。问题在于落盘的方式和频率直接影响整体性能。

MySQL 的 innodb_flush_method 是最常见的调优项之一。默认(或者 fsync 模式)下,数据写入会经过文件系统缓存,虽然看起来更快,但增加了数据在缓存到真正落盘之间的一层不确定性和双写问题;而使用 O_DIRECT 模式,数据直接绕过文件系统缓存写入磁盘,虽然单次写延迟略高,但减少了系统缓存层面的数据复制以及缓存回收压力,能让 InnoDB 自己管理的数据页刷盘在整体上更稳定。对于业务量大、刷盘频繁的线上 MySQL,多数实践认为 O_DIRECT 更稳。

另一个容易被忽略的是 innodb_io_capacity。它告诉 InnoDB 这台机器的磁盘能承受多少 IOPS,控制后台刷脏页的速率。如果值设置得过低,后台刷新跟不上,脏页比例上涨,最终会触发用户线程去帮忙刷脏页,导致写性能突然暴跌;设置得过高,后台刷新过于激进,可能白白占用磁盘带宽。

结合实际经验,SATA 企业级 SSD 可以设置 1000~2000,NVMe 盘可以设置 2000~5000 甚至更高;机械盘老老实实设置 200 左右。同时还可以把 innodb_io_capacity_max 设成 io_capacity 的 2~4 倍,允许压力尖峰时短时间超过额定值。

这些参数修改完一定要配合实际负载观测,不能凭感觉调。我建议用下面的思路做验证:记录同一时间段内脏页比例以及磁盘写延迟的变化情况,看刷盘节奏是否平滑。如果出现周期性写延迟尖峰,要检查是不是后台刷盘速率跟不上写入速率,及时调整 io_capacity 或脏页比例。

4.3 把测试数据拿回来:让参数调整可量化

调整配置前后,没有数据对比的“优化”都是耍流氓。一套可以复用的验证思路,会保证你的判断有理有据。

我在实际调优时会把流程拆成三步:

  • 第一步,用监控工具采集业务指标的基线数据,包括 CPU、内存、磁盘 util、await、进程 IO 等。
  • 第二步,针对业务压测或者观察一段实际流量,记录核心指标,比如数据库活跃会话数、慢查询数量、事务写入时延等。
  • 第三步,应用要修改的参数,重启相关服务或者等待参数热生效,再做同样维度的压测观察,对比前后差异。

一次调优只动一个变量,一次只改一个参数。如果同时改五个参数,将来出了问题根本不知道是哪一个引起的。我把每台机器的原始参数都会截图保留,调整时记录时间点,备注调整的目的是什么。这样即使调优之后效果不理想,回滚也是非常方便的一件事。

5. 读写性能故障排查与避坑实录

5.1 一次典型写入抖动排查过程

记录一次非常典型的线上故障排查过程。现象是某个跑批量任务的服务器,日常写入带宽较平稳,但每隔一段时间就会出现一次明显的写入停顿,持续时间约 20~30 秒,期间应用日志大量堆积,监控上能看到磁盘吞吐几乎归零。

上机器以后我先用 iostat -x 1 观察,发现 %util 并非持续饱和,而是偶发达到 100%,await 在尖峰时刻冲到几百毫秒,w_await 尤其高。再用 iotop 观察,发现周期性出现一个进程占用大量 IO,但不是业务进程本身,而是一个后台命令。再一看时间点,和脏页阈值触发的时机吻合。

排查系统参数时发现这台机器的 vm.dirty_background_ratio 被前人调高到了 20%,vm.dirty_ratio 则是默认值。在批量任务大量写入时,脏页不断累积,迟迟不触发后台回写,直到 dirty_ratio 撞线,所有写进程一起被阻塞等待回写,就形成了周期性的“憋大招”式暴跌。

解决办法是调低 background 阈值到 5%,适当地让回写从“憋到极限再写”变成“持续小批写”。之后磁盘写延迟曲线明显变得平滑,应用日志堆积的问题也随之消失了。这个案例的启发很直接:脏页回写参数对“间歇性大量写入”的场景影响巨大,调整前先想清楚业务是平滑写还是突刺写。

5.2 常见问题速查表

为了让排查过程更高效,我把平时经常遇到的读写性能问题整理成了一张速查表,供大家参考:

表现可能原因排查工具常用对策
iostat 显示 w_await 高,%util 几乎跑满机械盘性能不足、队列堆积iostat -x、fio换 SSD、增大队列深度、错峰调度大批量任务
%util 不高但应用读延迟大缓存命中率低、存在大量随机读pidstat -d、数据库状态扩大缓存/缓冲池、优化索引减少回表
周期性写入停顿脏页阈值设置不合理sysctl 参数 + iostat 时间线调整 dirty_background_ratio、dirty_ratio
飞快的 NVMe 盘 IOPS 上不去IO 调度器不对、队列深度不足、块未对齐cat /sys/block/*/queue/scheduler使用 none 调度器,应用层调高 iodepth
文件删除后磁盘空间未释放且性能下降部分场景 TRIM/discard 未启用lsblk -D、fstrim -av定期 fstrim 代替挂载 discard
数据库提交时突然大幅写延迟刷盘策略、磁盘 fsync 频率高MySQL 状态变量 + iostat调整 flush_method、合理设置 redo log 容量

还需要提一个反向案例——不是每次调优都需要改内核参数。某次查看一台机器性能差,误以为是内核参数不合适,后来发现同一台物理机上有邻居虚拟机正在做全量数据恢复,共享存储带宽被吃光了,单机层面的参数调整根本无效。这提醒我排查时要看完整链路,而不只是当前主机内部。

5.3 容易被忽略的“隐形性能杀手”

聊几个平时不太容易意识到、但影响很大的点。

首先是文件系统碎片和空间使用率。对于机械盘来说,碎片严重会明显影响顺序读性能;文件系统使用率超过 90% 以后,即使不是完全满,性能也会因为分配策略而下降。数据库数据目录所在分区,一旦使用率超过 85%,我会开始规划扩容或清理,不能等到报警才处理。

其次是系统日志与监控代理带来的隐形写放大。rsyslog、auditd、各类 agent 如果配置不当,可能高频写入大量小文件或日志,虽然在 iotop 里单个进程看着占用不高,但累积起来对磁盘 IO 的消耗相当可观。最典型的是某些审计组件默认开启后,文件访问事件大量落盘,直接拖慢整体 IO。排查时可以用 pidstat -d 1 逐个进程观察,把“谁在写”看清。

最后是硬件 RAID 卡策略对读写性能的影响。如果服务器配备了 RAID 卡且开启了写缓存,理论上可以显著提升写入性能,但前提是 RAID 卡配有电池或者电容保护模块;如果掉电保护模块缺失,写缓存策略往往会被强制关闭或降级为直写模式,写性能会出现断崖式下降。遇到新机器性能不合预期时,一定要检查 RAID 卡的缓存策略和电池状态,这个细节太容易被忽视。通过 storcli 或厂商管理工具可以查看策略,例如在 LSI 卡上查看当前策略是 WriteBack 还是 WriteThrough。

6. 关于调优节奏的几点个人经验

实际操作中我越来越倾向于一个原则:先把监控数据采集全,再动参数。每次调完一个参数,都会记录下来调整前后的指标数据。口头说“感觉快了”“感觉稳了”都没有用,数据曲线自然说明问题。调整要有取舍,如果是数据库读写混布的场景,要以业务核心指标为准去优化,而不是盲目追求某一块磁盘的最大 IOPS 或吞吐极限。可能你为了让顺序写带宽跑得更高,付出的代价是随机读延迟恶化,而后者恰恰才是业务真正需要的东西。所以动手前先定义好“这台机器需要好在哪里”,比任何参数都重要。

另外,调优线上生产环境前一定要准备好回滚方案。凡是能热生效的内核参数,调整后要立即观察一段时间;凡是需要重启、涉及挂载选项变更的操作,最好先在测试环境验证一遍再上生产。变更窗口尽量选在业务低峰期,并且保证数据有完整备份。这套习惯救过我很多次,现在也成了我带团队时最强调的底线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询