I - 稳定性治理

128 阅读7分钟

I:大表水平拆分

问题

  • 怎么分表?分为几张表?分表后,多表的读写场景怎么兼容?
  • 存量数据怎么迁移?增量数据怎么同步?数据一致性如何保证?
  • 读写流量如何切换?hive 表如何处理?老表下线事宜等等...

回答

1. 怎么分表?分为几张表?

一般来讲,分表方式包括:hash 法、range 法、时间划分法

由于两张表目前存储数据量较大,使用 range 法可以减少存量数据的迁移。此外,在平台侧是根据号段区间分配给业务方的,那么业务方在分页读的时候,一般读取的数据在空间上是连续的,所以使用 range 法更为合适

在确定分表方式后,重要的是确定单表能存储的数据量,这是根据延文哥进行的压测记录决定的,压测结果是 productidinfo 单表压到 8.9 亿量级下,数据库读流量在 12K 情况下未出现异常,那么考虑到集群稳定性,选择单表 8 亿的量级划分表

至此,采用 range 法,按照 8 亿一张表的方式进行单表的水平拆分。接下来需要确定分几张表的问题,这一点,我在分表数量中考虑到一次性满足未来 3-5 年的背景下,进行了简单的计算,最终决定 productidinfo 表和 skuidinfo 各分为 10 张表

2. 分表后,多表的读写场景怎么兼容?

分表方式确定后,接下来需要梳理一下对应表的新老接口,这是因为从原先的「单表」读转变到多张「分表」读过程中发生了变化

  • 对于读场景而言,预期内会出现查询放大的问题,但是具体放大多少倍,是需要调研清楚的。除此之外,在本次 productidinfo 表和 skuidinfo 表中存在非主键读场景,这种情况需要额外的支持
  • 对于写场景而言,需要确定批量写是否能正确写到各自的分表上

关于分页读造成查询放大的问题,手动编写代码加以支持,这部分研究了下 zebra 源码 - ShardPrepareStament.java 类

关于读写接口收敛,使用了代理模式 & 适配器模式进行兼容

关于新旧两表同名问题的兼容,这部分研究了下 zebra 配置,使用多数据源隔离

3. 老表存在非主键(即 biz_productid/biz_skuid)读场景,分表后如何兼容?

由于老表内存在非主键读场景,分表后,会出现查询放大的问题,这是因为分表键选择的是主键(productid/skuid)。那么,当 zebra(数据库中间件)遇到非主键读的场景时,会默认走全表查询,相应的查询放大了 10 倍,因为从单表分为 10 张表

解决方案:

  • 方案一:RDS 上配置辅维度,当 zebra 遇到非主键读场景时,使用辅维度的 key 去路由

    • 优点:无需额外操作,只需要配置一个“死”的分表规则,当遇到非主键读场景时,zebra 根据这个分表规则帮助路由

    • 缺点:

      • 未来平台可能会存在主键 productid 和非主键 biz_productid 不一致的场景,使用这种方案,意味着需要兼容多种情况。但分表规则是“死”的,兼容性/可扩展性事实上是比较差的,为了兼容未来多种情况,需要频繁改动这个辅路由规则

      • 另一点,根据[01_Zebra分库分表接入指南]相关文档,辅维度配置后,如果主辅维度路由结果不同时,物理上是不同的两套表,意味着存在额外的存储花销。此外,还需要开启「同步」按钮,这意味着存在主从延迟,对于一些延时要求较高的读场景也是难以接受的

  • 方案二:按照 biz_productid/biz_skuid 新建两张分表 bizproductinfo 和 bizskuinfo,当遇到非主键读场景时,走这两张表上查询

    • 优点:不存在主从延迟,并且由于是按照 biz_productid/biz_skuid 以 hash 的方式分表,无论 productid 和 biz_productid 是否相等,都能路由到对应表上

    • 缺点:由于新建分表,意味着存在存储花销。此外,新建分表,意味着写操作需要双写两张表,这里需要考虑到事务问题

最终选择了方案二,因为线下 DTS 同步任务实践证明,线下同步延迟高达 5000ms 的情况时有发生,这是比较难接受的

关于事务问题,目前 idservice 中是没有事务保证的,尽管存在双写两张不同表的场景,由于两张表使用的是不同的数据源,无法用事务进行保证,目前只能使用比较折中的方法,即在切写流量到新表之前先把数据对齐后,再进行切换

4. 存量数据怎么迁移?增量数据怎么同步?

关于存量数据迁移,存在三种方法:停机迁移、自己写 job 迁移、使用 DTS 工具进行迁移

尽管停机迁移理论上可以完成存量数据迁移的任务,但由于停机会造成线上服务暂停,导致经济损失,事实上并不可行

最初,我是自己写了 job 来实现存量数据迁移的。但经过第一次集体 CR 后,发现自己写 job 存在多种问题,例如:异常的处理、迁移速率的控制、线程池配置的选择等等,最后选择使用 DTS 工具迁移

这个 DTS 工具从 DB 层面,借助 binlog 迁移/同步数据

关于增量数据的同步,使用的 DTS 同步功能进行,但由于 ppglobal 集群存在北/上机器(物理因素),导致存在主从延迟,后果是出现主从不一致问题

后面增加了双写新旧表逻辑 + DTS 同步任务都兜底后,主从不一致问题消失

5. 上线代码后,开启采样 diff 时,出现了超时问题、diff 不一致问题,如何解决的?

关于超时问题,观察了下主要是走 biz 表读场景时出现的,经过考虑,由于 biz 表分表数量过多,根据 hash 分了 128 张表,那么最差的情况下,一批数据会走 128 张表查,造成了不可接受的查询放大

  • 优化方法:减少 biz 表的分表数量,从 128 张表缩减为 16 张,同时开启 zebra 并行读,增加并发
  • 结果:diff 超时现象偶有发生(几乎消失)

关于 diff 不一致问题,这个主要是源于 DTS 同步的主从延迟造成的,因为两张表的存量迁移 + 增量同步使用的是 DTS 工具进行的,在进行增量同步时,由于 MySQL 集群存在北京和上海的机器,主从延迟比较大,尽管联系了 DBA 换了高配机器,但实际上同步延迟仍然比较高

  • 优化方法:使用代码进行双写
  • 结果:diff 不一致问题消失,但存在唯一键冲突异常,需要代码兼容

6. 读流量切换?

读流量从 0% 放量至 15% 再放量至 100%,但线下实际上观测到无/少量符合预期的 diff 不一致后,全量走即可

关于 diff 不一致,但符合预期的情况,这部分咨询了 QA-建博,他的回答是 QA 方会构造 id 不一致的商品,并且会重复使用已有的 productid/skuid,然后进行 insert 操作,这样就会造成老表中存留的数据与新表的数据不一致,但这只是测试使用,符合预期

7. Hive 表如何处理?

关于 hive 的处理,由于本次改动只是在单库内单表的水平拆分,只需要登陆[DB 数据同步平台]将 hive 表的 etl 单表配置修改成分表配置并填好正则即可

8. 老表下线?

关于老表的下线,这部分等到切 4 后(读写全量走新表)即可开始老表的下线,对于本次改造,只需要原先的多数据源,根据[Zebra-MDP官方文档]进行 zebra 配置修改即可