MySQL应用的负载特征
对于典型的在线事务处理(OLTP)场景下的MySQL应用,其核心I/O负载特征就是“时延型随机小I/O”
1. 小I/O (Small I/O)
- 是什么:指的是每次从磁盘读取或写入的数据块很小。
- 在MySQL中为什么是这样:MySQL的InnoDB存储引擎(现在最主流的引擎)管理数据的基本单位是页(Page),默认大小为 16KB。
-
- 当执行一个
SELECT ... WHERE id = xxx;
查询时,即使你最终需要的只是一行几百字节的数据,InnoDB也必须从磁盘上读取包含这行数据的整个16KB的页到内存中(即Innodb_buffer_pool
)。 - 同样,当你修改一行数据时,最终也是将这行数据所在的整个16KB的脏页(Dirty Page)写回磁盘。
- 当执行一个
- 结论:MySQL的I/O操作,是以16KB为单位进行的,相对于动辄几MB甚至GB的文件传输或视频流,这属于非常典型的“小I/O”。
2. 随机I/O (Random I/O)
- 是什么:指的是数据访问的地址是离散的、不连续的,就像在磁盘上“跳来跳去”地读取。这与“顺序I/O”(Sequential I/O,即从头到尾连续读取一个大文件)相对。
- 在MySQL中为什么是这样:这主要是由其B+树索引结构决定的。
-
- 想象一下,你要查询3个用户的信息,他们的ID分别是
8
、5000
和12345
。 - 在B+树索引中,这三个ID对应的行数据,在物理磁盘上几乎不可能是存放在一起的。它们位于三个完全不同、毫无关联的16KB数据页上。
- 因此,数据库系统必须:
- 想象一下,你要查询3个用户的信息,他们的ID分别是
-
-
- 定位到存放ID
8
的数据页,读取它。 - 然后跳到另一个完全不同的物理位置,定位并读取存放ID
5000
的数据页。 - 再跳到又一个不同的位置,读取存放ID
12345
的数据页。
- 定位到存放ID
-
- 结论:这种由查询条件决定的、在磁盘上非连续的、跳跃式的读写模式,就是典型的“随机I/O”。
3. 时延型 (Latency-Sensitive)
- 是什么:指的是系统对单次I/O操作的响应时间(Latency,即延迟) 非常敏感。我们追求的是“快”,而不是“多”。
- 在MySQL中为什么是这样:
-
- 在线应用(如电商网站、社交App、游戏)通常要求极快的用户响应。用户点击一个按钮,背后可能触发了几个SQL查询。
- 每个查询都可能分解为数次独立的、随机的、16KB的磁盘读取。
- 整个请求的最终响应时间,很大程度上取决于每一次磁盘I/O操作完成得有多快。如果单次随机I/O需要20毫秒,一个需要5次I/O的查询可能就要花100毫秒以上,用户就能明显感觉到卡顿。
- 结论:我们关心的是“我这次I/O要等多久”(低延迟),而不是“我一秒钟能读多少MB数据”(高吞吐量)。这就是“时延型”的本质。
为什么理解这点很重要
将这三点结合起来,就是一幅完整的画面:一个典型的在线MySQL应用,其性能瓶颈在于能否快速地完成大量、不连续的、每次16KB大小的磁盘读写操作。
这个核心特征直接决定了MySQL的优化方向和硬件选型:
- 为什么
innodb_buffer_pool_size
至关重要? 因为它是一个巨大的内存缓存。目标就是尽可能把那些需要被随机访问的16KB数据页都放到内存里。内存的访问延迟是纳秒级,而磁盘是毫秒级,速度相差上万倍。Buffer Pool就是用来将“随机磁盘I/O”变成“随机内存访问”,从而消除“时延”的。 - 为什么索引是MySQL的生命线? 因为索引让MySQL能够进行精确的“随机小I/O”。如果没有索引,MySQL只能进行“顺序大I/O”——也就是全表扫描,对于大表来说这是灾难性的。
- 为什么SSD(固态硬盘)对数据库是革命性的? 因为传统机械硬盘(HDD)的“随机I/O”性能极差,磁头需要物理移动(寻道)和等待盘片旋转,延迟很高(通常>10ms)。而SSD没有机械部件,其随机读写性能是HDD的上百甚至上千倍,延迟极低(通常<1ms)。SSD完美匹配了数据库“时延型随机小I/O”的负载模型。
与之相对的是什么?
数据仓库、大数据分析(OLAP)等应用,它们的负载模型通常是“吞吐量型顺序大I/O”。它们需要一次性读取海量连续的数据进行批量计算,关心的是每秒能读写多少GB,而不是单次操作的延迟。这也是为什么这两类应用在架构设计和硬件选择上会有巨大差异的原因。
MySQL机器硬件指标
当前Linux服务器参数如下:80核,128G内存
1. fio
硬盘参数如下:
# 使用fio进行压力测试 fio 可以精准地模拟各种I/O负载,并提供详细的性能报告,包括IOPS和时延
sudo yum install -y fio
# 创建一个测试目录
mkdir /opt/test-dir
cd /opt/test-dir
# 运行fio测试命令
fio -name=randread_test -directory=./ -ioengine=libaio -direct=1 -rw=randread -bs=4k -size=10G -numjobs=4 -runtime=60 -group_reporting
● direct=1: 这个参数至关重要,它会绕过操作系统的页面缓存(Page Cache),直接测试物理硬盘的性能。否则,测试的将是内存读写的速度
● 测试文件: fio 通过读写一个文件来测试。请确保测试文件所在的分区有足够的空间。测试写入操作会破坏文件,请不要在重要的生产数据目录中直接测试。最好在一个空的测试目录下进行。
● 文件大小: size 参数设置的测试文件大小,建议至少是服务器内存的两倍,以确保测试的压力能真正落到磁盘上。
-name
: 测试任务的名称。-directory
: 测试文件存放的目录。-ioengine=libaio
: 使用Linux异步I/O引擎,效率更高。-rw=randread
: 随机读。-bs=4k
: 块大小4KB。-size=10G
: 测试文件总大小为10GB。-numjobs=4
: 启动4个并发进程进行测试。-runtime=60
: 测试持续运行60秒。-group_reporting
: 将所有任务的结果汇总报告
结果:
[root@localhost test-dir]# fio -name=randread_test -directory=./ -ioengine=libaio -direct=1 -rw=randread -bs=4k -size=10G -numjobs=4 -runtime=60 -group_reporting
randread_test: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=1
...
fio-3.7
Starting 4 processes
randread_test: Laying out IO file (1 file / 10240MiB)
randread_test: Laying out IO file (1 file / 10240MiB)
randread_test: Laying out IO file (1 file / 10240MiB)
randread_test: Laying out IO file (1 file / 10240MiB)
Jobs: 4 (f=2): [f(2),r(2)][100.0%][r=106MiB/s,w=0KiB/s][r=27.1k,w=0 IOPS][eta 00m:00s]
randread_test: (groupid=0, jobs=4): err= 0: pid=58998: Fri Aug 1 17:04:20 2025
read: IOPS=27.1k, BW=106MiB/s (111MB/s)(6343MiB/60001msec)
slat (usec): min=4, max=290, avg=14.39, stdev= 3.57
clat (usec): min=2, max=4089, avg=131.70, stdev=18.63
lat (usec): min=67, max=4103, avg=146.25, stdev=18.84
clat percentiles (usec):
| 1.00th=[ 111], 5.00th=[ 117], 10.00th=[ 119], 20.00th=[ 121],
| 30.00th=[ 124], 40.00th=[ 126], 50.00th=[ 129], 60.00th=[ 133],
| 70.00th=[ 137], 80.00th=[ 141], 90.00th=[ 147], 95.00th=[ 153],
| 99.00th=[ 186], 99.50th=[ 208], 99.90th=[ 314], 99.95th=[ 383],
| 99.99th=[ 594]
bw ( KiB/s): min=21344, max=28312, per=25.00%, avg=27063.67, stdev=978.15, samples=476
iops : min= 5336, max= 7078, avg=6765.90, stdev=244.54, samples=476
lat (usec) : 4=0.01%, 10=0.01%, 20=0.01%, 50=0.01%, 100=0.14%
lat (usec) : 250=99.63%, 500=0.20%, 750=0.02%, 1000=0.01%
lat (msec) : 2=0.01%, 4=0.01%, 10=0.01%
cpu : usr=3.65%, sys=15.07%, ctx=1624213, majf=0, minf=1904
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=1623896,0,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=1
Run status group 0 (all jobs):
READ: bw=106MiB/s (111MB/s), 106MiB/s-106MiB/s (111MB/s-111MB/s), io=6343MiB (6651MB), run=60001-60001msec
Disk stats (read/write):
dm-0: ios=1618828/59, merge=0/0, ticks=206620/0, in_queue=207052, util=100.00%, aggrios=1623896/76, aggrmerge=0/2, aggrticks=209112/1, aggrin_queue=208513, aggrutil=100.00%
sda: ios=1623896/76, merge=0/2, ticks=209112/1, in_queue=208513, util=100.00%
- 平均IOPS=27.1k
- 带宽 BW=106MB/s
- slat 提交时延:平均14.39us
- clat 完成时延:平均131.70us,0.13ms
99.00th=[ 186]
: 99% 的請求完成延遲都在 186微秒 以內99.90th=[ 314]
: 99.9% 的請求完成延遲都在 314微秒 以內- 用户态CPU占3.65% 内核态CPU占15.07% CPU非瓶颈
2. iostat
sudo yum install -y sysstat
# 每2秒刷新一次磁盘的性能指标
iostat -dmx 2
-d
: 显示磁盘设备的使用率。-m
: 以 MB 为单位显示统计信息。-x
: 显示更详细的扩展统计信息。
r/s
: 每秒读请求的数量 (Read IOPS)。w/s
: 每秒写请求的数量 (Write IOPS)。r_await
: 每个读请求的平均处理时间(毫秒),包括排队时间和服务时间。这是读时延的重要参考。w_await
: 每个写请求的平均处理时间(毫秒)。这是写时延的重要参考。aqu-sz
: 平均请求队列长度。如果这个值很高,说明I/O请求堆积严重。%util
: 磁盘的繁忙程度百分比。如果接近100%,说明磁盘已经饱和
3. 宝塔安装的mysql
[client]
#password = your_password
port = 3306
socket = /tmp/mysql.sock
[mysqld]
binlog_cache_size = 32K
thread_stack = 256K
join_buffer_size = 256K
query_cache_type = OFF
max_heap_table_size = 512M
port = 3306
socket = /tmp/mysql.sock
datadir = /www/server/data
default_storage_engine = InnoDB
performance_schema_max_table_instances = 400
table_definition_cache = 400
skip-external-locking
key_buffer_size = 1024M
max_allowed_packet = 100G
table_open_cache = 4096
sort_buffer_size = 16384K
net_buffer_length = 4K
read_buffer_size = 16384K
read_rnd_buffer_size = 256K
myisam_sort_buffer_size = 256M
thread_cache_size = 512
query_cache_size = 0M
tmp_table_size = 512M
sql-mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
explicit_defaults_for_timestamp = true
#skip-name-resolve
max_connections = 500
max_connect_errors = 100
open_files_limit = 65535
log-bin=mysql-bin
binlog_format=mixed
server-id = 1
expire_logs_days = 10
slow_query_log=1
slow-query-log-file=/www/server/data/mysql-slow.log
long_query_time=3
#log_queries_not_using_indexes=on
early-plugin-load = ""
innodb_data_home_dir = /www/server/data
innodb_data_file_path = ibdata1:10M:autoextend
innodb_log_group_home_dir = /www/server/data
innodb_buffer_pool_size = 40960M
innodb_log_file_size = 2048M
innodb_log_buffer_size = 512M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50
innodb_max_dirty_pages_pct = 90
innodb_read_io_threads = 80
innodb_write_io_threads = 80
[mysqldump]
quick
max_allowed_packet = 500M
[mysql]
no-auto-rehash
[myisamchk]
key_buffer_size = 1024M
sort_buffer_size = 16M
read_buffer = 2M
write_buffer = 2M
[mysqlhotcopy]
interactive-timeout
# bt_mysql_set = None
# bt_mem_size = None
# bt_query_cache_size = 0
可选性能调整参数:
4. 全局缓存区(Global Buffers)
这部分内存由所有线程共享,在MySQL启动时一次性分配。
1. innodb_buffer_pool_size = 4096 MB
(4GB)
- 意义: 这是InnoDB存储引擎最重要的参数,没有之一。它是一个缓存区域,用于缓存InnoDB表的数据和索引。理想情况下,尽可能多的热数据和索引都应放在这里,以减少磁盘I/O。
- 合理性分析:
-
4GB
的大小对于一个生产环境来说可能偏小,也可能合适,这完全取决于服务器的总物理内存。- 推荐原则: 在专用的数据库服务器上,通常建议将此值设置为总物理内存的 70%-80%。
- 结论: 如果服务器内存只有8GB,这个设置是合理的。但如果服务器有32GB或更多的内存,那么4GB就太小了,大部分内存都被浪费了,数据库性能会因为频繁的磁盘读写而受到严重影响。这是首先需要优化的参数。
2. key_buffer_size = 1024 MB
(1GB)
- 意义: 这是MyISAM存储引擎的索引缓存区。它只缓存MyISAM表的索引文件(
.MYI
),不缓存数据文件。 - 合理性分析:
-
- 这个值设置得相当大。其合理性完全取决于您是否还在大量使用MyISAM表。
- 在现代MySQL应用中,InnoDB因其支持事务、行级锁和崩溃恢复等高级特性,已经成为默认和推荐的存储引擎。
- 结论: 如果您的核心业务表都是InnoDB,那么这1GB的内存就是巨大的浪费。这些内存应该被回收,并分配给
innodb_buffer_pool_size
。如果确实有非常重要的、频繁访问的大型MyISAM表,这个设置可能是合理的,但这种情况现在比较少见。
3. innodb_log_buffer_size = 512 MB
- 意义: InnoDB事务日志的缓冲区。InnoDB在将事务日志写入磁盘(redo log)之前,会先将其存放在这个缓冲区中,以提高性能。
- 合理性分析:
-
- 512MB设置得过大了,属于典型的资源浪费。
- 日志缓冲区在以下情况会被刷新到磁盘:事务提交时、每秒钟(默认)、或者当缓冲区满了的时候。对于大多数拥有大量小事务的在线系统(OLTP),缓冲区远在被填满前就已经因为前两个原因被刷新了。
- 推荐值: 通常
16MB
到128MB
就足够应付绝大多数场景了。只有在处理包含大量数据(如TEXT/BLOB字段)的超大事务时,才需要更大的值。 - 结论: 强烈建议将此值降低到
64MB
或128MB
,并将节省下来的内存分配给innodb_buffer_pool_size
。
4. query_cache_size = 0
- 意义: 查询缓存(Query Cache),用于缓存SELECT查询的文本和其结果集。
- 合理性分析:
-
- 设置为0(禁用)是非常明智和正确的决定。
- 查询缓存在高并发和写操作频繁的场景下,会因为缓存失效和锁争用问题成为严重的性能瓶颈。它在MySQL 5.7中已被废弃,并在MySQL 8.0中被彻底移除。
- 结论: 配置合理,请保持。
5. 连接/线程级参数
5. max_connections = 500
- 意义: 允许的最大并发连接数。
- 合理性分析:
-
500
是一个中等大小的设置。它是否合理取决于您的应用架构和实际的并发访问量。- 需要注意的是,每个连接都会消耗内存(线程堆栈、连接缓存等)。这个值必须与服务器的内存大小和下面将要分析的“每连接缓存”相匹配,否则会有内存耗尽的风险。
- 建议: 通过监控
Threads_connected
状态变量来观察实际的峰值连接数,如果远小于500,可以适当调低。如果经常收到 “Too many connections” 错误,则需要增加(但在增加前必须先评估内存)。
6. table_open_cache = 4096
- 意义: 所有线程可以打开的表的缓存数量。当线程访问一个表时,MySQL会检查缓存中是否已有该表的句柄,从而避免重复打开文件描述符。
- 合理性分析:
-
4096
是一个比较大的值。对于拥有大量表(几千个)或者查询会频繁访问不同表的应用来说是合理的。- 可以通过监控
Opened_tables
和Open_tables
状态变量来判断。如果Opened_tables
持续快速增长,说明缓存可能不够。 - 结论: 对于繁忙的系统,此设置通常是合理的。
7. thread_cache_size = 512
- 意义: 服务器缓存的线程数量。当客户端断开连接时,其服务线程会被放回缓存中以供新连接复用,避免了创建和销毁线程的开销。
- 合理性分析:
-
- 512这个值设置得过高了。通常只有在连接和断开极其频繁(例如,每秒上千次短连接)的场景下才需要这么大的值。
- 一个过大的线程缓存会平白无故地占用内存。
- 推荐值: 一般
16
到64
之间就足够了。可以通过(Connections - Threads_running) / Connections
计算缓存命中率,或者观察Threads_created
的增长速度来判断。 - 结论: 建议大幅降低此值,比如先降到
64
,然后进行监控。
6. 每连接缓存区(Per-Session Buffers)
注意:这部分是潜在的“内存炸弹”! 这里的内存大小会乘以 max_connections
,构成理论上的最大内存使用量。
8. sort_buffer_size = 16384 KB
(16MB)
- 意义: 每个需要执行排序操作(如
ORDER BY
,GROUP BY
)的线程所分配的缓存。 - 合理性分析:
-
- 16MB这个值非常危险,设置得太大了。这是一个每个线程都会分配的缓存。如果有10个并发查询在排序,就会消耗
10 * 16MB = 160MB
内存。在500个连接的设置下,理论峰值极高,很容易导致服务器内存耗尽。 - 正确思路: 优化的首选应该是通过创建合适的索引来避免文件排序(filesort),而不是盲目增大
sort_buffer_size
。 - 推荐值: 建议设置为
256KB
到2MB
之间的一个更保守的值。 - 结论: 强烈建议立即调低此参数。
- 16MB这个值非常危险,设置得太大了。这是一个每个线程都会分配的缓存。如果有10个并发查询在排序,就会消耗
9. read_buffer_size = 16384 KB
(16MB)
- 意义: 用于对MyISAM表进行顺序扫描时使用的缓存。
- 合理性分析:
-
- 与
sort_buffer_size
一样,16MB同样过于危险。它也是每个线程分配的。 - 如果你的表主要是InnoDB,那这个参数基本无效。
- 正确思路: 同样,应该通过索引避免全表扫描,而不是依赖这个缓存。
- 结论: 强烈建议立即调低此参数,特别是如果您主要使用InnoDB。
- 与
10. read_rnd_buffer_size = 256 KB
- 意义: 用于随机读取行的缓存(例如,排序后按指针回表取数据)。这可以减少磁盘寻道,提高
ORDER BY
操作的性能。 - 合理性分析:
-
256KB
是一个比较标准和安全的值。对于需要大量排序和回表操作的查询,适当增大(如到1MB或2MB)可能有益,但256KB本身是一个不错的起点。- 结论: 设置合理。
11. join_buffer_size = 256 KB
- 意义: 当JOIN操作没有使用索引时,用于缓存表记录的缓存区。
- 合理性分析:
-
256KB
是一个标准、安全的默认值。- 正确思路: 解决join性能问题的根本方法是为关联字段创建索引,而不是增大这个缓存。
- 结论: 设置合理。
12. tmp_table_size = 512 MB
- 意义: 内存中临时表的最大大小。当SQL查询需要创建内部临时表(用于
UNION
,GROUP BY
等)时,如果大小没超过这个值,就在内存中创建。 - 合理性分析:
-
- 512MB同样是一个非常大且有风险的设置。它与
max_heap_table_size
共同决定内存临时表的上限。 - 虽然大的内存临时表可以避免使用磁盘临时表(性能较差),但它同样是每个线程都可能分配的。几个复杂的并发查询就可能耗尽数GB的内存。
- 结论: 建议降低到一个更安全的值,比如
64MB
或128MB
,同时监控Created_tmp_disk_tables
状态变量,看是否有大量临时表被迫转存到磁盘。
- 512MB同样是一个非常大且有风险的设置。它与
13. thread_stack = 256 KB
- 意义: 每个线程的堆栈大小。
- 合理性分析:
-
256KB
是64位系统下的标准默认值。通常不需要调整,除非有包含大量递归或复杂计算的存储过程导致堆栈溢出。- 结论: 设置合理。
14. binlog_cache_size = 32 KB
- 意义: 为每个执行事务的连接分配的、用于缓存二进制日志(binlog)更改的缓存。
- 合理性分析:
-
32KB
是默认值,对于有大事务(一次性插入/更新/删除大量数据)的系统来说可能偏小。- 如果事务大于这个值,超出的部分会被写入一个临时文件,造成磁盘I/O开销。可以通过观察
Binlog_cache_disk_use
状态变量来判断,如果该值大于0,说明需要增大binlog_cache_size
。 - 结论: 设置安全但可能不是最优。如果
Binlog_cache_disk_use
有增长,可以适当增大此值,例如1MB
或2MB
。
7. 总结与优化建议
这份配置最大的问题是:对“每连接缓存”设置得过于激进,而对最核心的“全局缓存” (innodb_buffer_pool_size
) 可能分配不足,并且存在明显的资源浪费。
优化优先级 sıralaması:
- 降低风险:
-
sort_buffer_size
: 降低到2MB
。read_buffer_size
: 降低到1MB
。tmp_table_size
: 降低到128MB
。innodb_log_buffer_size
: 降低到128MB
。thread_cache_size
: 降低到64
。
- 重新分配核心资源:
-
- 检查存储引擎: 确认线上是否还有核心业务在使用MyISAM。
key_buffer_size
: 如果基本不用MyISAM,将此值降低到32MB
。innodb_buffer_pool_size
: 将上述所有步骤节省下来的内存(可能有1GB + 0.5GB + …)全部加到这个参数上。根据服务器总内存,将其设置为物理内存的70%~80%。这是提升性能最立竿见影的措施。
- 监控与微调:
-
- 修改配置并重启MySQL后,持续监控关键状态变量(
Threads_connected
,Threads_created
,Created_tmp_disk_tables
,Binlog_cache_disk_use
,Innodb_buffer_pool_wait_free
,Innodb_buffer_pool_reads
vsInnodb_buffer_pool_read_requests
等),以验证新配置的有效性并进行微调。
- 修改配置并重启MySQL后,持续监控关键状态变量(