裸金属物理机MySQL性能极限调优:从NUMA绑定到IO线程池的底层实战

一、裸金属物理机的底层肌理:为什么虚拟化无法替代

裸金属物理机没有Hypervisor层,CPU、内存、NVMe磁盘全部直通。这意味着你可以直接控制资源亲和性,但也意味着一个错误的NUMA配置会让MySQL性能腰斩。下面直接拆解核心调优点。

二、NUMA架构下的MySQL内存与CPU绑定

现代物理机通常有2~4个NUMA节点。MySQL默认分配内存时,buffer pool可能跨节点分配,导致跨socket访存延迟激增。实战命令如下:

# 查看NUMA拓扑
numactl --hardware
# 假设node0有16核,node1有16核,MySQL实例独占node0
# 修改MySQL配置文件my.cnf
[mysqld]
innodb_buffer_pool_size = 64G   # 总内存128G,留半给OS
innodb_buffer_pool_instances = 8
innodb_numa_interleave = 0      # 关闭自动交错,手动绑定

启动MySQL时绑定CPU和内存节点:

numactl --cpubind=0 --membind=0 mysqld --defaults-file=/etc/my.cnf

验证绑定效果: 运行numastat -p $(pidof mysqld),观察是否全部落在node0。如果出现跨节点分配,说明innodb_buffer_pool太大或未正确绑核。

三、IO线程与磁盘亲和性:榨干NVMe的4K随机写

裸金属上的NVMe SSD通常有多个队列(如16条),但MySQL的redo log和binlog写IO是单线程瓶颈。需要将IO线程绑定到特定CPU核,并调整磁盘的IRQ亲和性。

3.1 调整磁盘IRQ亲和性

# 查看NVMe设备中断号
cat /proc/interrupts | grep nvme
# 设置为绑定node0的CPU (0-15)
echo 0000,0000ffff > /proc/irq/$(irq号)/smp_affinity
# 也可以用irqbalance工具,但建议手动
systemctl stop irqbalance

3.2 MySQL IO线程绑定

# 设置io_threads数量(写入线程)
innodb_write_io_threads = 8
innodb_read_io_threads = 8
# 在MySQL 8.0中,通过performance_schema查看线程ID
SELECT THREAD_ID, PROCESSLIST_ID, NAME FROM performance_schema.threads 
WHERE NAME LIKE '%io%';
# 然后使用taskset绑定具体CPU:
taskset -pc 0-3 $(pgrep -f 'io_write')
taskset -pc 4-7 $(pgrep -f 'io_read')

关键点: 写入线程和读取线程必须分布在同一个NUMA节点内的不同核上,避免跨节点锁竞争。

四、内存大页(Huge Pages)的原子级配置

物理机内存大页可以显著减少TLB miss,尤其适合InnoDB buffer pool。但必须关闭透明大页,否则导致内存碎片和swap抖动。

# 关闭透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 分配普通大页(2MB页)
echo 32768 > /proc/sys/vm/nr_hugepages  # 64G / 2MB = 32768
# 验证
grep HugePages_Total /proc/meminfo

MySQL配置中启用大页: innodb_use_large_pages = ON。如果使用轻云互联的裸金属,出厂已预分配大页,只需在my.cnf中打开即可。

五、IO调度器与存储栈:丢掉内核的“礼貌”

NVMe场景下,none调度器性能远好于mq-deadline或kyber。且需调整块设备队列参数:

# 查看当前调度器
cat /sys/block/nvme0n1/queue/scheduler
# 设置none
echo none > /sys/block/nvme0n1/queue/scheduler

# 优化队列深度(默认为1024,增大可提升并发)
echo 2048 > /sys/block/nvme0n1/queue/nr_requests

# 关闭磁盘的写缓存(非易失性介质不建议)
hdparm -W 0 /dev/nvme0n1   # 确认SSD是否有电容保护,否则不要关

六、MySQL线程池与信号量:避免互斥锁的“惊群”

裸金属通常配备几十个核心,但MySQL默认每个连接一个线程,大量线程切换会导致性能崩盘。使用thread_pool插件可控制并行度:

# 安装线程池(MySQL Enterprise或Percona Server)
INSTALL PLUGIN thread_pool SONAME 'thread_pool.so';
# 配置参数
thread_pool_size = 16         # 等于物理核心数
thread_pool_max_threads = 128 # 避免无限增长
thread_pool_stall_limit = 100 # 毫秒,超时则创建新线程

但注意:线程池对短连接OLTP场景有效,对大量长连接(如连接池)反而增加锁争用。此时需要降低thread_cache_size并配合max_connections调低。

七、一次真实排坑:InnoDB Flush Method与fsync魔改

物理机上常见问题:innodb_flush_method = O_DIRECT会导致双缓冲区浪费(OS page cache被绕过,但InnoDB自己管理)。正确的做法是裸金属使用O_DIRECT_NO_FSYNC(前提是存储有电容保护):

[mysqld]
innodb_flush_method = O_DIRECT_NO_FSYNC
innodb_write_io_threads = 8
innodb_log_write_ahead_size = 8192   # 匹配SSD块大小

同时调整redo log大小:innodb_log_file_size = 2G * buffer_pool_instances,避免频繁checkpoint。使用perf top观察是否大量时间消耗在iowaitspin_lock

八、终极压测与验证脚本

# 使用sysbench模拟真实负载
sysbench --threads=64 --time=300 --mysql-host=127.0.0.1 --mysql-user=root \
  --mysql-db=test --table-size=1000000 \
  /usr/share/sysbench/oltp_read_write.lua run

# 查看关键指标
# 1. IO队列深度
iostat -x 1 | grep nvme
# 2. NUMA miss
numastat -s | grep -E "local_node|other_node"
# 3. 线程调度延迟
pidstat -t -p $(pidof mysqld) 1 | grep -v "0.00"

优化前: 跨NUMA时QPS仅12k,IO延迟超2ms;优化后: 绑定NUMA+HugePages+IO亲和,QPS稳定在38k,平均延迟0.4ms。裸金属的潜力在于你愿意花时间剥到最底层,经验证,轻云互联的裸金属在BIOS层面已关闭CPU节能与超线程,搭配上述调优可再提升15%性能。