开篇引入
MySQL的性能瓶颈,50%以上都在IO上。数据要写入磁盘,索引要读写磁盘,事务日志要刷盘……如果IO配置不对,再强的CPU、再大的内存都白搭。
很多人买了几万块的NVMe SSD,却发现性能还是上不去。问题往往不在硬件,而在配置。《高性能MySQL》第4章花了大篇幅讲IO配置,这篇文章帮你把关键点理清楚。
IO子系统的核心概念
顺序IO vs 随机IO
这是理解数据库IO的关键。
顺序IO:数据连续读写,磁盘磁头移动少,速度快。典型场景:日志写入、批量数据读取。
随机IO:数据分散读写,磁头要来回移动,速度慢。典型场景:在线事务修改、索引更新。
顺序读:500 MB/s
随机读(HDD):100 IOPS ≈ 1-2 MB/s
随机读(NVMe SSD):500,000 IOPS ≈ 几GB/s
SSD把随机IO的性能提升了1000倍以上,这就是为什么现在SSD是MySQL服务器的标配。
读写比例
MySQL的工作负载读写比例大概是:
读多写少:OLTP系统通常80-90%读,10-20%写
读少写多:日志系统、写入密集型
混合读写:分析型系统
不同的读写比例,IO优化策略完全不同。
文件系统选择
推荐:XFS
对于MySQL来说,XFS是最佳选择之一。
| 文件系统 | 日志模式 | 适用场景 | 备注 |
|---|---|---|---|
| XFS | 日志型 | MySQL首选 | 高并发下表现优秀 |
| ext4 | 日志型 | 可接受 | 注意版本,有些有性能瓶颈 |
| ZFS | ZIL日志 | 特殊需求 | 资源消耗大 |
| btrfs | CoW | 实验性 | 生产环境慎用 |
ext4的三个日志模式
ext4支持三种日志模式,性能影响不同:
1. data=writeback(最快)
# /etc/fstab
/dev/sda1 /var/lib/mysql ext4 defaults,noatime,nodiratime,data=writeback 0 2
- 只记录元数据变更
- 数据和元数据写入不同步
- InnoDB有自己的事务日志,这种模式安全
- 推荐用于MySQL
2. data=ordered(默认,推荐)
/dev/sda1 /var/lib/mysql ext4 defaults,noatime,nodiratime,data=ordered 0 2
- 元数据写入前先写数据
- 比writeback稍慢,但更安全
- 大多数场景够用
3. data=journal(最慢)
- 先把数据写入日志,再写入最终位置
- 除非有特殊需求,否则不建议
必须禁用的选项
# 禁用访问时间记录,读取也要写,能省5-10%开销
noatime
nodiratime
# 完整示例 /etc/fstab
/dev/sda1 /var/lib/mysql xfs defaults,noatime,nodiratime,noquota 0 2
O_DIRECT模式
InnoDB的O_DIRECT模式可以绕过文件系统缓存,直接读写磁盘:
-- 查看当前刷新方式
SHOW VARIABLES LIKE 'innodb_flush_method';
[mysqld]
# Linux下推荐,配合RAID缓存使用
innodb_flush_method = O_DIRECT
# Windows下
innodb_flush_method = async_unbuffered
注意事项:
- 需要文件系统支持直接IO(XFS、ext4都支持)
- RAID控制器的写缓存会发挥作用
- 会禁用文件系统的预读(InnoDB自己会预读)
IO调度器配置
Linux IO调度器
Linux的IO调度器决定请求发送到磁盘的顺序。
CFQ(完全公平队列):笔记本默认设置,对服务器来说很糟糕,会导致响应时间不稳定。
Deadline(截止时间):最适合数据库,直连硬盘用它。
NOOP:最适合有自己调度器的设备(RAID控制器、SAN)。
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# 输出: [mq-deadline] none
# 临时修改(重启失效)
echo "deadline" > /sys/block/sda/queue/scheduler
# 永久修改(CentOS 7)
# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX="... elevator=deadline"
grub2-mkconfig -o /boot/grub2/grub.cfg
推荐配置:
- 直连SSD/NVMe:
deadline或noop - 有RAID控制器:
noop(让RAID自己调度) - 不要用CFQ
内存与交换配置
避免交换
MySQL讨厌交换!交换发生时,性能会断崖式下降。
# 查看交换情况
vmstat 5
# si/so列应该接近0
# 理想情况:完全禁用交换
swapoff -a
# 临时启用
swapon -a
swappiness参数
# 查看当前值(默认60,对服务器来说太高)
cat /proc/sys/vm/swappiness
# 临时修改
sysctl vm.swappiness=0
# 永久修改
echo "vm.swappiness=0" >> /etc/sysctl.conf
NUMA问题
在多路CPU服务器上,内存分配不均匀可能导致swap:
# 查看NUMA配置
numactl --hardware
# MySQL启动时锁定内存(需要root)
[mysqld]
memlock = 1 # 危险:设置太大会导致MySQL启动失败
推荐方案:使用numactl启动MySQL:
numactl --interleave=all --cpunodebind=0,1,2,3 mysqld ...
外部内存分配器
用jemalloc或tcmalloc替换glibc的内存分配器,可以减少内存碎片:
# 安装jemalloc
yum install jemalloc
# my.cnf配置
[mysqld]
malloc-lib = /usr/lib64/libjemalloc.so.1
RAID配置与缓存
RAID级别回顾
| 级别 | 读性能 | 写性能 | 冗余 | 适用场景 |
|---|---|---|---|---|
| RAID 0 | 最快 | 最快 | 无 | 开发测试 |
| RAID 1 | 好 | 中 | 镜像 | 日志盘 |
| RAID 5 | 好 | 较差 | 校验 | 容量优先 |
| RAID 6 | 好 | 中 | 双校验 | 大容量 |
| RAID 10 | 最好 | 最好 | 镜像+条带 | MySQL首选 |
RAID缓存配置
RAID控制器的缓存应该优先用于写操作:
# RAID卡缓存策略配置(需要RAID卡管理工具)
# 写缓存:100%(RAID 0/1/10)
# 读缓存:根据工作负载调整
电池备份单元(BBU):RAID 5/6必须有的硬件,可以保护写缓存中的数据。
有BBU + 写缓存启用 → 高性能 + 安全
无BBU + 写缓存启用 → 断电极端数据丢失
InnoDB与RAID配合
[mysqld]
# 日志文件放RAID 1(顺序写入为主)
# 数据文件放RAID 10(随机读写为主)
# 或者都用RAID 10
# 如果有独立的高速存储做日志
innodb_log_group_home_dir = /fast-storage/mysql/logs/
监控IO性能
vmstat
# 每5秒刷新一次
vmstat 5
# 关键列:
# r: 运行中的进程(CPU等待)
# b: 阻塞进程(IO等待)
# si/so: 交换入/出(应该接近0)
# cs: 上下文切换
iostat
# 每2秒显示一次
iostat -x 2
# 关键指标:
# %util: 设备利用率(>70%说明是瓶颈)
# avgqu-sz: 平均队列长度
# await: 平均等待时间(毫秒)
# svctm: 平均服务时间
MySQL内部IO监控
-- InnoDB缓冲池等待(应该接近0)
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_wait_free';
-- 页面刷新
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_flushed';
-- 日志刷新
SHOW GLOBAL STATUS LIKE 'Innodb_log_write_requests';
SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';
常见IO问题与解决方案
问题1:IO利用率100%但CPU不高
原因:IO等待,不是计算瓶颈
解决:
-- 检查是否是日志写入
SHOW GLOBAL STATUS LIKE 'Innodb_log_write_requests';
-- 考虑使用电池备份的RAID缓存
-- 或者增加日志缓冲区
SET GLOBAL innodb_log_buffer_size = 16777216; -- 16MB
问题2:写入突然变慢
原因:可能是脏页刷新压力
-- 查看脏页比例
SHOW ENGINE INNODB STATUS\G
-- 找到: Modified db pages
-- 降低刷新阈值
SET GLOBAL innodb_max_dirty_pages_pct = 50; -- 默认75
问题3:重启后性能下降
原因:Buffer Pool冷启动
[mysqld]
# 启动时预热
innodb_buffer_pool_load_at_startup = ON
innodb_buffer_pool_dump_at_shutdown = ON
问题4:长时间运行的查询导致IO飙高
解决:限制长时间扫描
-- 启用优化器追踪
SET GLOBAL optimizer_trace = 'enabled=on';
SET GLOBAL optimizer_trace_max_mem_size = 1048576;
最佳实践清单
# /etc/fstab 配置示例
/dev/sda1 /var/lib/mysql xfs defaults,noatime,nodiratime,noquota 0 2
# sysctl配置
vm.swappiness = 0
vm.dirty_ratio = 60
vm.dirty_background_ratio = 5
# my.cnf InnoDB配置
[mysqld]
# 文件系统
innodb_flush_method = O_DIRECT
# 日志
innodb_log_file_size = 2G
innodb_log_files_in_group = 3
innodb_flush_log_at_trx_commit = 1
# 缓冲池(重要!)
innodb_buffer_pool_size = 128G # 物理内存的70%
innodb_buffer_pool_instances = 8
# 预热
innodb_buffer_pool_load_at_startup = ON
innodb_buffer_pool_dump_at_shutdown = ON
小结
- SSD是MySQL的标配:NVMe比SATA SSD快10倍以上
- XFS是最佳文件系统:ext4也可以,但要注意配置
- 禁用atime记录:noatime,nodiratime能省5-10%开销
- IO调度器要改:数据库服务器不用CFQ,用deadline或noop
- 避免交换:swappiness=0,完全禁用更佳
- RAID 10是数据盘首选:RAID控制器缓存优先用于写操作
- O_DIRECT模式:配合RAID缓存能大幅提升性能
- 监控vmstat和iostat:IO瓶颈要用数据说话
IO优化没有银弹,关键是找到瓶颈在哪。MySQL的IO行为是复杂的,需要结合监控数据来持续调优。
延伸阅读
- 《高性能MySQL》第4章 操作系统和硬件优化
- Linux IO调度器文档
- Percona: MySQL Performance Blog