TDSQL企业级分布式数据库:分片键设计与迁移运维实战
三年前我接手过一个订单系统,单表数据量刚过八千万行,主库白天的QPS稳定在六千上下,夜里跑批量任务时磁盘IO能顶到90%以上。团队当时的第一反应是加索引、加缓存、做读写分离,能上的手段全上了一遍,结果撑了不到半年又回到了原点。后来才下决心做水平拆分,选型对比了一圈,最终落在腾讯云数据库TDSQL上。这段经历让我对企业级分布式数据库这个词有了非常具体的理解:它不是把单机MySQL换个包装,而是把数据访问方式、事务边界、运维节奏全部重新组织一遍。TDSQL这类产品的价值,不在于它能扛多少QPS的宣传数字,而在于它把分库分表这件原本需要自研中间件、自己写路由逻辑、自己兜底分布式事务的苦活,收敛成了一套可以靠配置和规范来管理的工程体系。下面这些内容,是我自己从选型、建表、迁移到线上排障一路踩过来的整理,适合正在评估分布式数据库的架构同学,也适合负责具体落地的DBA和后端开发。
1. 从单机MySQL撑不住的那一刻说起:TDSQL到底解决什么问题
1.1 单库单表的三个硬天花板
很多人以为单机MySQL的瓶颈是QPS,其实在实际项目里,最先撞墙的往往是另外三样东西。第一是单表数据量,InnoDB的B+树在数据量超过两三千万行之后,二级索引的维护成本会明显上升,DDL操作基本等于停机,加一个字段可能要锁表几十分钟。第二是单机磁盘和连接数,一台机器能挂的盘、能开的连接数都是有上限的,业务方还在不停地加需求,硬件却已经堆到机柜放不下。第三是扩容的不可逆性,垂直扩容(换更强的机器)总有一个终点,到了终点之后你只有水平拆分一条路,而水平拆分本身是一个需要停机或者长时间双写的架构手术。这三个天花板不是同时到的,往往是数据量先到,然后是连接数,最后是写入吞吐,等到三个都到齐了,改造窗口期已经非常紧张。
我见过太多团队的处理方式是把问题往后拖,用归档、冷热分离、加从库读来续命。这些手段当然有效,但它们只是把临界点往后推了几个月,并没有改变数据模型本身是单点的这个事实。真正的解法是在业务还能承受改造的时候,提前把数据模型设计成可水平扩展的形态,而TDSQL这类分布式数据库提供的,恰恰是把这种可扩展形态做成默认选项的能力。
1.2 TDSQL的产品家族:别把TDSQL-C和TDSQL for MySQL搞混
腾讯云这边的TDSQL其实是一个产品族,名字相近但定位差别很大,选型时搞混会直接导致架构方向错误。我把它整理成一张表,方便对照:
| 产品形态 | 底层架构 | 典型场景 | 是否需要业务改造 |
|---|---|---|---|
| TDSQL for MySQL | 分片式(Proxy + SET + GTM) | 高并发交易、海量数据、金融核心 | 需要设计分片键 |
| TDSQL-C | 云原生存算分离,共享存储 | 通用业务、成本敏感、弹性扩缩 | 基本不需要 |
| TDSQL for PostgreSQL | 分布式PG内核 | 复杂查询、地理信息、分析混合 | 需要设计分布键 |
| 分析型TDSQL | MPP列存 | 报表、离线分析、数仓 | 需要建模 |
TDSQL for MySQL走的是分片路线,数据按分片键被打散到多个SET上,每个SET内部是一主多从的强同步结构,跨SET的事务由全局事务管理器协调。TDSQL-C则是另一条路,它把计算和存储分开,多个计算节点共享一份分布式存储,扩容时不用做数据重分布,单实例就能到很大的容量。这两者经常被拿来做对比,但它们的适用边界其实很清楚:如果你的瓶颈是单表数据量和写入吞吐,选分片式的TDSQL for MySQL;如果你的瓶颈是存储成本和弹性伸缩,选TDSQL-C。判断错了,后面会很难受,因为分片式一旦上线,改分片键的代价几乎等同于重建集群。
1.3 一个判断标准:什么时候该上分布式数据库
我的经验是看三个指标,任何一个长期越过阈值,就该认真评估了。第一,单表数据量持续超过五千万行且在增长;第二,写入QPS峰值长期超过单机MySQL能稳定承载的量级,并且垂直扩容已经没有性价比;第三,业务上存在明显的、稳定的分片维度,比如按租户、按用户、按地区都能自然切分。第三个指标最容易被忽略,但恰恰是最关键的,因为分布式数据库的性能红利,本质上来自分片之后每个节点只需要处理一部分数据,如果你找不到一个能把数据均匀打散又不会频繁跨片查询的维度,那上分布式只会把简单问题复杂化。
反过来,如果业务数据量不大、查询模式复杂、大量跨维度join,那分布式数据库给你的不是解药而是新的负担。我见过一个报表系统硬上分片,结果每一条查询都要跨十几个片做聚合,性能还不如原来单机加索引。所以选型第一步不是看产品能力,是先把自己的数据模型和访问模式画清楚。
2. 拆开TDSQL看内部:Proxy、SET、GTM这三块怎么协作
2.1 SQL从客户端到存储节点的完整旅程
理解TDSQL的第一步,是搞清楚一条SQL从客户端发出去之后到底经过了什么。客户端连的是接入层,接入层背后是Proxy网关集群,Proxy负责做SQL解析、分片路由、结果集合并。当一条SQL进来,Proxy先解析它的语法树,取出分片键的值,算出这条SQL应该落到哪一个或哪几个SET上,然后把请求转发给对应的SET。SET内部就是标准的MySQL主从结构,主节点执行SQL,从节点通过强同步复制保持数据一致。执行完之后结果原路返回,如果是跨片查询,Proxy还要做一次归并排序和聚合。
这套架构里最关键的设计是Proxy是无状态的,它可以水平扩容来承接更高的并发连接,而数据层的扩容则是通过增加SET来实现。两者解耦之后,整个集群的扩展性就有了两个维度,这比单纯堆中间件要灵活得多。实际部署时Proxy数量往往和业务连接数成正比,我见过一个集群配了十几组Proxy,每组四到八个节点,前端挂负载均衡,这样单点故障基本不会影响整体可用性。
2.2 SET内部的主从与强同步复制
SET这个词在TDSQL里指的是一个数据分片单元,通常由一主两从或者一主多从组成,分布在不同物理机甚至不同可用区。主节点负责写入,从节点通过强同步复制来保证数据不丢。强同步的意思是,主节点提交事务之前,必须等到至少一个从节点确认收到了binlog,才会向客户端返回成功。这和MySQL原生的半同步复制有点像,但TDSQL在实现上做了优化,把等待逻辑放到了更底层,减少了网络往返带来的延迟波动。
强同步带来的是数据可靠性,代价是写入延迟会比异步复制高一点。在同一个可用区内,这个额外延迟通常是毫秒级的,业务基本感知不到;如果是跨可用区强同步,延迟会明显一些,所以多数生产环境会采用同城双可用区强同步、异地异步的方案,在可靠性和延迟之间取平衡。这里有个实操细节值得提醒:从节点如果因为慢查询或者大事务落后太多,会被踢出强同步集合,主节点在只剩一个从节点确认的情况下会降级为半同步甚至异步,这个时候如果业务正在做批量导入,风险窗口是真实存在的,一定要把复制延迟的告警阈值设好。
2.3 GTM与分布式事务:两阶段提交的真实代价
跨SET的写入就涉及到分布式事务,TDSQL用全局事务管理器(GTM)来分配全局唯一的事务ID和序列号,用两阶段提交来保证跨片原子性。流程大致是:协调者先向所有涉及的SET发送准备请求,每个SET把事务预提交并锁住相关资源,全部回复准备好之后,协调者再发送提交请求,各SET正式提交。这个过程听起来很标准,但实际使用中有两个点必须注意。
第一个是性能。两阶段提交意味着一次跨片写要经历至少两轮网络往返,加上锁持有时间变长,吞吐会明显低于单片写。所以设计分片键时,一个很重要的目标就是让绝大多数事务落在单片内,跨片事务越少越好。第二个是异常处理。如果协调者在第二阶段崩溃,部分SET提交了、部分没提交,恢复时需要依赖GTM的日志做补偿。TDSQL在这方面做了比较完整的处理,但作为业务方,我们仍然要避免把长事务、大批量更新放进跨片场景,因为那会把不确定窗口拉得很长。
3. 分片键设计:决定后面三年运维痛苦程度的一步
3.1 分片键的选取原则与反面案例
分片键的选择是整个TDSQL使用过程中唯一一个几乎不可逆的决策,因为改分片键意味着全量数据重新分布,代价极高。我在实际项目中总结出四条原则,按优先级排列:分片键必须出现在绝大多数高频查询的等值条件里;分片键的取值要足够离散,避免数据倾斜;分片键最好是整数类型或者能转成整数的类型;分片键一旦确定,就要在业务代码里当成硬约束去遵守。
反面案例我见过不少。有个项目用自增订单ID做分片键,结果所有新数据都写在最后一个分片上,因为自增ID是连续递增的,取模之后分布极不均匀,最后演变成单点热写。另一个项目用时间字段做分片键,问题更明显,所有当天数据都挤在一个分片里,历史查询又要跨片扫描。还有人用性别、状态这类低基数字段做分片键,数据只能被分成两三堆,完全失去了分片的意义。比较稳妥的做法是用业务主键的用户ID或者租户ID,如果确实没有合适的业务字段,可以用订单号的哈希值,把它作为分片键。
3.2 建表语句里的shardkey与分表数量
TDSQL的建表语法和MySQL高度兼容,区别主要在于分片信息的声明。下面是我常用的一个建表模板:
SHARDKEY指定分片键,SHARDNUM是总分片数量,两者共同决定数据怎么分布。分片数量不是随便定的,它要和预期的数据总量、单个SET的承载能力一起算。我的经验公式是:估算三年后的总数据量,除以单个SET能舒适承载的量(比如五千万行左右),再乘以一点冗余系数。分片数一旦确定,后续加片虽然支持,但需要做数据重分布,期间会有性能抖动,所以初始宁可选大一点,比如64或者128,让每个片的数据量小一些,反而更利于均衡。
这里还有一个容易被忽略的点:分片数最好选择2的幂次或者容易做哈希的数值,这样Proxy在计算路由时可以用位运算代替取模,路由开销更低。虽然这个差异在单条SQL上微乎其微,但放在十万级QPS的场景下,积累起来就是可观的CPU节省。
3.3 跨分片查询和热点写入的应对
跨分片查询是分布式数据库绕不开的问题,处理原则是能避免就避免,避免不了就控制规模。TDSQL里带分片键等值条件的查询会精准落到单个SET,性能最好;带分片键范围条件的查询会落到多个SET,Proxy做结果合并;完全不带分片键的查询会广播到所有SET,相当于全表扫描乘以分片数,这是最差的形态。所以业务侧的SQL规范一定要卡死:核心链路的查询必须带上分片键。
热点写入的处理稍微复杂一点。即便分片键选得好,也可能因为业务特性出现某个分片写压力特别大,比如某个大客户的所有订单都落在一个分片上。应对办法有几个方向:一是把大客户的ID做一次散列,人为打散到多个片;二是拆分大客户的业务表,把订单明细单独按更细的维度分片;三是利用TDSQL支持的读写分离,把统计分析类的查询全部导向只读实例,减轻主节点压力。这三种办法我都用过,最有效的还是从数据分布源头解决,也就是设计分片键时就把大客户场景考虑进去。
4. 兼容MySQL不等于可以无脑迁移
4.1 不支持的语法与行为差异清单
TDSQL宣称兼容MySQL协议,这话没错,但兼容的边界在哪里,必须提前摸清楚。我整理了一份在实际迁移中真实踩到过的差异清单:
| 类别 | 差异点 | 处理方式 |
|---|---|---|
| 存储过程 | 部分版本不支持或功能受限 | 业务逻辑上移到应用层 |
| 触发器 | 跨分片触发器不支持 | 改写为应用层事件处理 |
| 外键 | 跨分片外键不生效 | 用应用层保证一致性 |
| 跨片JOIN | 支持但性能差 | 改为分片键对齐的JOIN或应用层拼接 |
| 自增主键 | 全局自增需走序列服务 | 用分布式序列或雪花算法 |
| 部分DDL | 大表DDL需走在线变更 | 用gh-ost类工具或平台能力 |
| 用户变量 | 跨语句的用户变量行为不一致 | 避免在SQL里依赖会话变量 |
这份清单里,外键和存储过程是最容易出问题的。很多老系统把大量业务逻辑写在存储过程和触发器里,迁移时这部分必须重写,工作量往往被严重低估。我的建议是在迁移评估阶段就把全库的存储过程、触发器、外键、视图全部导出来做一次扫描,逐条判断能否保留、需要改写还是必须删除,形成一份明确的改造清单,而不是等到联调时才发现。
4.2 迁移的完整流程:评估、改造、双跑、切换
迁移不是一个动作,而是一条流水线,我把这条流水线拆成四个阶段。第一个阶段是评估,把源库的表结构、数据量、慢查询日志、存储过程全部拿过来做分析,输出一份兼容性报告和改造清单。第二个阶段是改造,按照清单把不兼容的语法改掉,把分片键设计落到建表语句里,把跨片查询优化掉。第三个阶段是双跑,用数据同步工具把源库的增量数据实时同步到TDSQL,同时让一部分只读流量打到新库做验证,比对两边结果是否一致。
第四个阶段是切换,切换又分灰度切换和一次性切换。灰度切换是把一部分用户或者一部分功能先切过去,观察一段时间再放开;一次性切换是在业务低峰期做短暂停写,把增量追平之后切流。我推荐灰度切换,虽然周期长,但风险可控,出问题可以快速回退。无论哪种方式,切换前一定要做一次完整的回滚演练,确认老库还能接得住流量,否则一旦新库有问题,你是没有退路的。
4.3 数据校验怎么做才靠谱
双跑阶段最有价值的事情就是数据校验。校验不是简单地比一下总行数,因为总行数相同不代表内容相同。我的做法是分三层校验:第一层比行数和主键的校验和,快速定位有没有明显的数据缺失;第二层按时间区间做分块的行级比对,把最近N天的数据按小时切片,逐片比对关键字段;第三层做抽样比对,随机抽取几万行做全字段对比,检查是否有字段截断、精度丢失、字符集转换错误。这三层都过了,才能认为数据是一致可靠的。
校验工具方面,如果数据量在TB级以下,可以用开源的校验工具或者自己写脚本;如果数据量很大,最好用支持并行分块校验的方案,否则一次全量校验可能要跑好几天。还有一个细节是校验窗口,如果你校验的是有实时写入的表,必须在追平增量的瞬间把写入暂停一小会儿,否则比对的时候两边数据还在变,永远比不平。
5. 线上运维:那些监控面板上看不出来的问题
5.1 慢查询的三种典型形态
上了分布式之后,慢查询的形态和单机时代完全不同,我把它归成三类。第一类是路由放大,一条SQL在单机上很快,但因为没带分片键被广播到所有分片,每个分片都要执行一遍,总耗时变成单片的N倍。这类问题在监控上表现为CPU被拉高但单条SQL的执行时间看起来不长,很容易被误判成正常波动。第二类是跨片合并,带排序或者聚合的跨片查询需要Proxy做全局归并,如果返回结果集很大,归并本身就会成为瓶颈。第三类是单分片内的慢查询,这种和传统单机SQL调优一样,看执行计划、加索引就行,反而最好处理。
排查顺序上,我的习惯是先看Proxy层的慢日志,确认SQL的路由形态,是单片、多片还是广播;再看各个SET的慢日志,定位到具体是哪个分片、哪条SQL慢。这个顺序能快速把问题从集群级别收敛到单点,避免在一堆指标里瞎找。
5.2 扩容与数据重分布
业务增长到一定程度就需要加SET,TDSQL支持在线扩容,但重分布的过程会消耗资源,需要挑业务低峰期做。扩容的基本流程是新增SET,然后按新的分片数重新计算路由,把部分数据从老SET迁移到新SET,迁移过程中要保证数据一致性,通常用一致性快照加增量追平的方式实现。整个过程对业务是透明的,但迁移期间的新旧分片都会承受读写压力,所以监控上会看到延迟升高、CPU上升,属于正常现象。
我的经验是扩容不要等到容量告急才做,最好在容量用到六七成的时候就规划,因为迁移本身需要缓冲空间。另外,扩容之后要重新审视一遍分片键的分布,看看是不是真的均衡了,如果某个新分片依然很空,那说明分片键本身有问题,光靠加机器解决不了。
5.3 备份恢复与误删数据的兜底
分布式数据库的备份比单机复杂,因为要保证多个分片在同一个时间点的一致性快照。TDSQL提供物理备份加binlog的机制,支持按时间点恢复,这部分能力平台已经封装好了,日常要做的就是确认备份策略是否符合你的RPO要求。我的建议是核心库每天至少一次全量备份,binlog保留时间不短于七天,重要的库可以保留更久。
误删数据是最考验兜底能力的事故。真发生了,第一步是立刻停止写入相关表的流量,防止删除操作继续扩散;第二步是确认删除的时间点,用PITR恢复到删除前一秒的实例;第三步是从恢复出来的实例里把丢失的数据导出来,再补回生产库。这里有个关键前提,就是把恢复实例到补数据之间的操作练熟,因为真出事的时候没有时间试错。我所在的团队每个季度会做一次误删恢复演练,把整条链路走一遍,这个习惯救过我们不止一次。
6. 选型横向对比:TDSQL、分库分表中间件、云原生数据库
6.1 三条技术路线的取舍逻辑
市面上解决扩展性问题的方案大致有三条路。第一条是自研或者用开源的中间件(比如ShardingSphere这类)配合自建MySQL,好处是可控性高、成本相对低,坏处是分布式事务、扩容、运维全套都要自己搞定,团队需要相当强的数据库功底。第二条是云原生共享存储方案,代表是TDSQL-C这一类,好处是基本不用改业务、扩容丝滑,坏处是存储层共享带来的性能天花板和成本模型需要仔细算。第三条就是TDSQL for MySQL这种分片式企业级方案,它把中间件能力和数据库内核整合在一起,分布式事务、强同步、扩容、备份都是平台能力,代价是业务要做分片键改造。
怎么选,我的判断是看团队构成和业务阶段。如果团队里有成熟的DBA和中间件开发,业务量还没到非分片不可,那中间件方案能省不少钱。如果团队人手紧张、业务增长快、又要求高可用和强一致,那托管式的分片数据库更省心。云原生方案适合那种数据量中等、但要求弹性伸缩和成本优化的场景。
6.2 成本账怎么算才不亏
选型时最容易算错的是成本。表面上看,自建中间件的机器成本更低,但隐性成本很高:中间件的开发和维护人力、分布式事务出问题时的排查时间、扩容时的人工投入、故障时的影响面。把这些折算成钱,很多时候并不比托管方案便宜。托管方案的成本结构则比较透明,按实例规格、存储、备份空间计费,弹性扩缩也意味着闲时可以降配,这一点在自建方案里很难做到。
我自己的算法是,把三年的总拥有成本列出来,包括机器、人力、运维、故障损失预期,再对比托管方案的账单。这个算法不复杂,但很多团队在做决策时只看了机器成本那一栏,结果上线半年才发现人力成本远超预期。
6.3 混合架构不是妥协而是常态
最后说一个我的真实体会:绝大多数中等以上规模的系统,最终都会走向混合架构,而不是全量上分布式。核心交易链路用分片式数据库扛写入和容量,报表分析走列存或者数据仓库,低频业务留在普通单机上,缓存层继续承担热点读。这种组合不是技术不纯粹,而是每种方案都有它的性价比区间,强行统一反而会让成本失控。
TDSQL在这个组合里的定位,就是那个承担核心交易数据、要求强一致和高可用的底座。它不负责解决所有问题,但它在自己擅长的场景里表现足够稳。我在实际使用中最大的感受是,上分布式数据库之前,先把分片键和数据访问规范定死,比选哪家产品更重要;规范定好了,迁移和运维都会顺很多,规范没定好,再好的产品也救不回来。