B站服务稳定性建设:高可用架构与多活治理

吉翔 2023-05-30 09:54:19
本文根据吉翔老师在〖deeplus直播:甩掉技术债包袱,B站的SRE体系建设与转型实践〗线上分享演讲内容整理而成。(文末有回放的方式,不要错过)




分享概要

一、高可用多活架构

二、业务多活改造

三、多活管控与治理

 

一、高可用多活架构

 

相较于传统的灾备单活的架构,多活指的是在同城或异地的一个数据中心建立一套与本地生产系统部分或完全对应的一套服务,再进行流量调度,使所有可用区的一个应用同时对外提供服务。

 

灾难发生时,借助多活的业务快速实现流量切换,可以避免或大幅降低用户受到故障的影响。较为典型的两种方案类型主要包括同城多活和异地多活。

 

图片

 

 
1.高可用整体架构

 

图片

 

 
2.多活的方案类型

 

图片

 

我们使用蚂蚁的CRG多活定义类型,CRG分别代表GZone 、RZone、CZone的三种模式。

 

  • GZone模式

用户间的数据可以共享,比如B站的视频播放、番剧播放、稿件信息直播间等数据偏向于平台侧,这类业务场景都可以做成GZone模式。

 

  • RZone模式

单元化模式,它更适用于用户侧的流水型数据,比如社区的评论、弹幕、动态及支付,比较适合做成Rzone模式。

 

  • CZone模式

介于GZone和 RZone之间,做用户间的数据共享,但它支持在当地的多可用区内可读可写,并且能够接受一定的数据延迟和不一致性。

 

B站具有多个数据中心,但整体定位比较混乱,所以我们在多活场景里重新做了梳理和划分。

 

以上海的机房为例,我们有4个机房,整体分为了2个可用区,用来做我们GZone的服务部署。因为两个可用区都在上海,一般延时情况都在一毫秒左右,所以在同城双活方面无需担心出现网络延时问题。这也是目前主要在推进的一个业务改造覆盖的方案。

 

另外,我们还在江苏设置机房,作为 RZone的服务部署,但由于距离上海较近,无法模拟出远端异地高延迟的情况,同时容灾能力存在不足。因此,目前我们仍在验证在华南或是在一些L2、L3的边缘机房进行的异地多活的方案,后文将详细介绍。

 

1)同城多活

 

同城多活是指我们通过流量调度,在应用多数据中心的同时,对外提供服务。同城多活的机房距离较近,所以耗时一般会控制在5毫秒以内,在数据层面具备主从同步和切换的能力。因为同城多活的网络延时比较低,所以在业务改造过渡的阶段,服务的调用不会增加过多耗时。

 

图片

2)异地多活

 

异地多活也是通过流量调度,同时对外提供多数据中心的服务。

 

如果是距离比较远的两个机房,那么全程耗时可能会达到30毫秒以上。这方面很难使用单集群的模式来提供服务,而多集群的模式下,又会因数据一致性的复杂度造成问题。

 

通常有两种解决方案,一种是要求业务进行单元化的改造,它需要具备单元数据的分片能力,而单元间需要双向同步数据;另一种是面向一些可以接受延迟的业务,它们在主可用区可写可读,然后在异地建立一个只读的单元,支持读的流量分流。

 

图片

3) B站的同城多活

 

因为很多数据都偏向于用户间共享,所以B站的很多业务适合做成同城多活。下图为B站同城多活的架构图,由一个流量层即DCDN层面,对用户维度的一些信息来进行Hash路由,然后进行请求分发。

 

目前,B站主要是基于向用户的一个MID或者是用户的一些设备ID来做路由,分发到不同的可用区,不同可用区的用户能够访问在该可用区的缓存,比如KV存储和DB存储。这两个存储通过Proxy模式访问,可以就近读本机房,写可回源到上海可用区一。另外,这套方案中又加了一个Invoker组件,进行多活的全局管控。

 

图片

 

①流量接入层

 

我们通常将流量的管控面分为南北向,即从用户侧到我们内部服务的方向。另一方向是东西向,一般指内网的服务间调用的流量。其中南北向这个部分就是由我们的DCDN、SLB,也就是7层负载,还有API网关。

 

在实际的架构中,用户从端上过来访问,会基于DNS或是HTTP DNS来访问我们的DCDN的节点,在DCDN回源时会有POP点来做流量的汇聚,再进入到我们的可用区。我们的DCDN基于边缘CDN节点来做整体的路由管控,实现动态的最佳选路。

 

我们自研了一个Picker模块,可支持用户的一个MID或者是设备ID Hash到不同的可用区,同时支持用户流量权重在多机房进行全动态和灵活的调整,99:1或日常的50:50都可以灵活定义。至于7层负载SLB,它能通过服务发现多可用区的下游服务和APIGW的节点,支持API的降级和全局限流的能力。

 

APIGW本身是一个容器部署的服务,和我们内部其他BFF层一样,支持发现多可用区的服务节点,所以我们将其放在了南北向和东西向流量衔接的这一层,使它支持单可用区服务的故障重试。目前我们也逐步将SLB侧的管控能力转移到GW层面统一进行管理,它可以支持服务的API降级、熔断和限流,包括我们和客户端联动的流控能力,避免服务故障时客户端疯狂重试导致进一步压垮服务。

 

APIGW也部署于PaaS平台,支持HPA弹性扩容,可以应对一些流量突增的情况。

 

Discovery是我们的服务发现组件,在服务启动后,它会向注册中心进行注册,并且定期更新Review,同时对服务下游依赖的信息如HTTP或者是GRPC的地址端口,及其可用区的信息进行拉取和缓存。

 

在调用下游时,客户端即SDK层面会默认选取同可用区的一个节点。若下游在该可用区未做部署,比如一些非多活的业务,则会回到主可用区调用。所以在同城多活的场景下,要求服务能够在本地完整地支持写请求的处理,以及强弱依赖必须在本地进行部署,而弱依赖则会被允许应用跨专线回主机房。另外,Discovery也能对东西向的流量权重进行管理,包括一些灰度验证等。

 

图片

 

②缓存一致性

 

为避免业务直连缓存实例,缓存接入方面我们提供统一的Proxy。SDK 在各开发语言上并不统一,所以可能会引发短连接的风暴,并引起性能下降。我们通过用Proxy来长连接缓存实例,Proxy支持 Sidecar和ProxylessSDK等模式提供接入。B站缓存的使用场景不支持多机房的同步,在多活场景下要求服务订阅同机房的一个数据源,对缓存进行数据更新或者删除,维护缓存数据最终的一致性。

 

  • 保证缓存一致性的处理方式

 

Cache Aside的模式即DB/KV的存储加上缓存,因为数据最终都会落到存储上,所以这类情况下业务只需要通过Canal订阅同可用区存储Binlog变更然后投递消息队列,由业务的的Job解析处理后删除或更新缓存。

 

图片

  • 纯缓存场景

 

一类是热数据的情况,业务在多可用区可能通过Job定时刷新,把数据从DB或者是其他的离线数据源中拉取,随后存放到缓存中。在多活情况下,这类场景要求独立部署,再通过job做定时更新,目的是使业务和缓存在单个可用区内实现闭环的调用。

 

另一类就是将缓存当做存储的用法,这个用法一般不推荐,出于性能考虑,B站不支持做持久化的缓存场景。若作为存储使用,即使是Cache Aside这样的模式,在遇到一些较大规模的故障时,仍旧会出现数据丢失的情况。

 

所以在这方面建议业务改造为Cache Aside的模式,或者是通过KV进行存储。KV是B站自研的分布式KV存储系统,本身的数据存储在SSD中,所以它的性能必然不如Redis Cluster内存的性能有优势。但我们做过评估,当业务的QPS小于10万,基本上可以迁移到我们的KV存储系统内。KV存储也支持Redis协议的一些常用命令和操作,它的最大特性是支持机房的多活。

 

  • 分布式锁场景

 

例如涉及一些分布式锁或者说处理幂等,这类情况就建议把业务改造为KV。因为KV本身支持如Cache这样的命令,并且它的数据持久化,同时它也支持跨可用区同步,与多活场景比较契合。

 

③消息多活

 

我们提供了三种模式以根据不同场景进行选择:

 

  • 各可用区的消息自产自销

 

这个模式下Topic间不会进行消息同步,需要生产端投递本可用区的 Topic,再由本可用区的下游直接进行消费。这种模式比较适用于 Service至Job异步消息的场景,或者是ServiceA投递给已多活的下游ServiceB这样的情况。

 

  • 多可用区全量消费(Global)

 

这个模式下Topic间会通过Sync的一个组件双向同步消息,每一个topic中有两个可用区,即可用区一+可用区二的全量消息,然后同步全量消息。它适用于ServiceA投递给未多活下游ServiceB的场景,比如离线或大数据,下游一般要继续在可用区一消费全量数据。

 

  • 全量消费(Global)自定义不消费可用区

 

从Global模式衍生出来的一种模式,允许选择任意一个可能区不消费,比较适用于消息由解析Binlog触发全量数据的情况。在可用区二的下游,需要考虑消费后的幂等处理,包括一些存储或者下游的调用放大的情况下,可选择其中一个可用区不消费。这个模式的好处是,出现可用区故障时,可通过切换消费模式快速恢复整个消息层的消费。

 

这三种模式的设置和Topic间消息同步的开启,不会做任何绑定。在多活过程中,我们和业务共同做场景梳理,包括梳理明晰涉及到哪些消息队列,哪些相关下游在确定整个消费模式的制定等。

 

④数据访问/存储层

 

存储层同样由我们的中间件支持,我们提供了MySQL,TiDB以及Taishan(KV)三种Proxy,整个机制没有区别,它具备多可用区路由的能力,并且能够具备实例拓扑的自动发现和动态切换能力。

 

  • GZone:对于同城双活场景下数据需要全局存放的情况,即一主多从这样的模式,服务在主可用区基本上能读写GZone的存储,本可用区有可读的从实例,在可用区也有从实例,通过Proxy将写的路由回源到主机房。而在一些强一致的场景下,也提供了SQL Hint级别的配置或在连接串请求头中增加一些master或者PRIMARY的配置,从而实现强制读主的场景。

     

  • RZone:在RZone单元化这一方面,业务要做数据分片,分片的数据需要完成双向同步,本可用区的一个分片能够实现读写操作的封闭。

     

  • DTS同步组件:可以实现数据的双向复制,目前整体延迟小于10秒,同时支持数据冲突检测,发送冲突时支持暂停同步或者说异步把通知投递到消息队列,再由业务来处理冲突。

 

图片

 

二、业务多活演进

 

 
1.多活业务划分

 

在多活架构中通常会按业务域进行多活业务划分,面向C端还是B端分别是两种不同的业务形态。

 

图片

 

 
2.B站的业务同城多活改造

 

  • 单活:B站大部分业务最初也是单活模式,即服务只在单个可用区做部署,缺乏Proxy支持,缓存和DB大部分都是客户端直连。业务发布接口需要在SLB和CDN上分别做配置,并进行规则发布。

 

  • 读多活:它是一个过渡方案。虽然我们的服务开始在另一个可用区做整个部署,但在流量层面,我们只能支持读接口的接流,而且接口大部分都通过 CDN侧或者SLB侧进行流量的转发,还有一些缓存或消息队列的一些组件未完成多活改造,存在跨机房调用的情况。

 

  • 同城多活:我们提供了各类组件的 Proxy接入支持,使业务在可用区二能支持处理写的请求,并借助Proxy支持整个容灾的自动切换。缓存的一致性也是这个方案里的一个重点,要求业务必须通过Canal+Job这样的方式维护缓存的一致性,包括消息的生产消费都达成了可用区内的闭环。

 

至于GZS的组件,则由Invoker平台和APIGW对服务接口的发布进行统一的管理。

 

我们认为,现阶段去做单元化改造成本较高,收益可能并不大。所以基于同城多活的方案,衍生出异地多活的架构,目前我们正在验证该异地多活方案。

 

华南可用区是我们相对远端的一个可用区,用来承接一部分读的流量,它整体的架构是对标可用区二读多活的模式。在服务发现层面依旧通过Discovery的组建实现当前可用区的调用,核心依赖在华南可用区完成整个部署,一些弱依赖则可返回上海可用区做调用。

 

在数据存储同步方面,原来的两个上海可用区距离不远,我们使用的基本是一些原生的同步组件,它本身也能满足单向同步。实现远端后,要继续满足DTS的单向同步能力。而缓存和消息队列,则继续遵循最终一致以及自产自销的原则来实施。

 

三、多活管控与治理

 

 
1.多活元信息规则治理

 

我们初期在CDN上的一些规则偏向非标,有大量的正则写法,所以我们在做第一步时就对多活元信息的规则进行了治理,APIGW接入时也应用了前缀路由的模式,以方便做后续的统一切流管理。另外,也保留了一部分非标的多活规则,能够提供自定义的规则录入,例如前端或Web端的规则。回源或缓存的规则等非多活规则就继续存放于CDN层面。

 

 
2.Invoker平台多活&强依赖降级&演练

 

Invoker平台也有一些依赖,我们将其中的业务资源元信息存放在CMDB中,还有一些存放登录态、鉴权和工单审批的系统。我们在建设Invoker的同时,对这个平台做了GZone模式的部署。我们对它的核心依赖都做了故障演练,对每一个依赖也做了降级方案。之前也遇到过管理后台在故障时无法登录的情况,所以留了超管权限,并做了异地部署,以保证Invoker平台在故障时可用。

 

 
3.多活流量管控

 

在多活编排接入流程化这一方面,基于跟业务做改造和做接入的经验,总结了一些方法论,完成了平台化和功能化。

 

图片

 

1)编排、预检与观测

 

做业务接入时,首先要对多活业务进行定义,由此平台侧能让我们基于CMDB选择业务,定义它的多活类型,从而编排整个接入层的切量规则和数据层的切量规则。目前我们支持消息层的切换和东西流量的编排。在进行日常的切量演练或故障演练前,我们会做前置的检查,例如容量巡检、sos层面的监控、数据库的连接池、业务在SLB平台的限流配置等,要提前检查其状态,并预检DB和KV主从同步的延迟情况。

 

2)切量

 

在切量的过程中,我们会观测业务多活流量的变化与新引入的SLO体系的相关指标。在这个平台做了集成后,最后一个环节则是将多活验证的思路落实到平台,例如要求多活流量在单可用区内做到闭环,而针对一些弱依赖则要求业务去做故障演练。

 

 
4.多活定义编排

 

多活定义编排是指,能够选择一个业务去定义它的多活模式,确定它是CZone、GZone还是RZone的方式,确定它的服务具体分布的地域位置和可用区。

 

在接入层,除了有APIGW比较标准的一些前置规则,也支持自定义规则的录入,以及它在DCDN层面流量调度的比例。

 

我们在数据层面支持Taishan(KV)和DB的编排录入,包括下游的消费任务,如Canal或 KV的同步任务这一部分的切换。

 

图片

 

上图是执行切量过程的界面,在切量申请时会选择一个业务,然后选择它的一个切流纬度,包括它要求的切流比例,选择是否同时去切换我们的存储,执行切流是切哪些规则,对切量对象选择进行配置。

 

 
5.切流预检与可视化

 

以往由人工完成的这一流程,如今被整合到平台中,支持容量延迟和限流的预检。在切流过程中,我们能观测服务、缓存和DB等容量情况。

 

目前在做的一些接入如SLO观测则包括链路的依赖、整体链路上下游的SLO情况等,我们也在做平台侧的接入。

 

图片

 

 
6.多活有效性验证

 

1)依赖展示

 

同城多活方案强调在机房内能够实现读写流量的处理,以便在故障时快速恢复。因此在有效性验证方面,比较注重依赖的排查。我们能在平台侧展示依赖,同时能展示服务的下游依赖和组件依赖,这方面用到了Trace的能力。

 

2)依赖检查

 

针对需要确定哪些依赖是强依赖,哪些依赖是弱依赖的类似情况,我们会对依赖进行检查和打标。

 

3)流量闭环

 

打标后,会进行流量闭环的检查,通过打标和依赖发现这些信息,然后进行跨可用区调用的发现,直到确认核心依赖在可用区内是闭环的,弱依赖则要求业务排期做演练。

 

4)故障演练

 

这部分由框架SDK支持,它能够实现依赖的自动发现和自动的故障演练,最终会输出一份报告,确认是否都符合预期。若与预期不符,再进行改造和演练。

 

图片

 

 

Q&A

 

Q1:同城双活架构下应用层做了双多活,基础架构层还有必要双活吗?

 

A1:我觉得有必要。所有的技术组件在设计时就要考虑双活模式,因为业务的多活基于组建本身的高可用,如之前介绍的Invoker组件的多活,它对于鉴权工单审批的依赖,我们都需要去考虑它的多活设计,以及在真正出现单可用区故障的时刻,我如何能登录这个平台去实现多活管控。

 

Q2:多活如何保证数据一致性?

 

A2:这还需要根据数据中心的分布和业务形态来进行方案的选择。若是同城,只要考虑写主库读从库的模式,强一致性的需要强制读写主库。若是比较远距离异地多活,需要进行数据分片、单元化双写的改造,并且能够接受部分数据的同步延迟,因为跨地域的耗时增加属于物理层面,无法避免。只能根据一个合适的业务场景,适配相应的多活方案。关于缓存数据,前面也介绍了缓存一致性的几种维护方式。

 

Q3:高可用多活的成本如何把控,过程中对ROI的考虑是怎样的?

 

A3:要从以下两个方面分析:一是收益。发生故障时,就会感知到多活的收益是有价值的。B站在21年经历过一次比较大的故障,当时恢复最快的原因是已经做了多活的业务,哪怕是只做了读多活的一些业务,也恢复得很快。可能因为B站读场景比较多,所以对用户感知层面读场景的快速恢复能大大缓解用户侧的焦虑和客户投诉的情况;

 

二是成本。一是注意资源增长,因为现在做的都是同城多活模式,同城的机房服务都是在线提供,所以它不存在资源的空好浪费。日常我们也会准备一些资源冗余来应对突发情况和高峰期,即换个思路,可以通过资源的弹性或混部来进行运维成本的增加。如Invoker平台,它就是大大降低运维成本增加的工具,它能把一些人工完成的事情都转化为平台的功能层面,出现故障时能够一键执行切流。所以要把收益和成本两件事结合考虑。

 

图片

获取本期PPT,请添加群秘微信号:dbachen

↓点这里回看本期直播
http://z-mz.cn/6F0OW

最新评论
访客 2023年08月20日

230721

访客 2023年08月16日

1、导入Mongo Monitor监控工具表结构(mongo_monitor…

访客 2023年08月04日

上面提到: 在问题描述的架构图中我们可以看到,Click…

访客 2023年07月19日

PMM不香吗?

访客 2023年06月20日

如今看都很棒

活动预告