【MySQL深入详解】第09篇:文件系统与磁盘IO——让MySQL数据写入飞起来

5 阅读7分钟

开篇引入

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日志型可接受注意版本,有些有性能瓶颈
ZFSZIL日志特殊需求资源消耗大
btrfsCoW实验性生产环境慎用

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:deadlinenoop
  • 有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

小结

  1. SSD是MySQL的标配:NVMe比SATA SSD快10倍以上
  2. XFS是最佳文件系统:ext4也可以,但要注意配置
  3. 禁用atime记录:noatime,nodiratime能省5-10%开销
  4. IO调度器要改:数据库服务器不用CFQ,用deadline或noop
  5. 避免交换:swappiness=0,完全禁用更佳
  6. RAID 10是数据盘首选:RAID控制器缓存优先用于写操作
  7. O_DIRECT模式:配合RAID缓存能大幅提升性能
  8. 监控vmstat和iostat:IO瓶颈要用数据说话

IO优化没有银弹,关键是找到瓶颈在哪。MySQL的IO行为是复杂的,需要结合监控数据来持续调优。


上一篇【第08篇】CPU与内存选型——MySQL服务器的硬件配置

下一篇【第10篇】MySQL配置原理——从配置文件到动态变量


延伸阅读

  • 《高性能MySQL》第4章 操作系统和硬件优化
  • Linux IO调度器文档
  • Percona: MySQL Performance Blog