Linux服务器读写性能调优实战:从基准测试到内核参数
发布时间:2026/9/7 16:03:48 作者:尧图编辑部 阅读量:1,286

机器变慢了这件事十次里有七八次是被存储读写拖住的。做“机器调优”最常碰到的情况是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 # 如果看到 waI/O wait经常偏高说明大量进程在等 IO 完成 # 实时查看每块磁盘的 IO 情况间隔 1 秒刷新 iostat -x 1 # 找到是哪些进程在打磁盘 iotop -o -Piostat 输出里值得重点看的是这几个字段%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 测试时一定要加 --direct1绕过 page cache让 IO 请求直接下发到存储设备。同时配合 --ioenginelibaio 使用异步 IO这更贴近真实高并发应用的请求模型。# 随机读4K 块大小队列深度 64测试 60 秒 fio --namerandread --filename/data/fio_test --rwrandread \ --bs4k --iodepth64 --direct1 --ioenginelibaio \ --numjobs1 --runtime60 --time_based --group_reporting # 随机写4K 块大小队列深度 64 fio --namerandwrite --filename/data/fio_test --rwrandwrite \ --bs4k --iodepth64 --direct1 --ioenginelibaio \ --numjobs1 --runtime60 --time_based --group_reporting # 顺序读1M 块大小 fio --nameseqread --filename/data/fio_test --rwread \ --bs1m --iodepth32 --direct1 --ioenginelibaio \ --numjobs1 --runtime60 --time_based --group_reporting # 顺序写1M 块大小 fio --nameseqwrite --filename/data/fio_test --rwwrite \ --bs1m --iodepth32 --direct1 --ioenginelibaio \ --numjobs1 --runtime60 --time_based --group_reporting测试完之后记得清理测试文件这种大文件留在线上磁盘里本身就是一种空间浪费和额外的 GC 负担。2.2 从测试结果反推业务场景我用一块普通 SATA SSD 举例说明怎么解读 FIO 输出。一般券商的 OLTP 数据库服务器使用的企业级 SSD4K 随机读 IOPS 可能在一两万到十万不等顺序读带宽能到 500MB/s 以上如果是 NVMe SSD4K 随机读 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_ratio5 sysctl -w vm.dirty_ratio15要永久生效的话写到 /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~2000NVMe 盘可以设置 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 或吞吐极限。可能你为了让顺序写带宽跑得更高付出的代价是随机读延迟恶化而后者恰恰才是业务真正需要的东西。所以动手前先定义好“这台机器需要好在哪里”比任何参数都重要。另外调优线上生产环境前一定要准备好回滚方案。凡是能热生效的内核参数调整后要立即观察一段时间凡是需要重启、涉及挂载选项变更的操作最好先在测试环境验证一遍再上生产。变更窗口尽量选在业务低峰期并且保证数据有完整备份。这套习惯救过我很多次现在也成了我带团队时最强调的底线。