做国产化迁移这几年,我处理过最多的需求不是“怎么把数据导过去”,而是“迁到TDSQL之后,原来200毫秒的SQL怎么就变成30秒了”。上个月刚协助一个老客户完成业务系统切换,数据导完、应用切流,监控却直接跳出慢查询告警,定位下来又是一条典型的分布式路由问题。类似的坑我在好几个项目里都踩过,所以这篇TDSQL性能优化方案,我打算从实战角度讲清楚:分片键怎么选、SQL怎么改写、强同步和事务怎么取舍、连接池和参数怎么调,以及上线前压测到底要测什么。
如果你正在做国产化迁移,或者刚接手一套TDSQL集群,这篇文章应该能帮你少走不少弯路。下面按我实际排查的顺序,把整套打法拆开讲。
1. 为什么TDSQL性能优化不能照搬单机MySQL的经验
1.1 上线当天的那条30秒慢查询
先说这个客户的具体情况。核心订单系统从商业数据库迁移到TDSQL,旧环境单表1亿多数据,一条“查某用户最近10笔订单”的SQL跑了200毫秒。迁移后数据量不变,硬件配置甚至还高了一档,可就是这条SQL,最慢的时候超过了30秒。
我第一时间查了数据库负载:CPU不高、磁盘IO不高、慢日志里却密密麻麻都是这条SQL。执行计划拉出来一看,问题很典型——这条SQL没有按分片键路由到单个分片,而是广播到了集群里的全部分片,每个分片各自扫表、各自排序,最后在网关层做了一次全局归并排序,再取出前10条。
这就是分布式数据库和单机数据库最本质的差别:单机MySQL里,一条普通的等值查询加索引就能搞定;TDSQL环境下,一旦SQL没有正确命中分片键,代价会被分片数量放大,而且是乘法级别的放大。
1.2 TDSQL的架构决定了性能问题的“四层结构”
很多DBA习惯性地用单机MySQL的思路来调TDSQL,一上来就改innodb_buffer_pool_size、调慢日志阈值,忙了半天发现效果不明显。原因在于TDSQL的性能瓶颈往往不在引擎层,而在架构层。我建议把一条SQL的完整路径拆成四层来看:
- 接入层(网关/Proxy):负责协议解析、SQL路由、结果合并,这里容易出现连接数不够、结果集过大、跨分片汇总开销。
- 分片层(每个Set内的MySQL引擎):每个分片是一套完整的存储引擎,单分片慢SQL和单机MySQL排查方法一致。
- 复制层:强同步复制需要等待备机返回,事务延迟会直接叠加。
- 分布式事务层:跨分片事务涉及两阶段提交,协调成本不可忽略。
这四层任何一个出问题,最终表现出来的都是“SQL变慢”,但优化手段完全不同。拿上面的订单查询来说,问题出在第一层的路由和第三层的归并排序,你再怎么优化单分片的索引都没用,必须从路由逻辑上解决。
所以我的经验是:拿到一条TDSQL慢SQL,先别急着看执行计划,先问三个问题——数据落在哪个分片?SQL有没有带分片键?网关层需要做多少额外工作?这三个问题想清楚,性能问题基本就能定位到具体层了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分片键设计:性能的胜负手
2.1 选错分片键的现场有多惨
TDSQL里,分片键(shardkey)决定了每一行数据落在哪个分片上。这个选择如果错了,后面所有优化都是徒劳。
还是拿订单系统举例。建表的时候,开发同学觉得订单号唯一、适合做主键,于是设置了shardkey=order_no。结果业务接口里高频查询是“按用户ID查订单列表”,每个用户的订单被分散到了几十个分片上,一次简单查询要扫描全部分片再合并。更麻烦的是,某个大客户的订单量占全表20%,数据全部落在了同一个分片上,导致那个分片成了热点:其他分片CPU利用率不到10%,这个分片常年80%以上。
这就是典型的两个问题叠加:分片键和高频访问条件不匹配导致跨分片查询,数据分布不均匀导致分片热点。
2.2 分片键选型的三条铁律
后来我帮客户重建订单表,把分片键从order_no改成了user_id。总结下来,分片键选型就三条标准,顺序不能乱:
- 高频查询条件优先。分片键必须是最常用、最核心的那个等值查询条件,让绝大多数SQL能直接路由到单个分片。
- 数据分布均匀。选的值域要足够离散,不能出现某些值的数据量碾压其他值的情况。比如tenant_id如果只有几个大租户,就不适合做分片键。
- 尽量保持不变。分片键一旦被更新,就意味着数据要跨分片迁移,代价极大。所以分片键尽量选业务上不会修改的字段。
多表关联的场景还要再加一条铁律:相关表的join字段必须是同一个分片键,并且取值相同,这样关联操作就能在分片内部完成,不需要跨分片传输数据。
比如订单表、订单明细表、用户表,三张表都按user_id分片,并且业务上能通过user_id关联,那join就都在本地分片完成。如果做不到,那就得考虑在应用层拆成多次查询,而不是让数据库硬扛跨分片join。
2.3 建表之后怎么检查和补救
如果你接手的是已经上线的表,不确定分片键是否合理,先用下面几招快速诊断。
第一,看每个分片的容量和行数。在管理端或者通过information_schema相关视图,查同一张表在每个分片上的行数,如果某个分片行数明显偏大,数据倾斜基本实锤。
第二,看慢SQL里跨分片语句的占比。TDSQL的慢日志和执行计划里能看到SQL涉及的分片数量,如果大量查询都涉及全部分片,分片键大概率选得不合适。
第三,如果确认分片键选错了,最简单粗暴但有效的办法是:建一张新表,用正确的分片键,数据迁移过去,应用切换后老表下线。数据量大的场景可以采用分批迁移、双写过渡的方式,但不要试图在线修改分片键——分布式数据库里改分片键的代价远超你想象。
我在实际项目里的做法是:所有新表评审,第一项就审核分片键,分片键没过关的不允许上库。这一关守住了,后面SQL优化的压力小一半。
3. 让SQL顺着数据跑:分布式SQL改写与网关优化
3.1 为什么limit 10反而慢得离谱
很多团队迁到TDSQL后第一个崩溃的场景就是“查最近10条记录”。单机MySQL里,ORDER BY gmt_create DESC LIMIT 10配合索引,是再普通不过的查询。但到了分布式环境,这条SQL的代价模型完全变了。
假设集群有10个分片,SQL要按时间排序取前10条。网关层不知道哪10条是全局最新的,所以只能让每个分片先把本片所有满足条件的记录都捞出来,传到网关,网关再统一排序取前10。这中间的数据传输量不是10条,而是10个分片各自过滤后的全部结果集。
如果这张表有1亿行,时间字段上的筛选条件又不强,每个分片可能要传输几十万行到百万行到网关,内存排序、网络传输的开销就全上去了。所以分布式环境里,LIMIT能优化性能的前提是:排序和筛选能够下推到分片层,每个分片先取局部前10,网关再归并10乘以分片数的结果,这样传输量才可控。
3.2 三个经典的SQL改写套路
排查跨分片慢SQL时,我基本按下面三个套路来改,大部分都能救回来。
套路一:SQL里必须带分片键等值条件。这是最立竿见影的。原SQL如果写的是WHERE status=1 ORDER BY gmt_create DESC LIMIT 10,一旦业务允许,加上AND user_id=?,查询就能从“广播全分片”变成“精准路由单分片”,性能差距是几十倍。
套路二:排序和聚合尽量下推。有些查询确实没法带分片键,那就要确认网关能不能把ORDER BY和LIMIT下推给分片执行。实测中,TDSQL网关对部分排序下推是支持的,但前提是SQL写法要规范,比如避免在ORDER BY字段上做函数计算,避免LIMIT和OFFSET过大。深分页在分布式环境里是个大坑,建议改成游标翻页或者基于索引条件的迭代查询,不要用LIMIT 100000, 20这种写法。
套路三:跨分片JOIN改应用层处理或数据冗余。非分片键字段上的JOIN,在分布式环境里会产生笛卡尔积式的广播,性能极差。我一般建议改成应用层多次查询再内存组装,或者干脆在业务表里冗余需要的关联字段,用空间换时间。
判断SQL改写是否生效,主要看EXPLAIN里执行计划涉及的分片数量。如果从“全部分片”变成“1个分片”,那这条SQL就稳了。
3.3 网关和并行度:先判断SQL有没有“下推”
网关层不是透明代理,它做的事情比想象中多。SQL路由、结果合并、连接管理、参数改写,全在网关完成。所以和网关直接相关的性能问题,也很容易暴露成慢查询。
一个常见的误区是追求“并行越高越好”。TDSQL可以配置并行查询,让一个复杂查询在多个分片上并发执行。听起来很美好,但实际上并行度太高会瞬间打满网卡和内存,尤其在高并发业务下,容易引起整体雪崩。我实测下来,并行度设置在4到8之间,对多数报表类查询比较合适;实时在线交易类的SQL,不要说并行,最好保证全部分片内单点完成。
还有两个容易被忽视的点:一是结果集大小限制,网关层如果一次性返回超大结果集,内存压力很大,建议按业务需要控制查询返回的字段和行数;二是连接超时时间,分布式环境下,跨分片合并比单机查询更耗时,超时设置太短会导致应用侧误报。
4. 强同步、复制延迟与应用侧的一致性取舍
4.1 强同步复制的性能账本
TDSQL默认采用强同步复制,保证主备数据不丢失。代价是性能:每次事务提交,主库必须等待备库返回确认才能回复客户端,相当于每个事务多了一次网络往返。
如果事务本身很小,比如单行写入,这个额外延迟可能只有几毫秒,体感不明显。但如果是大事务——比如一次性批量更新几万行——强同步带来的延迟就会显著放大,而且一旦备库所在网络抖动,主库事务提交就会被卡住,整个业务的写入链路都会跟着受影响。
所以用TDSQL,务必要对“事务大小”敏感。这不是说强同步不好,而是你得清楚它的性能边界在哪里。
4.2 什么时候该用强同步,什么时候该降级
一致性级别的选择,不应该全局一刀切,而是按业务类型分场景。
交易、账务、库存这类业务,资金或核心数据不允许丢失,强同步是底线,没有讨价还价的余地。但代价是要控制事务粒度,尽量把大事务拆小,每条事务只处理必要的数据,减少强同步等待的放大效应。
日志、报表、统计、非核心配置这类数据,可以接受小概率延迟甚至少量丢失,TDSQL支持调整为异步复制或者最终一致性的读写分离模式,性能会明显提升。我见过一个报表库,从强同步改成异步之后,批量写入性能提升了接近一倍。
应用侧也要配合做“降级开关”。比如设置一个配置项,正常走强同步,大促或者压测场景临时切到异步,压测完再切回来。这里要强调一下:降级方案一定要提前演练过,不要等到线上出问题才第一次切换,那样反而容易因为操作失误造成更大故障。
4.3 分布式事务的隐藏成本
TDSQL支持跨分片分布式事务,底层用两阶段提交保证一致性。但这东西是有成本的,参与者越多,协调开销越大,失败回滚的逻辑也越复杂。
优化思路很简单:尽量让一个事务只落在一个分片上。做法是,事务中涉及的所有数据行,都通过同一个分片键访问。比如“创建订单并扣库存”,订单表和库存表都按user_id或者store_id分片,事务内所有操作都带上同一个分片键,这个事务就是本地事务,不需要走分布式协调。
如果确实无法避免跨分片事务,那就换个思路:不要用数据库强一致,改用应用层补偿。比如“订单创建后异步通知库存系统”,订单系统保证本地落库成功,通知失败则通过消息队列重试,最终达到业务一致。这样数据库压力小,应用灵活度反而更高。
5. 连接池、事务边界与参数调优:30%的常规性能空间
5.1 连接池为什么会成为瓶颈
TDSQL的连接模型和单机MySQL不一样:应用先连网关,网关再连后端分片。这个中间层放大了连接数。假设应用有100个连接,10个分片,网关到后端可能会建立远超100条的连接,再加上每台应用服务器都这样做,后端分片很容易被打到max_connections上限。
最常见的故障场景是应用重启后的“连接风暴”:几十个应用实例同时启动,每个实例的连接池一下子拉满,瞬间把网关和分片的连接数打爆,导致大量连接失败,看起来就像数据库挂了,实际上是连接数爆了。
解决方案有三步。应用侧:连接池初始连接数别设太大,增长策略要平滑,最大连接数要根据全集群规模计算,而不是拿单机MySQL的习惯照抄。网关侧:把max_connections和超时时间调合理,让异常连接能快速释放。运维侧:应用发布时尽量分批,避免所有实例同时重连。
5.2 大事务拆小、批量提交的实测数据
我在一个客户现场做过一次对比测试:一个定时任务需要更新某表5万行数据,原来一条SQL直接干完,耗时不长,但所有并发的小查询在它执行期间全部被阻塞,造成业务卡顿。
改成每500行一批提交,效果非常明显。批量任务整体耗时反而缩短了20%-30%,因为每批事务都很短,不会长时间持有锁,也不会产生超大的回滚段和binlog,主备同步的延迟也大幅下降。这个结论不只在TDSQL上成立,在单机MySQL上同样适用,但在分布式环境下,大事务的影响面更大。
所以我的建议是:所有批量作业必须分片分页提交,每批控制在几百行到一两千行,并且要监听主备延迟,一旦延迟超过阈值就自动减速,避免拖垮整个集群。
5.3 一份可以照着用的参数调整清单
下面这些参数,是我在多个TDSQL项目中验证过的基础调优项。需要注意:TDSQL部分参数在引擎层开放调整,部分需要通过管理端灰度下发,应用侧确认参数开放后再改,不要盲目执行SET GLOBAL。
| 参数 | 调整方向 | 调整原因 |
|---|---|---|
| innodb_buffer_pool_size | 按“物理内存的60%-70%”估算 | 分片所在主机内存越大,缓存命中率越高,但需要预留系统和其他组件内存 |
| max_connections | 结合分片数量和网关并发估算 | 连接数过高会导致系统资源耗尽,过低会导致连接排队 |
| max_allowed_packet | 根据批量SQL和最大单行数据量设置 | 过小会导致大批量写入失败,过大会被误用导致内存浪费 |
| tx_isolation | 读已提交优先 | 可重复读在分布式锁和undo上的开销更大,非必要不开启 |
| 慢日志阈值 | 上线初期建议设到1秒甚至500毫秒 | 分布式环境下慢SQL定位依赖完整慢日志,太大会漏掉问题 |
| 并行度参数 | 报表类4-8,交易类关闭 | 并行度越高,网卡和内存压力越大,在线交易场景容易雪崩 |
最后提一句,参数调整不是一次性的。每次调整完,都要做一轮压测和数据对比,前后至少观察一周,确认没有引入新的问题再固化下来。
6. 压测验证与上线后的潜坑
6.1 压测方案怎么设计才不是“走过场”
很多团队做国产化迁移,压测就是用sysbench跑一下读写,看到结果“还行”就宣布达标。sysbench这类工具更适合测硬件和引擎能力,不适合模拟真实业务。真实业务是混合SQL:点查、范围查、批量写、报表查询一起发生,而且每条SQL都带分片键。
我的做法是准备一套业务典型SQL集合,覆盖三个维度:高频等值查询、列表分页查询、批量写入。数据量按线上数据的1倍到2倍准备,分片键的值域分布要和真实业务一致,不能全用连续的整数,要包含热点用户和大客户的数据倾斜情况。
压测压力建议拉到日常峰值的至少2倍,并持续运行15分钟以上,观察业务成功率、平均延迟、P99延迟、连接数、主备延迟这几个核心指标。低于这个标准的压测,我基本认为参考价值有限。
6.2 上线前必须检查的容量与数据倾斜
正式切流之前,有两件事我强烈建议做。
第一件,逐表检查分片数据分布。有些数据看起来均匀,实际因为分片键选得不好,某个分片的磁盘占用已经远超其他分片。这种情况一旦上线,热点分片会先打满,而整体集群看起来还有充足容量,很容易踩坑。
第二件,做一次全量慢SQL评审。从测试环境收集慢日志,把所有执行超过1秒的SQL逐条过一遍,重点看有没有没带分片键的跨分片查询。这个评审最好由懂业务和懂分布式数据库的人一起做,纯粹的业务开发往往意识不到跨分片查询的代价。
6.3 我在实际项目里反复踩过的坑
这里分享几个我认为最值得记下来的教训。
第一个坑:旧系统SQL全量迁移,一个都没改。商业数据库迁移到TDSQL,SQL语法兼容性整体不错,但查询路由逻辑完全不同。直接迁移容易把隐式跨分片查询带到线上,慢查询告警就会成为常态。迁移前一定要安排专门的SQL改造窗口。
第二个坑:升级TDSQL小版本后,执行计划行为变化。TDSQL版本迭代很快,同一个SQL在不同版本上可能有不同的路由和优化策略。升级前必须重新压测,回滚预案也要准备好,不要只备份配置不备份版本。
第三个坑:监控和告警没有提前就位。慢日志、连接数、主备延迟、内存使用这些指标,必须在上线前就全部接入监控平台。我在一个项目里因为没有提前接好主备延迟监控,结果复制延迟悄悄爬到了几十秒,业务端读不到最新数据才发现异常。分布式数据库的故障,很多是一点点劣化出来的,没有监控等于盲跑。
最后再说几句
我自己经历完这几轮TDSQL性能优化之后,最大的感受是:分布式数据库的优化是一个系统性问题,分片键、SQL路由、一致性级别、连接管理、参数设置,每个环节都有关联。最忌讳的是遇到慢SQL就只改索引、只调参数,那样治标不治本。
现在团队里新表设计一定要过一道分片键评审,所有应用发布前必须附带慢SQL自检报告,压测也纳入了上线标准流程。后续如果再遇到跨分片复杂报表场景,我打算在预聚合表上多做尝试,减少对实时跨分片查询的依赖。
希望这篇实战经验,能帮正在做国产化迁移的你和你的团队少踩几个坑。
