拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL性能分析实战:从慢查询定位到索引优化与参数调优

MySQL性能分析实战:从慢查询定位到索引优化与参数调优

1. 先定位慢查询:MySQL性能分析的起点

先说个我见过无数次的场景:业务线上反馈"查个列表要好几秒"、"接口超时了",运维同事第一反应是打开配置文件,把innodb_buffer_pool_size调大,把max_connections调大,重启完事。运气好能撑一阵子,运气不好问题依旧,因为方向一开始就偏了。

做MySQL数据库性能分析,第一步永远不是调参,而是回答一个问题:到底哪条SQL慢了?慢查询日志就是回答这个问题最直接的工具。

1.1 慢查询日志的开启与配置

MySQL的慢查询日志默认是关闭的。生产环境开启会有一点I/O开销,但这点开销相比排查问题的收益完全可以接受。我通常建议线上环境始终开着,哪怕阈值设大一点。

核心配置项就这几个:

-- 查看当前配置 SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time'; SHOW VARIABLES LIKE 'log_queries_not_using_indexes';

配置方式有两种。临时生效适合不动配置文件的情况下验证问题:

SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2; SET GLOBAL log_queries_not_using_indexes = 'ON';

永久生效就写进my.cnf的[mysqld]段:

slow_query_log = ON slow_query_log_file = /var/log/mysql/slow.log long_query_time = 2 log_queries_not_using_indexes = ON

这里有两个细节容易踩坑。

第一,long_query_time的单位是秒,支持小数。业务压力大的系统我建议设成1,压力小的系统设成2或3都行。设成10是MySQL的默认值,基本等于没开——能跑到10秒的SQL,大概率早被投诉了。

第二,log_queries_not_using_indexes这个开关很有用。它会把没有走索引的查询也记进慢查询日志,哪怕执行时间没到阈值。这等于给你一双额外眼睛,揪出那些"扫全表但运气好没超时"的潜在地雷。不过要留意,这会导致慢日志文件涨得很快,需要配合日志切割轮转策略。

1.2 用pt-query-digest分析慢查询日志

日志文件开起来之后,直接打开看偶尔还行,但线上跑一两天就是几百MB甚至几个GB,人眼看不过来。这时候要用工具。

pt-query-digest是Percona Toolkit里的经典工具,也是我做MySQL性能分析最常用的工具。安装方式各发行版不太一样,Ubuntu/Debian可以:

apt-get install percona-toolkit

CentOS/RHEL系可以直接从Percona仓库装:

yum install percona-toolkit

用法很简单:

pt-query-digest /var/log/mysql/slow.log

它会自动帮你做几件非常关键的事:

  • 按"指纹"把同构SQL聚合。比如只差查询参数的SELECT * FROM orders WHERE user_id = ?,会被归成同一条,再统计总执行次数、平均耗时、最大耗时、总耗时占比。
  • 按总执行时间排序,让你一眼看到哪些SQL是真正的性能黑洞。一条跑了20秒的复杂查询如果只执行了一次,和一个跑了0.5秒但每天执行几百万次的查询,后者才是更需要关注的。
  • 给出每个查询组的执行计划建议、索引建议,以及响应时间分布。

输出的报告里重点看这几列:

指标含义关注点
Query ID查询指纹ID同一个ID代表同类SQL
Count执行次数高频SQL值得优先优化
Exec time总执行时间排名的核心依据
Avg平均单次耗时倒推业务感知延迟
Max最大耗时是否存在偶发尖刺
Rows send返回行数是否拿了不需要的数据
Rows examine扫描行数判断索引是否生效

注意:pt-query-digest分析的是历史日志。定位问题之后,如果想让分析更贴近当前热点,可以先FLUSH SLOW LOGS清空原日志,跑一段时间窗口(比如5到10分钟),再针对这期间的数据分析,效果更精准。

1.3 一个真实的慢查询定位案例

两年前接过一个电商后台的活,运营反馈"订单列表页面上传Excel特别慢,经常转圈半分钟"。当时后台这套系统是Java写的,分页查询用的是最普通的LIMIT写法,数据库线上数据量在千万级。

我先开了慢查询日志观察半小时,然后用pt-query-digest分析,一条SQL非常扎眼:

SELECT * FROM order_detail WHERE order_status = 1 ORDER BY create_time DESC LIMIT 100000, 20;

执行次数不多,但单次执行时间超过了12秒。这条SQL的问题非常典型——深分页。LIMIT 100000, 20意味着MySQL取了前100020条,然后丢到前100000条,只把最后20条返回给你。偏移量越大,扫描和丢弃的行越多,耗时指数级上涨。

优化方案后来改成了延迟关联:

SELECT * FROM order_detail JOIN ( SELECT id FROM order_detail WHERE order_status = 1 ORDER BY create_time DESC LIMIT 100000, 20 ) t ON order_detail.id = t.id;

先让索引快速定位需要的那20条主键ID,再回表取完整行数据。同样查询时间从12秒降到了0.3秒以内。

这个案例我想强调的点是:如果没有慢查询日志,这种SQL藏在海量业务请求里,你根本无从下手。MySQL性能分析的第一步,永远是先拿到证据,而不是靠猜。

2. EXPLAIN执行计划:让MySQL告诉你它怎么干活

慢查询日志告诉你"谁慢",EXPLAIN告诉你"为什么慢"。这是MySQL性能分析里最核心、也最值得花时间吃透的一环。

EXPLAIN之后加一条SELECT查询,MySQL不会真的执行它,而是告诉你:这条SQL打算怎么执行,访问哪些表、用哪些索引、预计扫多少行、有没有额外的排序和临时表。这就像开车之前的导航预览——还没上路,你已经知道路线合不合理。

2.1 EXPLAIN各字段的实战解读

基础用法:

EXPLAIN SELECT o.id, u.name FROM orders o LEFT JOIN users u ON o.user_id = u.id WHERE o.status = 1 AND o.create_time > '2024-01-01';

返回结果是一张表,每行对应一个表访问路径。我最关注的字段是type、key、rows和Extra,这四个基本能讲清楚一条SQL的命案现场。

type是访问类型,它有一个从好到差的序列:

type含义参考评价
system系统表,只有一行极佳
const主键或唯一索引等值匹配极佳
eq_ref被驱动表通过主键/唯一索引等值匹配很优
ref普通索引等值匹配良好
range索引范围扫描(如 BETWEEN、>、<)可用
index扫描整棵索引树较差
ALL全表扫描最差,重点关注

见到ALL基本等于红灯。但这里有一个新手容易误判的地方:type是index也不一定就好。index表示它扫描了整个索引树,虽然比全表扫描的代价小,但数据量一大一样扛不住。我曾经碰到一条统计报表SQL,type显示index,看起来"用上了索引",实际慢得一批——因为它扫的是整棵二级索引树。

key字段告诉你实际用了哪个索引,possible_keys则列出可能被选中的候选。如果possible_keys不为空但key是 NULL,说明优化器评估后认为用索引还不如全表扫。这种情况往往是数据分布问题,比如在只有"男""女"两种值的性别列上建索引,优化器就不愿意用。

rows是优化器预估需要扫描的行数,记住是预估,不是真实值。用它做横向对比非常有用,但别当精确数字。

2.2 从ALL到ref的优化空间

举一个我调试过的例子。一个物流系统的运单表waybill,数据量大概1200万行,查询条件是:

SELECT waybill_no, status, sender_city, receiver_city FROM waybill WHERE sender_city = '上海' ORDER BY create_time DESC LIMIT 30;

EXPLAIN显示type=ALL,rows估算 1180万,Extra里还有Using filesort(文件排序)。因为sender_city列没有索引,MySQL只能全表扫,扫完1200万行还要对create_time做排序。

优化方案是在(sender_city, create_time)上建了复合索引。这个索引同时满足两个需求:等值过滤(sender_city = '上海')和排序(create_time DESC)。加了索引之后,type变成ref,rows降到几千行,Using filesort消失——因为索引本身已经按create_time排好序了,MySQL直接按索引顺序往后取30行就行。

这个案例能解释一个很重要的设计原则:能同时服务WHERE过滤和ORDER BY排序的复合索引,是最省事的索引。

2.3 Extra列里隐藏的信息

Extra列里的几个值值得特别警惕:

  • Using filesort:MySQL需要额外做一次排序。这不是说一定用了磁盘文件,但排序总是要消耗时间和内存的,性能杀手级别。
  • Using temporary:查询过程中创建了临时表。常见于GROUP BY没有走索引、DISTINCT大量去重、多表关联时驱动表选取不当。临时表要是落盘了,性能更是雪上加霜。
  • Using index:好消息,表示这个查询是覆盖索引,需要的字段都在索引树里,不用回表。这个状态越常见越好。
  • Using where:MySQL在存储引擎层取到数据后再用WHERE过滤。本身不是坏事,但配合高rows值来看,说明很多行被取出来又扔掉了。

注意:MySQL 8.0.18之前的版本EXPLAIN没有直观的耗时字段,8.0.18之后可以用EXPLAIN ANALYZE,它会实际上执行查询并给出每一步的耗时和真实扫描行数。在本地环境或从库上做诊断时,EXPLAIN ANALYZE比传统EXPLAIN更贴近真相。

3. 索引优化实战:最左前缀、覆盖索引与失效场景

慢查询日志和EXPLAIN都告诉你"索引没走对"之后,就要落到索引设计本身了。这一节我挑三个最常见的实战场景展开,每一块背后都有真实项目踩过的坑。

3.1 最左前缀规则与复合索引设计

复合索引是MySQL性能优化里最有威力的武器,但同时也最容易用错。它的核心规则就是最左前缀:一个复合索引(a, b, c),查询条件必须从第一个列开始,从左往右连续匹配,中间的列不能跳过。

举例说明:

-- 建一个复合索引 ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time); -- 有效:三个条件全用,走idx_user_status_time SELECT * FROM orders WHERE user_id = 1001 AND status = 1 AND create_time > '2024-06-01'; -- 有效:只用了user_id和status,索引部分生效 SELECT * FROM orders WHERE user_id = 1001 AND status = 1; -- 有效:只用了user_id,索引部分生效 SELECT * FROM orders WHERE user_id = 1001; -- 无效:跳过了user_id直接从status开始,索引完全用不上 SELECT * FROM orders WHERE status = 1 AND create_time > '2024-06-01';

第三行这种"跳列"是最容易踩的坑。很多开发同学建了(user_id, status, create_time)复合索引,就觉得status和create_time也稳了,结果WHERE status = 1根本走不了索引。这里面的逻辑可以这么理解:复合索引相当于先按第一列排序,第一列相同的再按第二列排,所以查询条件里没有第一列,后续列的顺序信息就完全无法利用。

设计复合索引还有一个实践上的排序原则:等值条件列放前面,范围条件列放后面。因为范围查询(>、<、BETWEEN)一旦出现,后续的列就没法走索引排序了。比如WHERE user_id = 1001 AND create_time > '2024-01-01',user_id放前面是等值匹配,create_time放后面做范围扫描,这样user_id能精确命中,create_time在命中范围内顺序扫描,效率最高。

3.2 索引失效的典型场景

下面这些场景在建索引时看着可行,实际上用起来索引会直接失效。我按踩坑频率排序:

第一,对索引列使用函数或表达式。

-- 失效:对create_time用了DATE函数 SELECT * FROM orders WHERE DATE(create_time) = '2024-06-01'; -- 有效:改成范围查询,索引正常工作 SELECT * FROM orders WHERE create_time >= '2024-06-01 00:00:00' AND create_time < '2024-06-02 00:00:00';

MySQL的索引存的是原始值,不是函数处理后的值。你拿函数处理列时,优化器没法利用有序的索引树,只能全表扫。类似地,WHERE id + 1 = 100这种对列做算术运算也会失效。

第二,隐式类型转换。

如果索引列是字符串类型,查询条件写成了数字,或者反过来,MySQL会做隐式转换,导致索引失效。比如user_phone列是VARCHAR,查询写成WHERE user_phone = 13800138000,MySQL会把字符串列转成数字去比较,索引就废了。最稳妥的做法是应用层统一好类型,查询条件里别偷懒省掉引号。

第三,LIKE前导通配符。

-- 失效:以%开头 SELECT * FROM users WHERE name LIKE '%张%'; -- 有效:以固定前缀开头 SELECT * FROM users WHERE name LIKE '张%';

索引最擅长的是前缀匹配。'张%'可以走索引范围扫描,'%张%'必须把整列扫一遍。业务上非要模糊中缀匹配,要么考虑全文索引或ES之类的方案,要么接受全表扫的代价。

第四,OR条件连接。

-- 可能失效:只要有一个条件没索引,整个查询走不了索引 SELECT * FROM orders WHERE user_id = 1001 OR status = 1;

MySQL优化器处理OR时,如果两边条件有不同的访问路径,需要合并结果集,复杂度上升,经常干脆放弃索引。常见做法是把OR拆成两个查询UNION ALL,或者改成IN(等价于多条等值,优化器处理更友好)。

3.3 覆盖索引与回表优化

理解"回表",要先明白InnoDB索引的物理结构。InnoDB是聚簇索引结构,主键索引的叶子节点存了整行数据;二级索引的叶子节点存的是索引列的值加上主键值。用二级索引查数据时,先在二级索引里找到主键,再拿着主键去主键索引里找完整行,这个"再查一次"的过程就叫回表。

回表不是坏事,但回表次数和行数成正比。如果一个二级索引已经覆盖了你需要的所有列,MySQL就不需要回表,直接从索引树里拿到全部数据。这就是覆盖索引,也就是EXPLAIN里Extra显示Using index的情况。

实际落地经验:把SELECT的列尽量塞进复合索引里。比如上一节那个物流运单的案例,如果查询只需要waybill_no, status, sender_city, receiver_city四个列,而索引建成了(sender_city, create_time, waybill_no, status, receiver_city),那这次查询直接覆盖,连回表都省了。

但覆盖索引有副作用:索引列太多会让索引树变得臃肿,写入性能下降。所以实际项目中要权衡——高频低返回量的查询优先做覆盖,低频大写入的大宽表别一味堆索引。

提示:查看索引基数和重复度可以用SHOW INDEX FROM 表名。Cardinality字段代表索引中唯一值的估算数量,除以总行数就是区分度。区分度太低的列(比如只有两个枚举值)建单列索引通常没有意义,优化器也会弃用。

4. 连接池与并发锁:高并发场景的瓶颈点

慢查询和索引解决的是"单条SQL效率"问题。但线上有时候每条SQL都快,整体系统还是慢,这时候瓶颈往往出在连接管理和并发控制上。

4.1 连接池参数:别让连接数成为隐性瓶颈

MySQL本身有个并发上限。默认max_connections是151,很多云数据库默认也就几百。这个参数不是越大越好,因为每个连接都会占用内存、线程和系统资源。连接数一多,线程切换和锁竞争反而拖垮整体性能。

应用侧,不管是Java的HikariCP、Druid,还是Go、Python的数据库驱动,连接池的大小设计都有讲究。先看几个核心参数:

参数建议方向说明
initialSize和核心接口并发量匹配预热连接,避免冷启动
maximumPoolSize参考CPU核数×2至×4超过这个值,多数是在排队锁等待
minimumIdle维护最小空闲连接减少频繁建连开销
connectionTimeout3到5秒超过即失败,避免请求堆积
maxLifetime建议略小于数据库wait_timeout防止数据库回收后连接失效

有个流传很广的经验公式:maxPoolSize = CPU核心数 × 2 + 1。这个公式源于PostgreSQL社区,应用在MySQL场景下大体方向也没错——连接数不是越多越好,IO密集或慢SQL多的情况下按业务实际压测调整。

连接池参数的另一个坑是wait_timeout。MySQL服务端默认8小时空闲断开。如果应用连接池里的连接长期空闲,超过wait_timeout之后MySQL服务端会主动断掉,但应用连接池不一定知道,拿到的就是一个"死连接"。表现就是偶发的"Communications link failure"。解法是应用连接池配置maxLifetime略小于数据库wait_timeout,比如数据库设8小时,连接池的maxLifetime设4到6小时,让它主动重建连接。

4.2 锁等待与死锁排查

数据库并发越高,锁冲突越常见。InnoDB行锁不是没有代价的——当两个事务同时操作同一行,后到的要等待前一个提交,等待时间超过innodb_lock_wait_timeout(默认50秒)就会报锁等待超时。

排查锁等待问题,我的标准流程是三步:

第一步,看当前有哪些事务在跑:

SELECT * FROM information_schema.innodb_trx\G

重点关注trx_state、trx_started、trx_query。如果发现某个事务已经跑了几分钟一直没提交,它持有的行锁不释放,后面所有碰同一行的操作都得排队。

第二步,查锁等待关系:

SELECT waiting_trx_id, waiting_thread, waiting_query, blocking_trx_id, blocking_query FROM sys.innodb_lock_waits;

MySQL 8.0的sys库里有现成的视图,直接告诉你谁在等谁、谁堵了谁。

第三步,拿根因证据:

SHOW ENGINE INNODB STATUS\G

看LATEST DETECTED DEADLOCK部分,里面有死锁的完整记录:哪两个事务、执行了哪两条SQL、持有和等待哪些锁。

死锁的典型例子是两个事务互相等对方持有的锁。比如事务A先更新了user_id=1的行,再更新user_id=2的行;事务B反着来,先更新2再更新1。两个事务各抱着一把锁不撒手,就死锁了。MySQL会检测到死锁并选一个代价较小的事务回滚。

应对死锁的实用策略:

  • 保持事务短小:一个事务里别塞太多SQL,尤其别在事务里做远程调用或复杂业务逻辑。
  • 固定操作顺序:多行更新按主键排序执行,大家排队来,不交叉。
  • 减少锁的持有范围:能走索引的DML一定走索引,否则行锁会升级成锁更多的记录。
  • 利用死锁检测:应用侧做好重试机制,死锁报错后重试一次通常就成功了。

4.3 事务隔离级别的取舍

MySQL默认的事务隔离级别是REPEATABLE READ(可重复读)。这个级别下,InnoDB除了普通行锁,还会有间隙锁(Gap Lock),用于防止幻读。间隙锁在高并发下会导致一个很隐蔽的问题:你更新了一个范围内的部分记录,MySQL把整个范围的间隙都锁住了,其他事务在相同范围内插入记录会被阻塞。

好多团队的线上业务其实没有强一致性的读要求,完全可以用READ COMMITTED(读已提交)替换。这个级别没有间隙锁,并发插入能力会明显提升。

实际切换方式:

-- 全局级别 SET GLOBAL transaction_isolation = 'READ-COMMITTED'; -- 会话级别 SET SESSION transaction_isolation = 'READ-COMMITTED';

永久配置写在my.cnf:

transaction-isolation = READ-COMMITTED

注意:切换隔离级别前要和业务方确认,真的有事务需要"同一个事务内多次读取结果一致"吗?一般来说,订单、支付这类强一致要求的场景,保持默认别动;报表、日志、社交类读取,切到READ COMMITTED收益明显。

5. 从热搜词看MySQL日常运维的经典问题

写这篇文章之前,我扫了一眼最近MySQL相关的搜索热词,高频问题集中在:mysql安装教程、mysql ssl连接错误、error 2002 can't connect to local mysql server through socket、数据库同步工具、主从复制。这些正是我日常在群里被问最多的问题。集中聊几个有代表性且和性能/稳定性直接相关的。

5.1 MySQL SSL连接错误:不是配置问题就是版本兼容问题

MySQL从5.7开始默认开启SSL支持,8.0更是默认就有。应用侧连接串里加上useSSL=true时,如果服务端的SSL证书配置有问题,报错一抓一大把:SSL connection error、Public Key Retrieval is not allowed、Unable to load authentication plugin 'caching_sha2_password'。

最常见的三个原因:

第一,服务器SSL证书过期。MySQL默认的SSL证书有效期比较长,但你要是自己签的证书,到期了客户端就握手失败。检查一下有效期:

SHOW STATUS LIKE 'Ssl_server_not_after';

第二,客户端服务端TLS版本不兼容。老版本客户端(比如JDBC驱动太老)只支持TLSv1.1,MySQL 8.0默认要求TLSv1.2以上。升级驱动或者排查一下驱动版本。

第三,本地开发环境无证书导致连不上。很多开发同学在本地用老版本navicat连接远端MySQL时,遇到Public Key Retrieval is not allowed,原因是MySQL 8.0的caching_sha2_password插件要求客户端先获取服务端公钥。连接串里加一个参数就行:

allowPublicKeyRetrieval=true&useSSL=false

注意这个是开发环境处理方式,生产环境还是要配置好正规证书,别把加密连接裸奔。

5.2 ERROR 2002 socket连接问题:路径不一致的典型排查

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)这个报错在Linux环境太经典了。它说的是客户端在指定的socket路径上找不到MySQL服务器。

排查顺序我建议这么来:

第一,确认MySQL进程是否在跑:

ps -ef | grep mysqld

如果没起来,看/var/log/mysql/error.log里的启动报错。很多情况下是磁盘满了、数据目录权限不对、配置文件里有非法参数。

第二,进程在跑但还报这个错,就是socket路径不一致。MySQL服务端和客户端用的socket路径配置在不同的地方。服务端在my.cnf的[mysqld]段写socket=/var/run/mysqld/mysqld.sock,客户端默认找/tmp/mysql.sock。

解决方式要么统一两边的socket路径,要么连接的时候手动指定:

mysql -h127.0.0.1 -P3306 -uroot -p

用-h127.0.0.1强制走TCP而不是本地socket,绕开这个问题。但要注意,如果MySQL配置了skip-networking,那TCP也连不上,还是得回头解决socket路径问题。

5.3 主从复制延迟与数据库同步方案

搜索热词里"数据库同步软件"、"主从复制"出现频率很高。业务一增长,单库扛不住,读写分离就要提上日程。

MySQL原生主从复制的原理不复杂:主库把变更写进binlog,从库的IO线程拉取binlog写入本地relay log,SQL线程再回放relay log。流程里任何一个环节慢了,就表现为从库延迟(Seconds_Behind_Master升高)。

查从库状态:

SHOW SLAVE STATUS\G

重点看这几列:

字段关心点
Slave_IO_Running是否在拉取binlog,应为Yes
Slave_SQL_Running是否在回放relay log,应为Yes
Seconds_Behind_Master回放延迟秒数,接近0正常
Last_IO_Errno / Last_SQL_Errno报错码,非0需要处理
Master_Log_File / Read_Master_Log_PosIO线程读到主库哪个位置
Relay_Master_Log_File / Exec_Master_Log_PosSQL线程执行到哪个位置

延迟居高不下的原因,通常是以下几种:

  • 从库机器配置比主库差,硬件跟不上。
  • 主库大事务太多,binlog里一个事务要回放很久。
  • 从库上跑着报表查询、备份任务,抢CPU和I/O。
  • 复制是单线程的(SQL线程只有一个),主库并发写入高时从库容易追不上。可以开启并行复制(slave_parallel_workers)缓解。

如果对MySQL原生复制不满意,或者有异构同步需求(比如同步到ES、ClickHouse、Kafka),业界成熟方案有Canal(阿里巴巴开源,伪装成从库解析binlog)、Debezium、DataX等。选型时重点考察三块:解析binlog的稳定性、断点续传能力、对DDL的处理方式。

6. 性能参数调优:一套可以直接参考的配置思路

最后聊聊大家最爱问的:MySQL到底怎么调参?我的态度是:参数调优是锦上添花,不是雪中送炭。SQL不优化、索引不设计好,调参只是自欺欺人。反过来,如果慢查询优化和索引设计都做到位了,参数调优能让系统更稳定地承接高负载。

6.1 内存与磁盘I/O的核心参数

先列几个影响最大的参数。它们每一个调整背后都有明确的原理依据,不是拍脑袋。

innodb_buffer_pool_size:这是InnoDB最重要的参数,没有之一。它决定了数据和索引在内存中的缓存容量。生产上我一般建议设为物理内存的50%到70%,前提是这台机器专门跑MySQL。设置后要确认实际生效:SHOW VARIABLES LIKE 'innodb_buffer_pool_size';。MySQL 5.7以上支持动态调整:SET GLOBAL innodb_buffer_pool_size = 4G;,8.0还支持在线调整不重启。

innodb_log_file_size:redo log文件大小,默认值在不同版本不一样。日志太小会导致日志频繁切换,刷盘更频繁,写入性能受影响。业务写入量大的话,建议redo log总量(innodb_log_file_size × innodb_log_files_in_group)至少能容纳半小时以上的写入量。

innodb_flush_log_at_trx_commit:这个参数是个典型的性能与安全取舍。值为1时,每次事务提交都强制刷盘,最安全,但I/O开销最大。值为2时,每秒刷一次,事务提交时只写操作系统缓冲,崩溃时最多丢1秒数据。对不需要极端一致性的业务(比如日志、非核心业务表),设成2能显著降低写延迟。

6.2 一张8G内存MySQL实例的参数示例

我以一个中小型业务为例,假设机器是8核8G,MySQL独占,给一份我实测过的基线配置,仅供参考:

[mysqld] # 连接数 max_connections = 500 max_connect_errors = 1000 wait_timeout = 28800 interactive_timeout = 28800 # InnoDB缓冲池,占物理内存60%左右 innodb_buffer_pool_size = 5G innodb_buffer_pool_instances = 4 innodb_log_file_size = 1G innodb_log_files_in_group = 2 innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT # 并发线程 innodb_thread_concurrency = 16 thread_cache_size = 128 # 表 table_open_cache = 2048 table_definition_cache = 2048 # 查询缓存相关(8.0已移除,5.7以下可忽略) query_cache_type = 0 # 字符集 character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ci

这里有几个参数值得单独说明。

innodb_flush_method = O_DIRECT的意思是数据文件的读写绕过操作系统文件缓存,直接访问磁盘。这样可以避免双重缓存浪费,因为innodb_buffer_pool_size本身已经是内存缓存,没必要让操作系统再缓存一份同样数据。

innodb_thread_concurrency我设置了16,这个值根据并发负载调整。设置太小会限制并发能力,设置太大又会增加线程切换开销。经验上从CPU核数两倍左右起步,压测后调整。

table_open_cache是打开表缓存。如果线上报Too many open files,值得关注这个值以及open_files_limit。

6.3 调参不能凭感觉,要建立基线

最后一条经验:任何参数调整都要有前后对比。改前记录三个基线数据:

  • 业务高峰期的QPS、TPS。
  • 慢查询数量。
  • 关键SQL的执行时间分布。

改完跑一天再对比,数据说话,别凭感觉。好的参数组合是压测和监控喂出来的,网上抄的配置只能作为起点。

还有一个容易忽略的事:调参后要在低峰期验证并留好回滚方案。有一次我在线上把innodb_buffer_pool_size从4G调到6G,结果这台机器上还跑着其他服务,内存不足导致数据库OOM。这种情况要从全局视角看资源,别让MySQL吃干抹净。

我自己做MySQL性能分析的套路,总结下来就是五步:先开慢查询日志定位问题SQL,再用EXPLAIN分析执行计划,然后针对性地补索引或改写SQL,排查连接池和锁问题,最后才动系统参数。这个顺序执行下来,绝大多数线上问题都能在半天内给出明确的解决方案。性能分析没有玄学,每一步都有据可查——把证据链拉出来,问题自然就浮出水面了。

返回列表