MySQL 8.0生产环境避坑指南:从5.7迁移必须知道的那些坑
发布时间:2026/10/19 9:59:00 锦皓数字建站

写这篇东西的起因是因为最近半年聊MySQL的人口径悄悄变了。前两年大家问的都是“8.0能不能上”现在问的是“8.0我们也上了但这些怪问题你们遇到过没有”。MySQL 8.x已经成了新项目的事实标准老项目也基本都在迁移路上或者已经迁完但大量此前没被暴露的坑恰恰是等大家真正跑起来、跑久了以后才集中冒出来的。我自己是从5.7时代一路用过来的一开始也是把8.0当成“大版本号性能优化”去理解结果在认证、优化器、存储结构、复制、在线DDL这些方向上先后被上了一课又一课。这篇文章不打算复述官方文档而是把我在生产环境里真实撞过、或者帮别人排查过的那些“8.0特有坑”整理出来。适合三类人看正在做5.7到8.0迁移评估的架构师、已经上线8.0但遇到诡异问题的DBA、以及写SQL时总被执行计划“教育”的后端开发。1. 一进版本就能撞上的三个默认值变化认证、排序规则、SQL模式1.1 caching_sha2_password不只是“改回去”这么简单8.0的默认认证插件从mysql_native_password换成了caching_sha2_password这个事绝大多数人都知道但知道和踩坑是两回事。最常见的症状是代码连不上库报Authentication plugin caching_sha2_password cannot be loaded于是很多人第一反应就是把用户的认证方式改回mysql_native_password。我见过好几个团队都是这么干的也确实立刻解决了问题。但这么做只是把一个坑挪到未来罢了老认证协议迟早被彻底弃用如果你们公司的安全审计已经开始强制检查弱认证协议你会发现腾挪空间越来越小。而且更隐蔽的是同一个实例里如果既有新插件用户又有老插件用户一些连接池在预热时的行为会很古怪偶发握手失败查日志又什么都看不出来。真正稳妥的做法是升级客户端驱动然后保留caching_sha2_password。这里有一个典型的细节Java驱动默认不允许获取RSA公钥来做密码明文传输如果不显式开启allowPublicKeyRetrievaltrue在缓存认证失效的瞬间就会报Public Key Retrieval is not allowed。很多系统测试环境跑得好好的一到线上并发一上来就偶发报错就是因为第一次握手需要做公钥交换。我自己的习惯是升级8.0后先把所有连接池预热脚本跑一遍再观察一两天认证相关的错误日志确认没有偶发握手失败再放量。1.2 默认排序规则变了索引可能在你没意识到的情况下失效8.0默认字符集是utf8mb4默认排序规则是utf8mb4_0900_ai_ci而大量5.7老库用的是utf8mb4_general_ci。这两者的区别不只是“排序结果可能不同”更麻烦的是它会悄悄影响SQL的执行计划。举个例子一张业务表用了utf8mb4_general_ci另一张字典表用了utf8mb4_0900_ai_ci两张表做JOIN连接字段的排序规则不一致优化器没法直接按索引条件做等值匹配可能就把连接条件当成普通表达式处理结果就是本来该走索引的变成了全表扫描。我不止一次看到有人排查慢SQLEXPLAIN一看索引没用上最后才发现是两个表collation不一致导致的。另外0900系列排序规则对特殊字符的相等性判断和general_ci不完全一样某些别名、特殊符号、带重音字符的排序在两套规则下结果可能根本不同。迁移时的建议很明确不要指望全库默认值帮你兜底直接梳理一遍核心业务表的DDL统一成同一种排序规则再上线。这个动作放在迁移初期做成本最低。1.3 严格模式和ONLY_FULL_GROUP_BY老代码的第一个报错现场8.0默认sql_mode里带STRICT_TRANS_TABLES、ONLY_FULL_GROUP_BY、NO_ZERO_IN_DATE、NO_ZERO_DATE、ERROR_FOR_DIVISION_BY_ZERO等一堆选项。5.7其实默认也有但很多老实例上线时被人为改成了宽松模式或者从古老版本一路升级上来实际跑代码的风格一直是“怎么都能塞进去”。所以迁移8.0以后最先炸的往往不是性能而是一堆“以前能跑、现在直接报错”的SQL。最经典的是SELECT name, COUNT(*) FROM member GROUP BY age这种写法name没有出现在GROUP BY里8.0直接拒绝执行还有往int列插入超长数值5.7只是warning然后截断8.0直接报错日期字段插入0000-00-00也一样被拒。这类问题真正难搞的地方在于测试环境不容易暴露。开发测主流程都是正常数据只有报表、数据修复脚本、批量任务这类会在边界数据上跑的SQL才会在线上忽然给你来个“启动失败”。处理建议是先别想着把sql_mode全局改宽松那相当于把脏数据问题重新请回来。正确做法是逐条修SQL然后把sql_mode固化到实例配置里并且纳入监控防止某台从库或某个测试实例因为配置不一致造出一套和线上完全不同的行为。这里可以给一个简单对照常见老写法5.7宽松模式下8.0默认模式下insert超长字符串warning后截断直接报错group by外查询非聚合列返回随机值报ONLY_FULL_GROUP_BY错误日期字段写0000-00-00允许写入默认拒绝除数为0warning后结果为NULL直接报错2. 优化器“换人”了我最信任的几条5.7经验全都失效2.1 hash join等值连接不建索引反而“更聪明”了8.0.18开始优化器在等值连接无法有效利用索引时可以选择hash join。这条特性本身是好事尤其对OLAP风格的查询问题在于它彻底颠覆了很多5.7时代的调优经验。以前我们处理大表JOIN小表核心思路是让小表驱动大表大表连接字段必须建索引这样才能一路嵌套循环走下去。到了8.0优化器有可能认为hash join代价更低直接对大表做一次全表扫描构建hash表然后另一个连接端再扫描。如果连接字段的字符集或排序规则不一致或者存在隐式类型转换hash join还可能要额外做转换内存开销和临时落盘都可能暴涨。我实际排查过一个案例某订单统计场景单表几千万行级别SQL完全相同5.7执行两秒多8.0执行十几分钟。EXPLAIN里出现了Using join buffer (hash join)优化器认为它选的方案更优但实际运行时hash表落盘性能反而不如老计划。这类问题不能靠“禁用hash join”一劳永逸更实用的处理是检查连接字段的类型、字符集、排序规则是否完全一致消除隐式转换如果优化器还是选错再考虑通过索引或FORCE INDEX把执行计划引导回嵌套循环。记住一个关键点hash join是在“没办法用索引”时才容易被选中的把连接字段的索引做对优化器自然会变回去。2.2 IN子查询和派生表不再按5.7的经验走5.7时代很多DBA会教你把IN改成EXISTS或者把一个子查询改成JOIN大家普遍觉得这样更快。到了8.0优化器对IN子查询的semi-join物化处理已经很成熟很多时候它自己就能生成不错的计划反而是那些“人工改写”的SQL出了问题。我见过一个挺典型的反面案例有人为了让一条IN查询更快改写成INNER JOIN并加上DISTINCT去重。5.7里这个改写确实有提升但到8.0后因为优化器的等价变换逻辑变了改写后的SQL反而被评估成一种更昂贵的派生表物化路径结果没快反而慢了一截。更麻烦的是这类改写常常会改变结果集语义出现重复行翻倍的问题排查起来特别费劲。我的建议是进了8.0先把那些基于5.7经验的“SQL改写手册”全部重新审视一遍。遇到IN慢查询第一件事是用EXPLAIN看它到底走了semi-join还是物化再决定要不要动SQL。很多老经验在新优化器下已经不成立了硬套反而帮倒忙。2.3 直方图与统计信息升级后手动ANALYZE的节奏变了8.0支持直方图这是优化器能力的一个重要升级但默认并不会自动给所有列建直方图。很多DBA从5.7带过来的习惯是“大版本升级完先跑一轮全库ANALYZE TABLE”这个动作在8.0下要谨慎。原因有两层第一8.0的统计信息存放在数据字典里采样和持久化机制跟5.7不完全一样全库一次性ANALYZE可能造成大量表的数据字典更新引发短暂的锁竞争和连接阻塞第二如果某个表已经存在手动创建的直方图ANALYZE的行为会和统计信息刷新叠加优化器读到的“选择性”可能变化很剧烈执行计划从好变差、从差变好都有可能。更值得警惕的是8.0下很多“绕过DDL直接改数据”的批量操作不会自动触发统计信息更新。比如一行一行UPDATE、DELETE后如果表数据分布已经明显变化优化器还拿着旧统计信息预估行数就可能选错索引。遇到这类场景针对单表做一次ANALYZE TABLE是合理的但别搞成全库统一刷。实践上我倾向于升级后先只对核心大表做受控的统计刷新观察一两天慢日志再决定要不要扩大范围。3. 存储层静默改版redo、undo、临时表和系统表空间全换了名字和格式3.1 8.0.30的redo日志改名事件8.0.30之前redo日志文件是ib_logfile0、ib_logfile1这样的固定命名8.0.30之后redo日志被统一放到了#innodb_redo目录下文件名也变成了#ib_redoNN一类。这个变化本身是InnoDB为了更灵活管理redo文件做的架构调整但对运维脚本来说简直是一场“定时炸弹”。很多团队的监控脚本、备份脚本、日志清理脚本都是按老目录和老文件名校验的。升级到8.0.30之后脚本可能找不到文件监控项直接失效或者清理逻辑把不该动的目录漏掉。更隐蔽的是老参数innodb_log_file_size和innodb_log_files_in_group在8.0.30之后语义变了真正生效的是innodb_redo_log_capacity如果你还在老配置文件里改这两个参数不仅不会达到预期效果日志里还可能出现参数废弃的warning。这里给一个非常具体的建议在升级到8.0.30或更高版本之前先全局搜一遍所有包含ib_logfile关键字的脚本、告警配置、自动化巡检项逐个改成新路径。别相信“我的脚本只读参数不读文件”这种话实际排查里我见过太多因为监控哑掉而让redo空间撑爆的案例。3.2 临时表和ibtmp1排序慢从“内存不足”变成“磁盘类型”8.0内部临时表的实现从MEMORY引擎换成了TempTable引擎内存不够落盘时也不再写MyISAM格式而是用InnoDB格式临时表。这带来的最直接变化是你在5.7里对临时表空间的理解基本要推翻。以前出现Using temporary大家第一反应是tmp_table_size设太小内存里的临时表不够用落到磁盘就慢。8.0里除了这个因素你还要留意临时文件写到哪去了。默认tmpdir如果放在系统盘或者某些环境是挂载在/dev/shm这种内存盘一旦大排序、大GROUP BY触发落盘可能直接把那个分区的空间写满。另一个老生常谈的坑是ibtmp1文件。8.0的临时表空间文件尺寸增长后不会自动收缩长时间跑大查询、大事务的系统ibtmp1涨到几十上百G是很常见的而且等查询结束它也不会自动瘦身可能只能等实例重启才能释放。排查的时候记得把ibtmp1当成“只记录峰值不自动回收”的文件来看别指望它像普通binlog那样自己清理。对策很简单监控tmpdir和ibtmp1的增长率独立挂载临时目录SQL层面尽量消灭大排序和大临时表。3.3 mysql.ibd、undo和双写缓冲的新布局8.0的系统表空间不再是那个膨胀到几十GB的ibdata1数据字典和系统表大部分挪到了mysql.ibd。以前5.7时代有些DBA会通过“重建ibdata1”来收缩系统表空间这个土方子在8.0里千万不要再用因为那基本等同于删掉数据字典实例直接起不来。undo表空间的布局也变了。5.7能把innodb_undo_tablespaces配成多个文件分散放8.0初始化后默认2个undo表空间数量调整和文件管理方式都有新规则。升级时如果老配置里带了旧的undo路径或数量可能被新版本忽略或者产生告警。8.0.20之后双写缓冲也不再沿用老文件布局而是放到#innodb_dblwr目录下。如果你的备份恢复、数据文件巡检脚本里还在找老的双写文件路径升级后就会失效。这类问题的共同点是一切跟“文件名、目录路径”相关的运维经验都要拿官方文档重新对一遍千万别拿5.7手册直接套8.0。4. 复制链路的变化从库追不上和半同步超时都换了症状4.1 binlog默认ROW排障方式完全不同8.0默认binlog_formatROW很多从5.7带着STATEMENT或MIXED习惯过来的团队在排查主从不一致时会被狠狠教育一次。ROW模式下binlog里记录的不再是原始SQL而是每一行数据变更前和变更后的镜像。以前排查数据不一致可以对着binlog里的SQL语句在从库手动重放一遍看结果现在这么做基本没用你手工执行的结果和从库应用的结果都可能对不上因为从库应用的是行镜像触发器、存储过程、函数在复制链路上根本不会二次执行。我们踩过的真实场景是主库某条数据被修复后从库的数据和主库不一致但靠SQL日志完全看不出原因最后只能用mysqlbinlog --base64-outputDECODE-ROWS把行事件解出来对比前后镜像才定位到问题。另外8.0.20开始支持binlog事务压缩。压缩是好东西但如果你有一些基于binlog解析的数据同步工具或审计脚本版本跟不上就会直接不认识压缩事件表现为解析中断或跳过一堆事务。升级前务必把所有binlog消费端、解析工具全部核对版本做好兼容性测试。4.2 并行复制配置和参数迁移很多5.7实例的并行复制配的是slave_parallel_typeDATABASE因为不同库的事务才能并行。到了8.0默认的并行复制策略已经是基于LOGICAL_CLOCK能在一个库内并行应用没有锁冲突的事务这样效率高得多。但麻烦在于如果迁移时直接把老的my.cnf复制过来配置还是DATABASE模式从库并行能力就废掉大半大事务一多延迟立刻飙升。更隐蔽的是老配置里那些replicate-wild-ignore-table之类的过滤规则在8.0下的语义也可能有变化。如果过滤规则写得不准可能把不该过滤的表也滤掉造成从库与主库结构漂移。我建议升级8.0复制链路时参数不要无脑搬运而是重新根据新版本默认值来写大事务造成延迟时也别急着调大slave_parallel_workers先看并行应用的锁等待状态很多时候瓶颈不是线程数是行锁竞争。4.3 从库重建的姿势要更新server_uuid和克隆插件5.7时代重建从库最常见的方式是拿物理备份直接拷贝到新机器然后change master。这个流程在8.0里有一个特别容易忽略的雷物理备份通常会连带auto.cnf里的server_uuid一起带走多台从库如果都从同一份备份克隆就会出现多个实例使用同一个server_uuid。这个问题的诡异之处在于主库初期看不出任何异常因为连接和复制都能正常建立。但等你要做选主、一主多从切换或者某些管理工具基于server_uuid做实例识别时就会开始出现错乱。排查起来极其费神因为日志不会直接告诉你“UUID重复了”。8.0官方提供了CLONE插件来做在线克隆它会在目标实例上重新生成必要的实例标识比老办法规范得多。如果还是习惯用物理备份手工重建从库至少要在恢复完成后主动删除或替换auto.cnf让实例重新生成server_uuid。这个动作建议写进SOP否则早晚出事。5. 在线DDL变“瞬时”后真正的隐患才开始显现5.1 INSTANT加列快是真的隐患也是真的8.0的INSTANT DDL能让你在秒级内给一张大表加列第一次用的时候真的会觉得“这功能太香了”。但深入了解之后才发现快是有代价的。INSTANT加列不是完全无痕它会基于表当前的行格式做“元数据级变更”本质上是在行记录里扩展一个“新列的空值版本”。如果一张表被反复执行INSTANT加列行格式里会积累大量“历史列信息”表碎片会增加全表扫描的代价也会悄无声息地上升。我自己碰到过一个案例某业务表一年内被各种需求加了差不多20个字段大部分是INSTANT加的后来一条简单查询忽然变慢EXPLAIN里走的是全表扫描优化器估算代价很高。整理完表之后扫描代价明显下降。所以这里有一条经验8.0里加列虽然爽但要记住定期巡检那些频繁加列的表必要时执行ALTER TABLE ... FORCE或OPTIMIZE TABLE整理一次。把INSTANT DDL当成“可以频繁用但不能无限用”的工具会稳妥很多。5.2 临时目录决定了大表DDL的生死8.0执行ALGORITHMCOPY或部分INPLACE重建时会在tmpdir下生成临时文件。很多DBA都知道临时表会用到tmpdir但忽略了在线DDL也一样依赖这个目录。你在生产环境跑一个大表ALTER如果tmpdir所在分区空间不足DDL会直接报磁盘满甚至把整个实例的写入拖垮。我帮别人排查过一个真实事故某系统在给一张快上亿行的表加索引执行到一半报错“No space left on device”但业务库所在的数据盘明明还剩很多空间最后才发现问题出在系统盘/tmpdir上。当时的tmpdir目录就在根分区根分区剩余空间本身就不多一个重建操作直接把它写满了。对策归纳起来就是三条tmpdir单独挂载到有足够空间的数据盘别放系统盘也别放内存盘执行大DDL前检查磁盘水位给DDL设置合理的锁等待超时时间避免它无限期卡住。5.3 在线DDL照样会被MDL锁卡死“在线DDL”这个名字很容易让人误以为它完全不影响业务实际上8.0的DDL在整个执行过程中还是需要获取元数据锁MDL只是持有时间和锁粒度比5.7优化了不少。真正卡死系统的场景经常是这样某个长事务占着表的MDL读锁然后你启动了一个ALTERALTER排队等写锁结果后续所有对该表的DML也全部排队最后整个连接池被打满应用瞬间雪崩。这个故障链条非常典型而且特别难排查因为表面上看“ALTER没跑完”只是慢实际是“整个表不可用”。如果执行DDL之前不检查是否有长事务或锁等待生产事故基本就是时间问题。我现在每次执行DDL前都会先查information_schema.innodb_trx和sys.schema_table_lock_waits确认没有长事务和锁链了再动手。特别是那种业务高峰期有人开了事务没提交的情况DDL一排队后续所有查询都得跟着陪葬。另外说一个容易被忽略的细节8.0的在线DDL在复制环境中主库执行完很快但从库应用DDL时同样要拿元数据锁如果从库上有正在执行的长查询从库的DDL一样会卡住进而拖垮从库的复制线程。升级到8.0后很多人只盯着主库的DDL表现忘了检查从库的锁等待这也是一种“新暴露的坑”。最后补一点个人体会从5.7迁到8.0最大的障碍其实不是文档差而是你脑子里那套“老经验”太根深蒂固。我现在的习惯是每升一个版本先把配置文件里所有参数对照官方文档过一遍再重点检查脚本里写死的文件路径和目录名最后才是跑业务SQL验证。MySQL 8.x已经是绕不开的事实标准了与其等它在生产环境给你上一课不如提前把这篇里提到的这些坑当成升级前的体检清单一项一项过一遍。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。