MySQL 8.4 升级踩坑记录
去年底立项把核心库从 8.0.36 升到 8.4 LTS,理由很朴素:8.0 的支持周期到头了,再拖就要裸奔。整个过程测试环境跑了一个月,业务回归全绿,自我感觉准备充分。然后灰度第一天就中了两枪。
事后看,两个坑文档里都写了。但「文档里写了」和「你意识到它会打到你」之间隔着很远的距离。按时间线记录一下。
第一枪:mysql_native_password 没了
升级后两小时,一个边缘服务开始报连接错误:
Authentication plugin 'mysql_native_password' cannot be loaded
8.4 把 mysql_native_password 从「默认禁用」改成了「彻底移除前最后的警告状态」,默认不再加载。我们主干服务早就切到 caching_sha2_password 了,测试环境回归当然也没问题。中招的是一个三年没动过的内部报表服务,用的还是老版本的 PHP mysqli 扩展,配置里写死了认证插件。
这种服务的特点就是:没人维护、不在回归清单里、但挂了会有人找上门。
临时解法是在 8.4 里显式把插件加回来:
[mysqld]
mysql_native_password = ON
但这个开关在 9.x 里是真的没有了,等于把炸弹往后挪。所以当周还是把那个报表服务的依赖升了——顺便发现它用的 PHP 版本也早就 EOL 了,工单越修越多,这是后话。
教训:升级影响评估的盲区永远在「没人认领的存量服务」。后来我们用 performance_schema 把所有还在用老认证插件的账号扫了一遍,建议大家升级前都做一次:
SELECT user, host, plugin FROM mysql.user
WHERE plugin = 'mysql_native_password';
这个一行 SQL 能避免的事故,比任何回归测试都划算。
第二枪:utf8mb3 和 utf8mb4 的隐式转换
第二天下午,DBA 群里贴了一张慢查询截图:一个平时 20ms 的查询,偶发飙到 4 秒。explain 一看,走全表了。
查询本身很普通,跨两张表 join,条件列两边都有索引。问题在字符集:一张表是上古时期建的,列是 utf8mb3(也就是老 utf8),另一张是新表,utf8mb4。join 的时候发生隐式字符集转换,转换函数包在列上,索引废了。
-- 简化后的形态
SELECT ...
FROM legacy_order o -- order_no: utf8mb3
JOIN new_payment p -- order_no: utf8mb4
ON o.order_no = p.order_no;
-- 8.0 的执行计划里 o 这边还能走索引
-- 8.4 优化器选择了另一个 join 顺序,转换落到了有索引的一边
严格说这不是 8.4 的 bug。隐式转换一直存在,8.0 时代优化器恰好选了一个对我们有利的 join 顺序,掩盖了问题;8.4 的优化器代价估算有调整,选了另一个顺序,问题就露出来了。老版本的「正常」有时候只是运气好,升级把运气抽走了。
短期处理是给那条 SQL 加 hint 固定 join 顺序。根治只有一条路:把存量 utf8mb3 的表全部转成 utf8mb4。我们盘了一下,这种表还有 47 张,都是 2018 年之前建的。转换用 ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4,大表走 gh-ost,排了一个月的计划慢慢洗。
这件事还附赠一个发现:测试环境的这几张表,不知道哪年被谁顺手统一成 utf8mb4 了,和线上 schema 不一致——所以测试环境永远复现不了。schema 漂移检查后来也进了升级 checklist。
灰度方案本身
有同学私下问过怎么灰度数据库升级,简单说下我们的做法,不一定是最优解。
利用主从架构:先升一台从库,挂到集群里只承接读流量,观察一周——重点看复制延迟、慢查询数量、错误日志里的新面孔。然后逐台升完所有从库,再做主从切换,让 8.4 的节点当主,原主降级成从库并最后升级。整个窗口拉了三周。
切换前做了两手准备:一是逻辑备份加 binlog 位点记录,保底;二是保留了一台 8.0 从库在集群外跟着复制,作为「快速逃生通道」——真出大事可以把它提成主。8.4 从库往 8.0 复制属于高版本往低版本复制,官方不保证支持,所以这台逃生节点只在切换后保留了 48 小时,确认平稳就升级归队了。
事后看最有价值的动作是「从库先扛一周读流量」。两枪里的第二枪(join 顺序变化)就是这一周里暴露的,当时只影响从库的读,没伤到写路径,处理起来从容很多。如果直接主从一起切,那就是另一个故事了。
没踩到但值得一提的
查资料过程中记下来的几个 8.4 变化,我们没中招,但场景普遍:
复制相关的默认值变了不少,replica_parallel_workers 默认 4(以前是 0),对从库延迟敏感的业务最好升级前后对比观察一下;做双向复制或者级联复制的,注意 GTID 相关参数的默认值变化。
InnoDB 的 innodb_buffer_pool_in_core_file 这类参数的语义有微调,用自动化巡检脚本管参数的团队,脚本要跟着过一遍,我们有两个巡检项升级后一直误报,修了半天。
expire_logs_days 这种废弃多年的参数在 8.4 里是真删了,my.cnf 里如果还留着,实例直接起不来。建议升级前拿 8.4 的 mysqld 对现有配置做一次 --validate-config,这个功能 8.0.16 就有,但好像没什么人用。
mysqld --validate-config --defaults-file=/etc/my.cnf
升上来到底图什么
光写坑显得这次升级很冤,其实收益也是实打实的,列两个上线后观测到的。
并行复制默认开启后,大促压测时从库延迟从原来动辄十几秒压到了两秒以内,这个以前在 8.0 上要手动调一堆参数才能达到,现在出厂就是这个状态。对我们这种读写分离重度依赖从库的架构,等于白捡一次架构升级。
另一个是不太被提起的:8.4 作为 LTS,承诺补丁周期内不引入行为变化(bug fix only)。8.0 时代最烦的就是小版本升级也可能踩到行为变化,每次打补丁都得做一轮回归。现在小版本可以放心滚动更新,运维成本降了一截。说起来这才是 LTS 模式对生产环境最大的意义——可预期,比任何新特性都值钱。
总结
整个升级周期两个半月,真正花在「升级动作」上的时间不到 10%,剩下都在处理存量:没人管的老服务、上古字符集、漂移的 schema、过时的配置参数。所以要说有什么总体经验,就是升级项目的本质是一次被迫的技术债清算,工作量估算要按债务规模来,不要按「执行一次滚动升级」来。
我们算是付了学费但没出大事故。两枪都打在灰度批次上,没伤到核心流量——分批灰度这件事,怎么强调都不过分。