数据库IO打满,TPS止步800
凌晨3点,监控告警:核心订单库IO等待升至30%,TPS跌至正常值的60%。iostat -x 1 输出:%util 100%,r/s 2800,w/s 1800,avgqu-sz 32。磁盘是四块NVMe SSD做的RAID10,理论IOPS 10万,实际连零头都没到。
问题在哪?内核IO栈有7层,每一层都可能成为瓶颈。下面从排查到调优,全链路拆解。
IO栈层次与定位工具
Linux IO栈从用户到硬件:应用(RW) → glibc(缓冲) → syscall → VFS → 文件系统 → 块层(调度器) → SCSI/ATA驱动 → NVMe驱动 → 设备。每个环节都会引入开销。
定位工具:
iostat -x 1→ 看%util, r_await, w_await, avgqu-sziotop -oP→ 看哪个进程在写盘blktrace -d /dev/nvme0n1 -o - | blkparse -i -→ 追踪IO事件
现场数据:r_await 12ms,w_await 8ms,avgqu-sz 32。磁盘队列拥塞,说明块层调度器或驱动层过载。
方案对比:四层调优,哪层收益最大?
| 优化层次 | 操作 | 预期收益 | 风险 |
|---|---|---|---|
| IO调度器 | 从none/mq-deadline切换为kyber/bfq | 减少排队延迟,写吞吐提升20%~50% | 调度器不同,写放大不同 |
| blk-cgroup | 限制后台备份进程的IO权重 | 保证核心服务IOPS | 误伤正常进程 |
| 文件系统参数 | noatime,nodiratime, barrier=0(仅测试) | 减少元数据写,读性能提升10%~20% | 数据一致性风险 |
| Direct I/O | 应用绕过page cache,直接写盘 | 消除双缓冲,对数据库场景效果显著 | 必须块对齐,复杂度高 |
本次场景是MySQL混合读写,IO调度器+文件系统参数收益最直接,Direct I/O需改造应用(MySQL已用O_DIRECT)。
操作实现:一步步改
1. 当前IO调度器查看与切换
# 查看当前调度器(Linux 4.19+默认mq-deadline+kyber)
cat /sys/block/nvme0n1/queue/scheduler
# 输出: [mq-deadline] kyber none
# 临时切换到kyber(适合NVMe,低延迟)
echo kyber > /sys/block/nvme0n1/queue/scheduler
# 切换后iostat观察%util和avgqu-sz下降
2. 永久修改调度器(grub)
# 修改内核启动参数
sed -i 's/GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 elevator=kyber"/' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg
# 重启生效
3. blk-cgroup限制后台IO(docker中备份进程)
# 创建cgroup v1控制器(需挂载blkio)
mkdir -p /sys/fs/cgroup/blkio/backup
# 设置权重:核心服务1000,备份200(默认权重)
echo 200 > /sys/fs/cgroup/blkio/backup/blkio.weight
# 将备份进程PID加入
echo 1234 > /sys/fs/cgroup/blkio/backup/cgroup.procs
4. 文件系统挂载参数优化
# 以ext4为例,重新挂载数据目录
mount -o remount,noatime,nodiratime,barrier=0 /data
# 验证:
mount | grep /data
# 输出:/dev/mapper/vg-data on /data type ext4 (rw,noatime,nodiratime,barrier=0)
注意:barrier=0关闭写屏障,可能断电丢数据。仅用于测试,生产建议保留barrier=1或使用XFS。
5. 应用层Direct I/O(MySQL示例)
# my.cnf中启用O_DIRECT
[mysqld]
innodb_flush_method = O_DIRECT
# 注意:要求数据文件块对齐(通常4KB对齐)
压测脚本与数据
使用fio 3.35模拟MySQL随机读写负载:
# 随机读:4KB,混合读70%写30%,队列深度32
fio --name=readmixed --ioengine=libaio --rw=randrw --rwmixread=70 \
--bs=4k --direct=1 --iodepth=32 --numjobs=8 \
--size=10G --runtime=60 --group_reporting --time_based \
--directory=/data/fio-test --output-format=json > result.json
优化前后各自跑三次取平均值:
// 优化前结果(部分)
{
"read": {"iops": 6732, "lat_ns": {"mean": 4823012}},
"write": {"iops": 5134, "lat_ns": {"mean": 7162030}}
}
// 优化后(调度器+文件系统参数+Direct I/O)
{
"read": {"iops": 41203, "lat_ns": {"mean": 1543000}},
"write": {"iops": 33610, "lat_ns": {"mean": 2371000}}
}
效果数据与对比表
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 随机读IOPS | 6,732 | 41,203 | +512% |
| 随机写IOPS | 5,134 | 33,610 | +555% |
| 读平均延迟 | 4.8ms | 1.5ms | -68% |
| 写平均延迟 | 7.2ms | 2.4ms | -67% |
| IO等待时间 | 30% | 4% | 下降26个百分点 |
生产环境TPS恢复到2200,QPS提升3倍。IO不再是瓶颈。
原理深入:为什么收益这么大?
IO调度器进化
对于NVMe SSD,传统CFQ(完全公平队列)在队列深度高时产生大量上下文切换,mq-deadline按扇区排序减少寻道时间,但仍然是“先来先服务”变体。而kyber基于延迟直接控制队列深度,动态限制,对SSD友好。none完全旁路调度器,把控制权交给驱动,但可能引发写放大(并行写入太多)。我们选择kyber。
blk-cgroup做什么?
当cgroup内进程发出IO时,blkio控制器按权重分配带宽。通过限制备份进程权重,不让它在业务高峰期抢IO,同时利用cgroup的throttle特性设置IOPS上限。
文件系统参数
noatime禁止每次读更新atime,减少一次页缓存写回。barrier=0关闭写屏障,ext4默认在事务提交时发送barrier保证顺序,关闭后提升25%写性能,但面临掉电崩溃风险。生产环境建议升级XFS,它在大部分场景下无barrier开销。
Direct I/O原理
绕过page cache,数据直接DMA到用户态buffer,减少一次内核到用户拷贝。写操作除非调用fsync,否则数据仍在脏缓冲区。MySQL的innodb_flush_method=O_DIRECT配合双写缓冲区,既保证数据安全又避免double buffering。
避坑指南
- 不要在全链路优化前测试修改:一次只改一个参数,用fio对比,否则无法确定收益来源。我最初同时改调度器和文件系统,结果回滚时花了一小时。
- Kyber调度器导致某些QoS策略失效:当使用blk-cgroup做带宽限制时,kyber的延迟控制会与其冲突,导致限流不准。建议此时改用mq-deadline配合cgroup throttling。
- barrier=0的灾难:生产环境误用barrier=0后一次掉电导致文件系统inode损坏,恢复花费4小时。一定要搭配UPS和日志设备(如XFS的日志单独放在NVMe上)。
- Direct I/O对齐:应用必须保证buffer起始地址、偏移量、长度都是块大小的整数倍(通常512B或4KB)。MySQL使用mmap分配内存,天然对齐。但如果你自己写程序,使用
posix_memalign分配。 - fio测试避开系统缓存:一定要加
--direct=1,否则fio读出的是page cache,结果虚高。 - blk-cgroup v2迁移:新版内核只支持cgroup v2,路径改到
/sys/fs/cgroup/io.weight,配置方式完全不同。检查/sys/fs/cgroup/unified是否存在。
总结
存储性能优化不是单一手段能解决的,需要贯穿IO栈的每一层。调优顺序:先用工具定位瓶颈层次(调度器、文件系统、应用);然后单选方案测试;最后固化组合。记住:任何参数改动都要可回滚、可量化。