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

资讯详情

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

MySQL迁移GoldenDB必改参数:sql_mode与ONLY_FULL_GROUP_BY踩坑复盘

MySQL迁移GoldenDB必改参数:sql_mode与ONLY_FULL_GROUP_BY踩坑复盘 说个我自己的真实经历。上个月帮一个项目做数据库迁移源端是 MySQL 8.0目标端是三节点部署的 GoldenDB。前期测试环境一切正常切了生产之后业务方火急火燎地找我某个聚合查询直接报错应用日志里清清楚楚写着Expression #2 of SELECT list is not in GROUP BY clause。我一开始还以为是 GoldenDB 的兼容性问题查了半天发现根子出在一个参数上sql_mode。准确地说是sql_mode里那个默认开启的ONLY_FULL_GROUP_BY。如果你也准备把业务从 MySQL 迁到 GoldenDB这个参数记得改本文就把这次迁移里踩到的参数坑完整复盘一遍顺便给一份迁移前值得过一遍的参数清单。1. 先搞懂 GoldenDB 的分布式到底改了什么为什么同样的 SQL 会“不讲道理”迁移前我对 GoldenDB 的理解比较粗浅以为它只是 MySQL 的“加强版”协议兼容、SQL 语法兼容那业务 SQL 应该原封不动就能跑。但真正上手才发现分布式数据库和单机数据库的差别远不止“多几台机器”这么简单。1.1 一台服务器 vs 一组集群SQL 执行路径的变化单机 MySQL 时代一条SELECT语句从客户端发过来MySQL 实例自己完成解析、优化、执行数据就在本机的存储引擎里。你改参数改的也就是这一台实例的行为影响范围是确定的。GoldenDB 不一样。它通常由计算节点Coordinator和数据节点Data Node组成数据按照分片规则打散到多个数据节点上。我这边三节点部署实际上每个节点上都跑着计算和存储服务数据被水平切分后分布在不同节点上。一条 SQL 进来后计算节点要先生成分布式执行计划把一条语句拆成多个子任务下发到不同数据节点去执行再把结果汇总回来。这个变化会带来两个直接后果第一原来在单机上一个节点就能搞定的事情现在要经历“拆-算-并”三个阶段。最典型的就是聚合查询比如SELECT region, COUNT(*) FROM orders GROUP BY region单机 MySQL 直接扫全表就出结果了GoldenDB 会先在每个数据节点上各自做一次分组计数部分聚合然后把各个节点的中间结果汇总到计算节点再合并一次。这就是大家常说的“两阶段聚合”。第二SQL 语义的检查更严格了。单机 MySQL 里很多“睁一只眼闭一只眼”的写法在分布式执行计划面前完全行不通因为计算节点无法为跨节点的数据推断语义。1.2 参数在分布式环境下的“放大效应”单机环境下你改一个参数影响的是一台实例的所有连接到 GoldenDB 里你改一个参数可能要同时考虑计算节点和数据节点两层。更麻烦的是同一个参数在不同节点上的取值如果不一致就会出现“这次连接报错下次连接不报错”的灵异现象。我这次踩坑的过程就很典型测试环境的 GoldenDB 是部署同事给配好的参数都是默认值生产环境虽然也是三节点但某个计算节点上的sql_mode配置是从模板导入的多带了个ONLY_FULL_GROUP_BY。结果就是测试环境跑得好好的 SQL一到生产就报错。排错排了半天才发现是两个环境参数不一致。所以迁移到 GoldenDB 时一定要把参数当“全局资源”来对待而不是像单机 MySQL 那样只关心某一个实例上的取值。先确认全集群参数一致再谈业务适配。1.3 GoldenDB 兼容 MySQL 的边界在哪GoldenDB 确实兼容 MySQL 的大部分语法特别是 DML 和 DDL 的基础能力我迁移的业务表结构基本没改就能建上。但它毕竟不是 MySQL有几个地方需要特别注意存储引擎层面GoldenDB 对存储引擎有自己的一套管理方式你建表时可以指定ENGINEInnoDB但实际数据分布、分片规则是集群层面决定的。分布式事务跨节点的多表更新会走分布式事务对隔离级别、锁行为的处理逻辑和单机不同。执行计划EXPLAIN的结果会比 MySQL 复杂能看到下推到数据节点的执行细节但如果你不熟悉分布式执行计划很难一眼看出性能瓶颈。也正因为 GoldenDB 保留了 MySQL 的外壳但内核变成了分布式架构很多和 SQL 语义强相关的参数比如sql_mode就会出现“同一个参数、两套表现”的情况。这也是我写这篇博文的核心原因别以为参数兼容就等于行为兼容。2. 主角登场sql_mode 里那个 ONLY_FULL_GROUP_BY迁移后第一个要改的这一章我们聚焦sql_mode。它应该是所有从 MySQL 迁到 GoldenDB 的项目里最容易被忽略、又最可能造成线上故障的参数。2.1 那个让我排查了半天的报错现场场景还原一下。业务表是一张订单表orders表里有user_id、order_no、amount、create_time等字段。应用里有一条统计 SQL大概是这样的SELECT user_id, order_no, COUNT(*) FROM orders WHERE create_time 2024-01-01 GROUP BY user_id ORDER BY NULL;先声明一下这条 SQL 写得不严谨。order_no既不在GROUP BY里也不是聚合函数按 SQL 标准它是不该出现在 SELECT 列表里的。但业务代码是好几年前写的一直跑在 MySQL 5.7 上当时那个实例的sql_mode被人为调整过去掉了ONLY_FULL_GROUP_BY所以这条 SQL 从来没报过错。迁移到 GoldenDB 后我执行同样的 SQL直接抛了ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column testdb.orders.order_no which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_modeonly_full_group_by这个报错信息本身就是答案——sql_mode里带着ONLY_FULL_GROUP_BY而 GoldenDB 默认配置里它是开启的。业务方早年在单机 MySQL 上关掉这个参数等于给这条不严谨的 SQL 开了绿灯结果这个“绿灯”在 GoldenDB 上没生效。2.2 ONLY_FULL_GROUP_BY 在分布式聚合中的坑比想象中深有人会问那我把 GoldenDB 的sql_mode也改成和当年 MySQL 一模一样去掉ONLY_FULL_GROUP_BY问题不就解决了吗理论上是的但这里有一个分布式数据库特有的坑即使你关掉了ONLY_FULL_GROUP_BYGoldenDB 在生成分布式执行计划时对于 SELECT 列表中无法确定语义的非聚合列依然可能拒绝执行。为什么因为order_no是随user_id变化的非聚合字段在单机 MySQL 宽松模式下它直接取分组内第一行的值就可以了但在 GoldenDB 里数据分布在不同的数据节点上计算节点做第二阶段的汇总时面对各个数据节点传来的中间结果它不知道应该听谁的。取第一个节点的还是取某个节点的随机值这个语义在分布式执行引擎里没法定义清楚所以部分版本的 GoldenDB 即使sql_mode里没有ONLY_FULL_GROUP_BY也会在生成执行计划时直接报错。这也是很多人迁移后反复踩坑的原因改完sql_mode发现报错变了从 1055 变成了解析执行计划失败之类的错误然后就开始怀疑是 GoldenDB 不兼容。我的建议是分两步走第一步当然是把sql_mode里的ONLY_FULL_GROUP_BY去掉尽可能缩小和源库的行为差异。这是标题里说的那个参数也是大多数情况下能解决 1055 报错的关键。第二步如果去掉之后执行计划还是生成不了那就要回到 SQL 本身把 SELECT 列表里不在 GROUP BY 中的非聚合列改成聚合函数比如MAX(order_no)、ANY_VALUE(order_no)或者干脆去掉。这不是 GoldenDB 的错是 SQL 本身不符合规范只是单机数据库帮你兜了底分布式数据库不兜了。2.3 怎么改Session、Global、配置文件三层操作先把查看和临时改的方法写清楚这些在任何 MySQL 兼容的实例上都通用。-- 查看当前生效的 sql_mode SELECT GLOBAL.sql_mode; SELECT SESSION.sql_mode; SHOW VARIABLES LIKE sql_mode; -- 临时调整当前连接SESSION 级只对当前连接生效 SET SESSION sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION; -- 临时调整整个实例GLOBAL 级对所有新连接生效 SET GLOBAL sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION;注意上面的字符串是我根据自己的情况整理出来的在 GoldenDB 默认sql_mode的基础上把ONLY_FULL_GROUP_BY拿掉。建议你改的时候也先SELECT GLOBAL.sql_mode把当前值查出来然后照着原值把ONLY_FULL_GROUP_BY删掉不要直接手工敲一长串否则容易漏掉一些你本来需要的模式项比如STRICT_TRANS_TABLES如果也被你丢了后续插入异常数据时会出现告警而不是报错反而掩盖问题。关于持久化GoldenDB 和单机 MySQL 最大的不同是它不是改一个my.cnf再restart就完事。我这边的情况是管理平台上可以统一下发配置参数也可以登录到每个数据节点的实例上修改配置文件。但无论走哪条路都有两个细节必须注意一是要保证所有计算节点和数据节点的sql_mode都改成同一个值。GoldenDB 前面可能有接入层网关你连上去可能随机路由到不同的计算节点如果只改了一个节点下一次连接落到另一个节点上问题依旧。二是配置下发后一般需要滚动重启或者至少让新配置重新加载。在业务低峰期做先拿一台节点验证确认没问题再推全集群。2.4 改了之后怎么验证改完参数别急着说“搞定”一定要用真实业务 SQL 验证一遍。我自己的验证步骤是先执行典型的聚合查询看是否还有 1055 报错。执行SHOW VARIABLES LIKE sql_mode确认当前会话拿到的是修改后的值。用EXPLAIN跑一遍刚才报错的 SQL看分布式执行计划能否正常生成。如果能看到类似gather、shard、aggregate之类的关键字说明走了两阶段聚合执行计划没问题了。最后跑一个真实业务接口回归让应用自己去执行刚才的查询路径确认应用层日志不再刷这个错误。这里还有一个容易被忽略的点应用侧的连接池通常是长连接。如果应用用的是 Druid、HikariCP 这类连接池SET GLOBAL只对新连接生效已经存在的连接可能还保留着旧的会话级sql_mode。所以验证时最好让应用重启连接池或者等旧连接超时回收后自动建新连接否则你代码里看到的可能还是“明明改了却还报错”。3. 和 sql_mode 一起踩的还有四个参数大小写、超时、包大小、字符集sql_mode只是开头。迁移 GoldenDB 之后被我一起发现问题的还有四个参数建议你在迁移前就过一遍别等上线后再一个个补。3.1 lower_case_table_names表名大小写导致“找不到表”这个坑在新老环境表名规范不统一时非常致命。MySQL 在 Linux 上默认lower_case_table_names0也就是区分表名大小写在 Windows 上默认是1不区分。如果业务之前跑在 Windows 的 MySQL 上表名是Orders、UserInfo这种驼峰风格迁到 Linux 上的 GoldenDB 之后lower_case_table_names又是默认的0那代码里写的select * from orders就找不到Orders这张表直接报Table doesnt exist。GoldenDB 作为分布式集群对表名大小写的一致性要求更严格。如果不同数据节点上的元数据里表名大小写不统一跨节点查询就会出现“部分节点能找到、部分节点找不到”的诡异现象。我的建议是迁移前把所有表名、字段名统一成小写lower_case_table_names保持和 GoldenDB 默认一致通常是0不要轻易改成1。因为1意味着把大写表名自动转成小写存储如果应用代码里还有大小写混用的 SQL反而可能因为转换规则不一致而踩新的坑。统一小写是一劳永逸的做法。3.2 max_allowed_packet大字段和批量导入的最强拦路虎这个参数控制的是单次报文的最大长度。单机 MySQL 时如果业务经常往表里写比较大的文本、JSON、或者一次插入几千行max_allowed_packet设置小了会直接报Packet too large。GoldenDB 因为是分布式架构一条跨节点的 SQL 可能涉及多个节点之间的数据传递报文开销只会比单机更大所以这个参数要舍得给大一点。我当时直接设成了 256M比源库的 16M 大了不少。如果你的业务里有批量写入、大字段LONGTEXT、JSON类型或者经常导入导出数据建议至少 64M 起步视数据量往上调。这里给个排查技巧如果应用日志里出现Got a packet bigger than max_allowed_packet bytes不要犹豫先查这个参数当前值再结合业务最大单条数据量算一下。单条数据几十 KB 的情况下默认的 16M 按理说够用但如果一次写入几百行拼成一个 SQL报文大小就可能突破上限。GoldenDB 下因为多了节点间通信同样的业务在 MySQL 不报错到了 GoldenDB 就可能踩线。3.3 wait_timeout 和 interactive_timeout连接池被数据库踢掉分布式环境下应用通过连接池维护的数据库连接经过网关再打到具体的数据节点。如果wait_timeout非交互连接超时或者interactive_timeout交互连接超时设置得太短空闲连接会被数据库侧主动断开。应用连接池里的连接就成了“死连接”下次从池里拿出来用时底层 socket 已经断了就会报Communications link failure之类的错误。这种问题的迷惑性在于你直接拿命令行客户端连上去执行 SQL 是好的但应用一跑就报错因为连接池里的空闲连接已经被服务端关了。我建议 GoldenDB 上把wait_timeout和interactive_timeout都设成28800秒8 小时同时应用侧连接池要配置连接保活和空闲回收机制。比如 HikariCP 的connectionTestQuery、Druid 的testWhileIdle这样才能让连接池主动把失效连接剔除掉而不是等服务端断开后再被动发现。3.4 字符集参数乱码、索引长度超限都跟它有关源端 MySQL 如果是utf8mb4到了 GoldenDB 如果默认字符集是utf8mb3也就是常规意义上那个utf8中文显示没问题但遇到 emoji 表情、生僻字就会变成问号更麻烦的是utf8mb3的索引长度限制比较小建表时明明在 MySQL 上能建出来的索引到 GoldenDB 上可能直接报Specified key was too long。所以建库建表时要明确指定字符集和排序规则CREATE DATABASE testdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;同时确认集群层级的character_set_server和collation_server也统一成utf8mb4。迁移时最好写个脚本把建表语句里的字符集显式带上不要依赖默认值。3.5 一张表总结迁移前要过的参数清单参数单机 MySQL 常见配置GoldenDB 迁移建议踩坑表现sql_mode按业务调整可能去掉ONLY_FULL_GROUP_BY以 GoldenDB 默认值为基准去掉ONLY_FULL_GROUP_BY全集群一致GROUP BY 查询报 1055 或执行计划生成失败lower_case_table_namesLinux 0 / Windows 1建议与源库保持一致表名统一小写Table doesnt existmax_allowed_packet16M 默认至少 64M按业务批量写入情况调整Packet too largewait_timeout/interactive_timeout默认 28800建议保持 28800别为省连接数改小Communications link failurecharacter_set_server/collation_serverutf8mb4统一 utf8mb4 / utf8mb4_general_ci中文乱码、索引长度超限表格里这几项是我实际项目中碰到过的其他的参数比如innodb_buffer_pool_size、max_connections这类资源型参数GoldenDB 有自己的集群调度逻辑一般不需要像单机 MySQL 那样精细调优先把上面这张表过了基本能躲开绝大多数迁移初期的接口级故障。4. 迁移前五分钟参数体检我是怎么提前发现这些坑的很多人喜欢等迁移完、压测出问题了再回头排查参数。我的习惯是反过来的——数据还没迁先把两边参数拉出来比一遍很多坑在迁移前就能暴露。下面是我这次用的办法成本很低效果很好。4.1 用 DBeaver 连上 GoldenDB 后先别急着导数据GoldenDB 兼容 MySQL 协议所以 DBeaver 里直接选 MySQL 驱动就能连不需要装什么特殊的插件。连接信息里填 GoldenDB 计算节点的地址和端口驱动选择 MySQL 8.0 的驱动版本就行。这一点很多刚接触 GoldenDB 的同事不知道以为要专门找 GoldenDB 的驱动其实不用。连上之后先不看表数量先看参数。我一般会同时连源端 MySQL 和 GoldenDB各开一个 SQL 窗口跑同样的命令SHOW VARIABLES;然后把输出导出来做 diff。只看差异项重点看我们前面讨论的那几个参数。这一步相当于“验血”先确认两边的基本配置是否在同一频道上。4.2 SQL 层体检抓一批业务 SQL 跑 EXPLAIN参数 diff 只能发现配置差异但配置差异最终是通过 SQL 行为暴露的。所以第二步我建议把业务方提供的、生产环境 Top N 的 SQL 抓一批出来在 GoldenDB 上一条一条执行EXPLAIN。不用真的跑全量数据EXPLAIN只生成执行计划不执行查询快得很。重点看哪几条 SQL 报错哪几条生成了跨节点的执行计划。这时候sql_mode的问题就会原形毕露比上了生产再被报警吓一跳强一百倍。我这次迁移里就是在这一步重新验证了那条 GROUP BY SQL发现ONLY_FULL_GROUP_BY的问题后提前改掉了参数后面正式切换时业务方甚至都没感觉到曲线波动。4.3 数据层校验用分组查询验证聚合语义最后数据核对阶段不要只做SELECT COUNT(*)这种简单对比。聚合查询才是最容易暴露问题的场景。我会针对每张核心表在源端和 GoldenDB 上分别执行相同的分组统计 SQL比如按月统计订单量、按用户统计消费总额然后对比结果。为什么因为分布式环境下两阶段聚合的中间结果合并逻辑在某些边界情况下可能出现和单机 MySQL 不一致的结果比如分组键为 NULL 的行、浮点数累加时的精度问题。这种问题靠COUNT(*)对不出来的必须用真实业务语义的分组查询去验。如果两边结果有差异优先看是不是参数的锅比如sql_mode影响聚合函数对 NULL 的处理再看是不是数据本身没迁干净。大多数情况下参数改对了聚合结果也就对齐了。5. 最后补一刀我的迁移参数核对习惯这次从 MySQL 迁 GoldenDB 的经历让我养成了一个习惯任何数据库迁移项目第一周的任务清单里必然有一项“参数 diff 核对”而不是等业务提需求再被动响应。具体做法很简单我会准备一个脚本把源端和目标端的关键参数自动拉出来对比生成一张差异清单。人工过一遍清单标注出哪些差异会影响业务哪些差异可以接受然后形成一份迁移参数基线。这张基线会留档后续 GoldenDB 版本升级或者参数配置变更时再跑一次 diff就知道环境有没有发生预期外的漂移。关于sql_mode这个参数我个人的最终建议是迁移初期以 GoldenDB 默认值为基准只做最小调整去掉ONLY_FULL_GROUP_BY不要试图把源端 MySQL 的整套参数原样复制过来。因为 GoldenDB 的分布式执行引擎对某些参数的处理逻辑和单机 MySQL 就是不一样强行对齐反而可能引入新的不确定行为。SQL 层面如果因为参数放宽而报新的错误优先改 SQL 去适配而不是继续放宽下一个参数否则到了最后你会发现参数全改了业务却变脆了。迁移这东西说到底就是一场“预期管理”。提前把参数差异摊在桌面上看一遍比上线后被报错信息追着跑要舒服太多。希望这篇把sql_mode和其他几个关键参数的坑讲清楚的记录能帮你少走一点我走过的弯路。
返回列表