分库分表——实战篇
基本流程
根据分库分表基本原理和方案,这里将通用的操作流程总结为以下几步:
根据要解决的问题,确定基本方案(分库 or 分表?水平 or 垂直?)
根据数据量(当前数据量和增长量)评估分库或分表个数
选分片 key(注意切分数据要均匀)
确定分表规则(hash或range等)
执行(双写/从库升级)
必要时进行扩容(尽量减少数据的移动)
结合这个操作流程,接下来我们看下具体的案例。
点赞表分库分表
背景
短视频业务中,通过点赞表保存了用户对视频的点赞行为,点赞表结构如下:
1 | CREATE TABLE user_like ( |
由于产品的日活逐渐上涨,点赞表的数据总量、增量都也随之上涨,当时已经有上亿的数据,单表查询速度已经明显下降,另外因为写数据量较大,数据库主库的 IO 也即将达到瓶颈。所以需要对点赞表进行分库分表。
操作流程
- 确定基本方案
点赞表面临的问题:
单表数据量太大,查询耗时
写请求量大,磁盘 IO 成为瓶颈
所以确定基本方案为:水平分库+分表
- 评估分库或分表个数
结合当前数据量级和增量,希望未来两年不再需要扩容,计算后,决定分为 2 个库,每个库分为 128 个表。
这里分为两个库,是指点将点赞数据分为两个库,由于点赞表原先是和其他业务数据放在用一个数据库中,所以这里其实是要进行一个垂直方向上的分库,然后将拆出来的点赞库再水平分为两个库。
每个库之所以分为 128 个表,则是希望再次扩容时,数据库 IO 和 单表数据量能同时达到瓶颈,具体数字的确定需要结合机器配置、数据量、请求量等数据来做估计。
- 选分片 key
这个根据当前 SQL 逻辑确定即可,点赞表中只有根据用户 ID 查询数据的需求,自然选择用户 ID 作为分片 key
- 分表规则
标准二次分片法,具体内容在《分库分表——原理篇》中有介绍。
- 执行
这个例子因为即要进行分表,同时还要进行分库,所以在实际操作的时候,我们是分两步来进行的,先分表,然后分库,分库的同时将数据从原库中,迁移到新建的两个库中。
1)分表
分表具体的操作如下所示:
在原库中新建 128 张表
业务代码上线双写逻辑,增量数据同时写入原表和新表
- 写入新表时要应用分表规则
- 写原表和写新表要放在一个事务中,保证数据一致性
通过跑任务的方式将原表历史数据同步到新表
同步完成后通过定时任务对全量数据进行校验
间隔一定时间重复执行校验逻辑,验证双写数据的一致性
代码度读据逻辑改为从新表读数据
在改为读新表之前,必要时可以采取双读的方式进行校验,也就是线上业务流量读原表数据的同时,异步读取新表数据进行数据对比,无论是否一致都返回原表数据,因为点赞表逻辑比较简单,我们没有进行这一步
删除双写逻辑,只写新表
经过上述操作,点赞表已经被分为了 128 张表,但是整个数据还是在原库中,下一步就要进行数据库垂直拆分,以及再水平拆为两个点赞库。
2)分库
分库采用主从复制加从库升级的方式,并通过数据代理层来减少主库停写时间,具体操作如下:
新建两个数据库,每个数据库中都有 128 张表
将两个新库的主节点配置原库的从库,通过主从复制的方式,保证三个库中点赞表数据完全一致
业务代码中接入双数据源,应用分库规则,读写新建的两个数据源
注意此时,虽然代码中已经应用了分库规则,并且数据库配置也改为了新建的两个库的地址,但是我们中间接入的是代理层,两个代理依然指向了原数据库
然后再代理层进行切换,直接指向新库,服务开始开始读写新库,借助代理层的能力,1 秒内可以完成切换,几乎不停写,服务基本无损。
最后,跑任务删除两个新库中各自的冗余数据(主从同步时同步的是全量数据)
小结
上述案例,数据表和业务场景并不复杂,通过分库分表可以解决性能问题和资源瓶颈,同时并没有引入其它问题,所以是一个比较典型的适合分库分表的场景。
视频表分表
背景
视频表字段过多,单表业务逻辑过于复杂,各种各样的 SQL 非常难以维护和服用,另外单表数据量超过 1.5 亿,查询性能也有下降。
面临这样的问题时,很容易想到分表,但是因为这个表的业务比价复杂,分库分表的代价也比较高,我们当时先调研了其他方案。
比如从新进行技术选型,通过 MongoDB 来重构业务,还是因为复杂性,工作量太大,风险太高,可行性不大;另外也调研了声称兼容 SQL 的分布式数据库,但还是无法 100% 兼容,也不可行。最终还是决定采用分表的方案。
操作流程
- 确定基本方案
视频表面临的问题:
单表数据量太大,有查询性能下降的风险
但表字段太多,不好维护
所以确定基本方案为:垂直分表+水平分表
- 评估分库或分表个数
如何确定具体数字和上一个案例的思路是一样的,这里直接给结论:
- 水平分表:32 个表
- 垂直分表:4 类信息(4个表)
- 选分片 key
视频 ID 和 用户 ID 都有查询需求,所以需要按照两个维度进行分表,不过再按用户 ID 分表时,仅保留了必要字段
- 分表规则
标准二次分片法
第五步:执行
面临的问题
前面也提到了,因为这个表的业务比较复杂,所以分表遇到了一些问题,这里逐一介绍下。
1)全表扫描问题
原先存在一个定时任务,会定期扫描全表数据,和其它数据源如 MonogDB、ES、推荐内容池中的视频数据进行对比,及时发现不一致的情况,分表后想要进行全表扫描,这块逻辑实现变得更加复杂。
2)非分片 key 的查询问题
前面已经提到了,有根据不止一个 key 的查询需求,所以才去了冗余分表方案,同时采用的一个优化是没有冗余所有字段,仅保留了部分必要字段。
3)复杂查询问题
原先一些复杂查询用到的字段被拆分到了不同表中,无法执行了。这类查询不多,而且不是核心业务场景,所以我们不希望为了支持这类需求,依然容忍表中过多的字段。最终采用的方案是修改业务调用方,改为从 ES 中查询数据
4)查询排序问题
原先一些查询用到的排序字段被拆分到了不同表中,基于和上述第三点相同的理由,我们没有在表中保留冗余字段,而是分别查询后,在内存中进行排序。
5)DML 明显增多
迁移过程中,双写逻辑上线后,DML增长了 9 倍,不符合预期。后定位到原因是在批量更新数据的任务中,由于分表,变成了多次批量。采取的解决是首先按分表规则聚合数据,再批量更新数据。
6)服务负载升高
读取新表后,服务负载明显升高,需要加机器保证服务稳定性。定位到原因是分表导致单次查询变为并行的多表查询导致的,采取了的解决方案是对调用方逻辑优化,只查询必要的字段,减少查询任务量
原先因为是单表,为了复用查询方法,无论业务方需要什么字段,都会查询全量字段。分表过程中,为了保证底层修改对调用方透明,依然会返回所有字段,所以需要并行查询多表,但实际上大部分调用方不需要用到所有字段
小结
视频表作为短视频业务的基础数据表,涉及的业务多且复杂,不仅导致工作量大,而且在执行方案前后都遇到了不少问题。另外,分表后,整体的灵活性和可维护性其实都受到了一定影响。
对于类似的业务场景,相比于在不得已的时候做分库分表,其实更好的方案是做好前期的技术规划。在业务迭代的过程中,能更早的发现未来可能存在的瓶颈,提前做出应对,这样也许可选的方案会更多一些。
总结
本文介绍的两个分库分表的案例,基本遵循了标准的分库分表方案和操作步骤。具体原理和细节和可以参考《分库分表——原理篇》这篇文章。如果有类似场景的话,可以作为参考。但更重要的是,我们要看到分库分表的收益和代价,在做技术方案时做好权衡。