一、业务场景
在开始讲解之前,我先为大家介绍一下B站的业务场景。B站的业务大体上可以分为以下几类:
1、点播类业务
点播类业务就是大家经常看的视频以及稿件之类相关的业务,这类数据使用场景的特点有:
数据一致性要求较高
耗时敏感
流量大
可用性要求高
2、直播类业务
直播类业务对应B站的S12、跨晚、拜年祭等,有以下几个特点:
数据一致性要求较高
热点数据,如S12的主播房间
平时流量中等,大型直播流量会呈现爆炸性增长
可用性要求高
3、游戏类业务
数据一致性要求较高
耗时敏感
流量大
可用性要求高
4、电商类业务
如B站本身的会员购,这类业务的要求如下:
数据一致性要求较高
热点数据,集中在秒杀场景及热门番剧
平时流量中等,热门番剧及商品会呈现爆炸性增长
可用性要求高
5、支付类业务
数据一致性要求极高
可用性要求高
流量不大
二、架构演进
介绍完B站的业务场景之后,接下来是B站整体数据库的架构演进历史。
1、1.0阶段
1.0阶段对于所有互联网公司而言,其实都有类似的架构——简单的主从,所有流量集中在一个主库上。另外,与以前使用的商业数据库类似场景——单实例多库。这种架构在公司刚起步的时候是比较方便的,便于业务的快速迭代,但是随着流量的增长,会出现以下几个问题:
1)单机的性能瓶颈
服务器的CPU、内存、存储的限制我们不可能一直垂直升级,从而出现了我们第一个架构演进的小版本——读写分离和一主多从,此场景有两个核心要求:
数据一致性要求较低
数据敏感度要求较低
满足以上两个要求的场景可以很好地规避因MySQL主从复制存在的延迟所带来的问题,同时又可以满足业务快速增长带来的流量压力。
2)各业务互相影响
随着业务的发展,各个业务之间的互相影响推动了我们架构的第二个小版本出现——按照业务库进行迁移拆分。
基于读写分离和业务库维度的拆分还是无法避免各个功能模块的互相影响。在这种情况下,架构1.0阶段的第三个小版本应运而生——基于业务的功能维度进行拆分,将一个X库拆分为n个库,拆分完之后分布在不同的实例。在每个不同的实例下,我们会有不同数量的从库支撑业务的流量增长,以满足大部分场景的业务需求。现在B站也有很多业务采用类似的架构,通过进行垂直业务拆分满足我们的业务增长。
2、2.0阶段
架构2.0阶段——水平拆分。成熟、稳定、定制的Proxy是水平拆分的利器,而一个符合要求的Proxy是需要时间进行打磨。为满足业务的快速发展,我们选择在业务层实现,也就是我们在代码层实现路由,虽然配置时会比较繁琐,但能够满足大部分业务场景,很多互联网公司也有类似的阶段。在业务侧进行水平拆分之后,我们其实面临着一个新的问题——跨实例查询。
3、3.0阶段
1)第一个阶段
进入B站演进的3.0阶段,我们引入了TiDB,将之前业务层面的分片数据通过TiDB本身的DTS同步到TiDB集群,从而满足了大部分业务的查询需求。同时我们在部分场景的业务下直接尝试使用TiDB。
引入TiDB之后,基于B站的特点,我们对其进行了本地化定制。由于TiDB Server是无状态的,而官方对于如何路由到每个节点也没有一个通用的解决方案。因此我们结合 B站的基础平台能力,将TiDB Server全部在PaaS上进行容器化,同时把我们的服务发现能力和TiDB Server进行整合,并对相应语言的SDK进行改造,从而实现了TiDB Server的负载均衡,解决了TiDB Server本身的瓶颈,如:故障切换、业务快速感知节点变化、连接数等。
2)第二个阶段
到了3.0的第二个阶段,我们已经把Proxy打磨为一个很成熟的产品,同时为满足支撑了异地多活的场景,我们还定制了DTS,把我们数据库的部署从同城多活直接做到了异地多活,也就是两地三中心的架构。
首先,在DTS方面,我们也基于B站本身技术栈的特点做了大量的定制,与其它公司开源的组件有部分不同。例如冲突检测,我们提供了多种可选择的规则,包括基于特定字段的以及全字段匹配的,同时对于冲突字段数据的R数据处理,我们一般会有两种途径:
一是直接冲突的场景,将不重要的数据直接打入到我们的队列里,也就是我们公司之前的Data Bus里;
二是业务方可以基于打到Data Bus里的数据做冲突数据处理,通过DTS提供一个接口将数据回写到特定的机房,因为当需要把数据重新写入数据库时,也是需要做防回环的处理,所以我们DTS上提供一个接口供业务使用。
其次,在主从切换的时候,由于两地三中心要保证数据可以进行来回切换,切换期间虽然是全局进行,但是一些边缘场景下仍然会存在数据冲突的问题。所以我们也提供了一个在主从切换下数据冲突以及相关信息的打捞队列,实现二次处理的功能,这也是我们中间 DTS提供的一个能力。
Proxy的能力与各家主流的功能是类似的,都能够支持读写分离、分库分表、限流、黑白名单等。对于Proxy的部署,我们采取了两种方案:
一是集中式部署,类似于大家常说的网关模式,能够便捷地进行统一的限流及资源的调控;
二是Sidecar模式,应用层在使用方面比较简单,直接配置本地IP即可,但是同时已带来其他问题,如全局的管控(限流、连接等)、成本等。
三、架构设计
接下来为大家介绍的是B站对于不同数据量的场景的架构设计理念。
1、大型直播活动
整体概括起来有以下四种类型:
1)高并发写入
高并发写入考验的是主库的写入能力和从库的复制能力。
2)高并发查询
高并发查询一般都会引入缓存的能力,缓存主要涉及以下几种:
分布式缓存主要解决容量的问题;
Local Cache在应用层提供能力,在应用本地可以缓存部分数据,但是数据可能存在不及时的问题;
多级缓存能够缓解爆炸性增长的流量带来的压力。
3)实时排序
实时排序最直观的场景就是观众在直播间刷礼物的时候展示出来的名次,为保证时效性以及顺序,我们一般会采用Redis有序集合。
4)预期外突发流量
预期外突发流量对于我们而言考验的主要是应用层的快速扩容以及如何对流量进行削峰,同时保证数据库比较平稳地写入,也就是异步写入的场景。今年最明显的预期外突发流量场景是佩洛西事件,比我们平常的流量大了将近5倍。
2、电商大促
整体上归纳下来有以下几个特点:
1)秒杀场景
秒杀场景主要涉及合适的选型和请求最简化。基于公司的基建进行定制,才可以实现更好的性能和体验。
2)订单
订单有很明显的冷热数据特征。一般情况下,我们的订单会被进行一年前、两年前以及实时订单的不同拆分。这块对于数据库而言考验的是数据的归档及查询能力。
3)库存
库存与秒杀场景存在一定关联,但并不是完全相关。秒杀场景会涉及到库存,但是库存在平常也会一直使用,因此两者不能进行强挂钩。
库存场景主要在于保证减库存的准确性,以及减少用户端在访问时可能存在的冲突,另外是一致性的问题,也就是在秒杀和减库存时不能出现超卖的情况,避免对商家造成亏本。
4)流量削峰
流量削峰与大型直播赛事遇到的突发流量是不同的,因为这一部分流量是我们已知的,已经预估好会有多少流量,因此我们一般会进行队列处理以及做分层。
前面介绍了大型直播赛事和电商大促两个典型场景,我们做了一部分数据库架构设计以及与应用端的联动。下面介绍我们真正进行数据库架构设计时,需要考虑哪些关键点。
3、数据
首先需要考虑数据,按照我们数据类型的使用场景,我们可以将数据分为以下三种:
1)配置型
配置型类似于我们的数据字典以及一些权限配置,特点包括:
量小
几乎无事务依赖
读多写少
如果需要对配置型数据进行高并发访问,只需要加缓存即可,不需要做过多处理。
2)日志型
日志型数据包括交易流水、订单状态等,我认为日志型数据也可以称为流水型数据,特点包括:
量大:无法避免,因为我们需要记录中间各部状态;
无事务依赖:我们后续进行的更多是查询而很少更改;
写多读少:读的比例一般是写的几十分之一。
3)状态型
数据量:与业务有关,状态型数据可以理解为我们的订单,以及直播场景里刷礼物的扣减情况;
事务强依赖:必须保证用户下单成功之后的库存扣减,以及用户给主播打赏之后平台的扣减和主播收到的礼物;
读多写多:与用户的进程挂钩,写和读的场景都比较多。
综上所述,对于数据一般通过数据量、事务和读写请求三个维度进行判断,从而对数据进行规整和梳理,对比上述我列出的三种数据类型,可以得出数据的特定类型。有了数据类型之后,我们就可以考虑进行下一个阶段,即业务对数据库的要求。
4、业务
业务对数据库选型的要求相对而言比较多,包括事务、性能、扩缩容、高可用、迁移。
1)事务
对事务的要求需要基于数据类型进行判断。
2)性能
一些业务对耗时比较敏感,也就是性能要求比较高,要求必须在多少毫秒以内将数据结果反馈回来。那么在进行数据库选型时,我们需要考虑该数据库能否承载这么高的性能反应。
3)扩缩容
如果业务要上一个新业务,要考虑满足一年至两年的增长的需求,因此数据库的扩缩容能力非常重要。如果之前申请的数据量比较大,但是业务发展没有达到预期,那么数据库需要缩容,所以这一方面对于数据库选型也是有要求的。
4)高可用
高可用需要进行取舍,如果要保证数据的强一致性,以及性能的稳定性,必须舍弃一部分东西,具体要与业务沟通和协调,从而保证实际效果符合业务要求。
5)迁移
迁移不仅是业务代码的改造,从A类数据库迁到B类数据库还需要考虑数据库的迁移成本,以及能否支撑同构和异构。对于业务而言,业务更多考虑的是迁移带来的业务改造成本,一般业务会比较喜欢协议无变更、基础操作语法不变的平滑迁移。
5、数据库
数据库我们要考虑的关键点有:
1)事务
如果你想要强事务依赖,可以用传统型数据库,以及现有的NewSQL,比如TiDB、OceanBase等。如果不考虑事务,数据库选择会更多,比如Redis、MongoDB,主要取决于具体的使用场景和数据库要承担的能力。
2)性能
每一种数据库的性能不同,以关系型数据库和非关系型数据库为例,MongoDB和MySQL两者的性能差别是很大的,依然取决于数据库要承担的能力。
3)扩缩容、高可用
扩缩容和高可用不需要进行过多的解释,因为高可用是DBA选择数据库的硬性要求。
4)迁移
这一部分的迁移与业务的迁移存在差异,业务的迁移主要考虑业务改造成本,数据库的迁移需要考虑以下三点:
数据是否一致
数据迁移时是否有增量
数据迁移会对业务产生什么影响
如果业务允许直接一刀切,那么方案则比较简单;如果业务要求无损,那么如何评估方案也是需要大家进行考量的。
5)备份/还原
如果可能出现数据需要恢复的场景,则必须考虑备份/还原的能力。我们一般会更倾向于做物理备份,因为物理备份还原比较快,但是一些数据库没有提供物理备份的能力,如MongoDB。Redis我们也不会做持续化的备份,因为会导致性能的严重下降。
6)容灾
容灾是第一部分B站数据库架构演进我们提到的两地三中心和同城多活需要具备的一个能力。
7)稳定性
数据库的要求是能够平稳地对外提供服务,因此稳定性非常关键。
8)成本
我们不可能为了保证性能无限地往数据库里加机器,因为成本会很高。同时需要考虑开源数据库和商业数据库的选择,在一定程度上商业数据库的性能比同等规格的开源数据库更好,但是需要考虑维护成本和二次定制化能力的成本。
9)定制化
商业数据库有时不会让我们做更多的定制化开发,但是这会给我们的上下游依赖带来一个问题,因为大部分场景我们会依赖于类似MySQL的binlog,下游的刷缓存能力以及大数据的实时数仓能力都需要依靠binlog去往下游,也就是CDC能力。那么这一方面也是数据库选型需要进行评估的重要能力。
6、策略
1)多维度综合考虑
数据库架构选型并不是从一个维度考虑的,每个数据库有自身的使用场景和特点,因此数据库架构选型需要从多个维度综合考虑,包括数据的维度、业务的真实诉求、DBA团队能提供的数据库能力,以及公司对于数据库的支撑能力,主要是公司其他团队如开发和平台类支撑。
2)满足未来三到五年需求
数据库架构例如扩缩容能力,必须满足未来3~5年的需求,而不是频繁地迭代和更新,否则对业务而言是有损的。
3)稳定为主
数据库需要平稳运行,而不是天天宕机,因此数据库架构选型需要以稳定为主。
上图的右侧是目前B站的数据库团队使用的数据库占比,可以看出:
Redis、MySQL分别占比第一和第二;
占比第三的是MC,因MC无高可用,这一方面需要从业务层进行设计,如MC异常后的回源能力;
其他数据库相对数量较少。
总体来说,B站的数据库特点是Redis和MySQL为主,其它数据库主要是基于我们的使用场景进行选择和提供。
四、稳定性
今天主要是想向大家介绍B站万亿级数据库选型与架构设计实践,所以需要考虑数据库如何提供稳定性能力。
1、高可用
在提供稳定性方面,主要是如何保证数据库高可用。BRM是我们基于B站的业务特点自研的MySQL高可用组件,在该架构上我们提供了两个功能节点Leader和Follower,能够对集群内的所有节点进行管理和探活。不管在哪个节点进行注册,我们都可以将其注册到整个集群。因为内部有一个网关会把所有请求转发到主节点,同时再分发到剩下的Follower节点上。
Leader和Follower都参与投票决策,用以规避因网络抖动问题导致BRM误判数据库不可用,然后由Leader节点根据投票结果判断该节点到底是否宕机。
整体概括起来,我们自研的BRM会有以下六个核心功能:
多节点部署:解决MHA单点风险;
支持跨机房:跨机房部署解决因网络异常引起的误切风险;
支持权重:B站的数据库有单机房、同城多活和异地多活,如何保证切到想要的节点上。通过对不同节点设置权重,实现类似MongoDB一样的基于权重的选主能力;
多节点投票决策:通过多个BRM节点对同一个实例探测,满足多数节点一致才判定实例不可用;
专线抖动误切预防:通过多机房多节点部署我们可以预防因专线抖动导致的主节点误切,也可以避免跨机房专线异常造成的误判;
熔断机制:如果出现机房宕机的情况,我们可以先切一部分,查看故障发生的原因,确认没有问题之后再把熔断机制放开。
2、预警
保证系统的平稳运行,也涉及到预警的能力。对于数据库的预警,真正比较具有可预测性和可观察性的是慢查询。数据库的CPU和IO之类的也可以作为参考,但是会存在一定的误判,所以我们的方案是针对慢查询,并且做了一套慢查询预警体系。
首先对于DB层的慢查询,我们做了流式的采集上报和实时分析。在实时分析之后,可能会存在误报的情况,因为如果集群在常态情况下,每天固定某个时刻都会出现比如100条慢查询,那么此时是否该报,其实这本身是一个业务某个时间点的特定行为,不会影响整体行为,所以需要将其屏蔽。针对这一方面,我们引入多次线性回归,通过多次线性回归实现了对偶发性的抖动的过滤,不同业务级别环比倍数、持续性增长(未到阈值倍数,但持续增长或存在)慢查询的预警,并且基于规则引擎实现自定义处理。
3、Proxy
通过对Proxy的大量使用,我们可以实现针对某个数据库、某个服务、某类SQL指纹进行拦截、限流、熔断,以阻止某些异常流量打崩数据的场景,也可以做比较轻松状态下的读写分离。
我们也可以做多机房路由,将机动架构下的数据流量转发到主库,同时能够动态发现拓扑结构的变化,新增或删除从库以及节点的变化都比较易于发现。
同时我们可以去做更精细化的Sidecar模式,从而减少业务技术与能力,通过Sidecar模式使用Proxy,可以满足大家在大量场景下的能力。
4、多活
多活是为了保证在一个机房挂掉之后,我们可以有另外的机房支撑这一方面的能力,我前面讲到的Proxy、BRM以及DTS等都是用于满足多活的诉求。通过多活我们可以保证最大能力的冗灾,同时对用户的影响达到最小,当一个机房挂掉之后,影响的用户可能只有一部分,快速将用户全部导流到另外一个机房可以为用户提供平稳的使用体验。
五、效率
最后是自动化效率的问题,不管是TiDB这种原生的分布式数据库还是我们基于Proxy和业务层自研的分布式数据库能力,同时比如Redis这种超大规模集群,我们现在经常会超过Redis本身的上限,因gossip通信机制,如果节点数量过大会导致节点间的心跳请求将带宽占满,所以我们的自动化如何提供效率?以下是自动化运维演进的方向:
当前我们仍然处于自动化运维的阶段,自动化平台能力的核心有四个方面,分别是资源管理研发自助、运维操作和风险管理。
自动化运维平台
1、资源管理
资源管理简单理解就是资源如何进行分配,有多个维度:
主机管理;
Operator管理:无论是否上k8s都要提供Operator的管理能力;
资源池管理:涉及到如何提高机器使用度的问题;
资源报表:涉及到账单能力,通过账单可以明确告诉业务哪个地方使用不合理,哪个成本可以节省,以及哪个架构可以调整。
2、研发自助
日常情况下,研发有很多事情需要做,例如查询、导入、加字段以及健康检查等。资源申请指的是我们办了一些比较简单的常规业务,他们可以基于我们前面讲到的策略进行匹配后选择数据库。到DBA审核的时候,我们会评估他们写入的内容是否合理,保证不会出现由于架构设计失败引发重构的问题。
3、运维操作
集群管理、实例管理和数据管理是一些比较日常的运维操作,整体上由平台化进行支撑,大部分可以通过自动化解决,不需要人工进行管理。
4、风险管理
风险管理包括监控与告警、健康度报表以及接入信息脱敏和存储信息脱敏。B站涉及到电商和支付方面,需要对一些数据和用户信息进行大量的脱敏,通过数据扫描保证数据的合规。