dbaplus社群

吞吐量达4000+TPS!B站交易系统架构演进

同时我们对系统做了以下优化:

  • 对没有依赖的服务进行并发调用(商品/店铺/活动/用户信息等一起并发调用),如图2-3所示

  • 优化调用链减少冗余调用,推动下游服务接口改造及合并,保证一次请求下来,每个基础接口只会被调用一次,如图2-3所示

  • 设置合理的超时时间和及连接重试(200ms, 部分接口99分位上浮100%,connect连接重试)

  • 排除事务内的外部调用(服务依赖,mq,缓存)

  • 对弱依赖接口进行mq或异步调用(设置关注/缓存手机号/回滚库存优惠券等) 

Image

2-3 优化后的调用链路

经过优化后的接口耗时如2-4图所示,从原来300ms降低100ms左右,效果比较显著,用户的下单体验得到较大提升。

Image

2-4 下单耗时对比图

2.异步下单优化

面临问题:电商活动离不开秒杀场景,通常情况下小库存秒杀做好限流的话问题不大,但拜年祭手办通常有5000个左右的库存,如2-5如图所示,属于大库存秒杀 , 限流值设得太小会严重影响用户体验 ,大库存抢购时下单qps遇到瓶颈 600+qps的时候库存服务行锁比较严重,耗时开始大幅上升,大量数据库操作占用连接数较高。

Image

2-5 拜年纪商品

思考:服务器的处理能力是恒定的,就像早高峰一样,需要错峰限行,这就是我们说的削峰,对流量进行削峰不仅让服务器处理得更加平稳,也节省服务器资源。一般削峰的手段有验证码,排队等方式,这里我们主要是采用异步下单这种排队的做法。

说到排队,最容易想到的就是消息队列,可以通过消息队列把两个系统模块进行解耦,对于抢购场景来说也是非常合适的,可以有效把流量通过队列来承接,然后平滑地进行处理,如图2-6所示。

Image

2-6 消息队列解耦

按照消息队列的排队方案,我们把整个下单流程调整为异步批量下单链路(图2-7所示) ,在合法校验过后生成订单号提交到databus消息队列 (图2-8所示),再监听databus批量拉取订单进行合并下单(图2-9所示),目前设置的是最多20个一消费,下单结果会在数据库及redis中保存。

Image

2-7异步下单链路

Image

2-8 提交下单请求至mq

Image

2-9 从mq消费订单消息

如图2-10所示,在进入队列后,前端会提示活动火爆,正在努力下单中 ,同时在0~2秒内随机调用下单结果查询接口,轮询30秒(必须设置最大时间兜底,防止无限查询)。

对于合并的订单进行批量冻结库存,并行冻结优惠券,批量合并sql插入数据库,最大限度上减少性能消耗。

Image

2-10 异步下单示例图

其他优化细节:

  • 下单限频/限流;

  • 其中对于一些弱依赖的操作直接进行降级,比如设置商铺关注,缓存手机号,记录操作日志等;

  • 批量操作异常时(接口超时则fail fast),会分解为单个订单重新进行调用(库存操作会试探单库存扣减 单库存扣减成功 并发请求剩余订单,单库存扣减失败 剩余订单全部置为失败);

  • 下单结果查询走redis,异常情况降级为数据库;

  • databus异常时,直接降级为同步下单(库存服务也会做限流);

  • databus消费者会做幂等及超时判断(订单投递时间跟当前时间差值),超过一定时间会自动抛弃,下单失败 。

经过改造,压测下单支持4000+tps,最终也顺利利用异步下单支撑了早期的拜年祭手办抢购,如图2-11所示。

Image

2-11   活动抢购qps图

3.分库分表

首先并不是所有表都需要进行切分,主要还是看数据的增长速度。切分后会在某种程度上提升业务的复杂度,避免"过度设计"和"过早优化"。分库分表之前,不要为分而分,先尽力去做力所能及的事情,例如:升级硬件、升级网络、垂直拆分、读写分离、索引优化等等。当数据量达到单表的瓶颈时候,再考虑分库分表。 

数据量过大的风险如下:

1)高负载下主从延迟严重,影响用户体验,并且对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO。例如1T的数据,网络传输占50MB时候,需要20000秒才能传输完毕,整个过程的风险都是比较高的

2)对一个很大的表进行DDL修改时,MySQL会锁住全表,这个时间会很长,这段时间业务不能访问此表,影响很大。如果使用pt-online-schema-change,使用过程中会创建触发器和影子表,也需要很长的时间。在此操作过程中,都算为风险时间。将数据表拆分,总量减少,有助于降低这个风险。

3)大表会经常访问与更新,就更有可能出现锁等待,一旦出现慢查询,风险很大,容错性很低。将数据切分,用空间换时间,变相降低访问压力,而且利用水平切分,当一个数据库出现问题时,不会影响到100%的用户,每个库只承担业务的一部分数据,这样整体的可用性就能提高 

这里我们明确下分库、分表到底能解决什么问题:

  • 分表:解决单表过大导致的查询效率下降(海量存储,即使索引正确也会很慢) MySQL 为了提高性能,会将表的索引装载到内存中。InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还与具体的表结构的设计有关,最终导致的问题都是内存限制。这里,增加硬件配置,可能会带来立竿见影的性能提升。

  • 分库:解决Master服务器无法承受读写操作压力(高并发访问,吞吐量) 

在2020年的时候,会员购随着业务发展,订单数据快速增长,基本每半年数据量就会翻倍,所有核心表均达到千万级别大表的DDL,查询效率,健壮性都有问题 ,并且高负载下,会有较为明显的主从延迟,影响到用户体验。

首先是技术选型:

站在巨人的肩膀上能省力很多,目前分库分表已经有一些较为成熟的开源解决方案:

  • 阿里的TDDL,DRDS和cobar

  • 开源社区的sharding-jdbc(3.x开始已经更名为sharding-sphere)

  • 民间组织的MyCAT

  • 360的Atlas

  • 美团的zebra 

这么多的分库分表中间件全部可以归结为两大类型:CLIENT模式、PROXY模式。

无论是CLIENT模式,还是PROXY模式。几个核心的步骤是一样的:SQL解析,重写,路由,执行,结果归并。

经过讨论大家更倾向于CLIENT模式,架构简单,性能损耗较小,运维成本低,而且目前部分项目中都已经被引入shardingjdbc,并且部分模块已经在使用其分库分表功能,网上文档丰富,框架比较成熟 。

选择sharding key:

sharding column的选取是很重要的,sharding column选择的好坏将直接决定整个分库分表方案最终是否成功。

sharding column的选取跟业务强相关,选择sharding column的方法最主要分析你的API流量,优先考虑流量大的API,将流量比较大的API对应的SQL提取出来,将这些SQL共同的条件作为sharding column 例如一般的OLTP系统都是对用户提供服务,这些API对应的SQL都有条件用户ID,那么,用户ID就是非常好的sharding column。

非sharding column查询该怎么办? 

  • 建立非sharding column属性到sharding column的映射关系  

  • 双写冗余全量数据(不需要二次查询)

  • 数据异构(TIDB,ES,HIVE等,应对复杂条件查询,近实时或离线查询)  

  • 基因融合(比如订单号里融合mid基因,最新的订单号规则:orderId+mid%512 比如4004164057659338) 

切分策略:

1)范围切分

比如按照时间区间或ID区间来切分,如图3-1所示,优点:单表大小可控,天然水平扩展。缺点:无法解决集中写入瓶颈的问题。

Image

3-1 范围切分

2)Hash切分

如图3-2所示,如果希望一劳永逸或者是易于水平扩展的,还是推荐采用mod 2^n这种一致性Hash 。

Image

3-2 Hash切分

3)会员购交易切分策略

如图3-3所示,切分键选择:mid 和 order_id相关

根据 mid 分表,使用新的orderid 生成规则,orderid 融入(mid%512)

  • 库表数量:4个集群(主从),每个集群4个库,每个库16张表,总计256张表

  • 库路由策略:mid %16

  • 表路由策略:(mid%512)/32

公式:

  • 中间变量 = MID % (库数量*表数量)

  • 库路由 = 中间变量 % 库数量

  • 表路由 = 取整(中间变量 /库数量) 

Image3-3 库表策略示意图

我们采用的是不清洗老数据的方式,好处是老的订单数据依然走老库,这样能节省一部分清洗数据的工作量。

梳理sql:

项目中的sql有些是不满足分片条件的,所以我们是要提前梳理项目中的sql的。

  • 通过 druid 界面 可以统计到所有运行的 sql

  • 配合静态扫描sql工具

  • DBA 拉取 SQL

  • 人工查看代码 当梳理出对应所有 SQL

针对没有分片键的SQL进行改造,不确定的SQL进行验证,不支持的SQL给出处理方式

当然如何进行迁移也是很重要的步骤,我们是采用下面的步骤,如图3-4所示

  • 历史数据归档,不做迁移,老数据修改依旧路由到老库

  • 切读写请求,即将读写流量请求引入到新系统中

  • 回写数据,binlog 监听新数据库回写到老系统中,并进行校验 。

Image

3-4 不停机迁移示意图

最后总结下整个分库分表的步骤:

  • 根据容量(当前容量和增长量)评估分库分表个数 

  • 选key(均匀)

  • 分表规则(hash或range等)

  • 梳理sql并验证

  • 执行(一般双写)  

在整个交易系统完成分库分表后,彻底解决了数据库的瓶颈问题,历经多次大促压测突发流量等场景都没有出问题,保障了整个平台系统的稳定性。

三、总结

在经过调研链路优化、异步下单改造、数据库分库分表后,整个交易系统的性能得到了较大的提升,也较为顺利地支撑了历次大促活动,后续我们也会继续对一些历史系统(比如票务系统)进行改造升级来提升用户体验。