B 站 713 事故后的多活容灾建设|TakinTalks 大咖分享
「社区发起人推荐语」——
1.分布式系统无法保障绝对可用,相信大家都碰到过软件系统长时间不可用。面对类似问题,美国经济学家⽶歇尔·渥克提出了灰犀牛理论,用灰犀牛⽐喻⼤概率且影响巨⼤的潜在危机。
2.如果你也面临复杂系统稳定性保障的难题,推荐阅读本文,武老师给你讲述 B 站如何遭遇、盯紧、应对稳定性”灰犀牛“的故事,希望对你有一定启发。
——杨德华
作者介绍
B 站在线 SRE 负责人-武安闯
「TakinTalks」稳定性技术交流平台特聘讲师,2016 年加入 B 站,深度参与 B 站微服务拆分、云原生改造、高可用建设、SRE 转型和稳定性 体系落地等项目 ,如 Oncall、问题管理 &复盘、高可用建设、多活容灾、混沌工程、SLO 运营、容量管理等 。当前关注 B 站 SRE 的稳定性体系建设和推广,对 SRE 的实践有深入的探索与思考。
上周 B 站技术发的“713 事故”复盘文章爆了,很多小伙伴都在关注我们 B 站的高可用建设,其实一直以来我们在这个方向做了很多的努力和尝试。看了文章的小伙伴都知道事故是由于 SLB 层面故障引起的,我们最终是通过多活进行服务恢复的,当然这其中也存在很多其他的问题,今天我想就以下几个方面来跟大家分享我们是怎么从故障中汲取经验并进行优化的。
1.B 站的高可用方案是怎么做的?
2.做了高可用方案,就能防止系统不出故障吗?
3.“713”事故后,针对多活容灾做了哪些优化?
一、B 站的高可用方案是怎么做的?
故障的分析和复盘是我们 SRE 永远绕不开的工作,对于 SRE 来说,想要定位问题必须要对系统架构和业务架构有足够的了解,这是一切工作开展的基础。我将分享 B 站的高可用方案相关建设,为了便于大家理解,这边先给大家介绍一下 B 站的系统架构。
1、先来看看整体系统架构
B 站的架构主要包含用户访问、接入层、服务层、中间件/平台以及基础设施。
用户访问层:用户访问是多端的,有 APP、Web、多屏,包括 OTT 电视等;
接入层:主要有三部分,我们的动态 DCDN、第三方的商业 CDN 以及七层的 SLB 负载均衡,SLB 下面还有一层 API GW;
服务层:第一层是服务的 BFF 网关也是服务的 Interface 层,网关层下面是我们服务的 Service 层;
中间件/平台:比如核心的中间件缓存、DB、可观测系统,还有我们的 KV 和对象存储、CMDB 和业务流程平台等;
基础设施:包含 Paas、Iaas、混合云等。
2、每一层架构做了哪些高可用方案?
每一层架构都会有不同类型的常见故障,针对不同层面的问题和架构有不同的高可用方案,下面我会针对接入层、服务层、中间件这 3 个层级的高可用方案展开说一说。
2.1 接入层的高可用
前面有提到我们接入层有 DCDN、SLB 和 API GW,那在实际的架构里面,用户会基于 DNS 和 HTTP DNS 来访问我们的 DCDN 的节点,然后 DCDN 回源时会由边缘的 POP 点来做流量的汇聚并进入我们多个机房,因为我们做了多活的架构,所以是多个可用区的模式。
在这里面可能出现的故障有网络故障、组件故障、服务故障以及机房故障常见 4 种,比如说用户访问 DCDN 节点时运营商网络故障或者是 CDN 节点回到机房的骨干网故障都属于网络故障,而我们“713”故障就是组件层面的 SLB 的故障;服务层的故障就比较多了,服务的代码 bug、性能过载都会导致故障,机房故障就不太常见了。
针对以上这些常见的故障,目前常见的高可用方案大概有这些:
1)针对网络故障的方案
在 DNS 故障的时候,很多公司都会想到降级到 HTTP DNS;
对于端上来说 APP 请求解析到一个地区返回多个 DCDN 节点,让移动端来做一个最佳的选路;
当我们 APP 在访问 DCDN 时出现网络层面的故障,我们可以降低到第三方的 CDN,对我们自己的 DCDN 做容灾;
当 CDN 回源的时候会走我们的 pop 点进行流量汇聚,我们的 pop 点有多个可以做线路的互备;
2)针对组件以及服务故障的方案
因为我们“713 故障”是 SLB 层的问题,所以在针对这个部分的高可用方案会做的多一些。
SLB 向后段转发的时候是可以发现多个可用区里的服务的,包含 API GW 以及没有走 API GW 的其他服务;
当单可用区的节点故障了,是可以自动降级到其他可用区的节点的;
当 SLB 所代理的服务出现异常的时候,我们也可以做对应的 API 的降级、熔断和限流等;
对 SLB 故障的处理流程做了优化,之前新建一套集群包括配置公网 IP 到检验完成需要花一个小时的时间,现在我们可以做到 5 分钟重建一套集群并完成配置与下发。
API GW 层面我们把很多 SLB 的能力给下放了,API GW 也可以返现多可用区的服务节点,当出现故障时也能自动降级到其他可用区,同时它也支持 API 的降级、熔断和限流等;
2.2 服务层的高可用
SLB 是南北向的流量架构,但服务层的流量其实是东西向的流量,服务层的高可用方案可能更偏向于微服务侧的治理。对于 SRE 来讲,是必须要知道自己所负责业务系统的治理能力的,不然在业务系统出现故障的时候,你就只能干等着了,不能给业务一个合理的止损建议和决策。接下来就详细说说服务东西向流量的高可用应该怎么做。
我们的服务发现是通过 Discovery 发现组件来实现的,采用的是多可用区的部署,比如 Service B 在可用区 A 和可用区 B 都有部署,部署是 k8s 多 pod 模式,分布在不同的物理机上。由于采购的机器批次不一样,机器有新有老就会导致算力不一致。如果流量过载怎么办?两个可用区其中一个可用区故障了又该怎么办呢?我们这里有一些常见的故障预案跟大家分享一下。
1)P2C 算法
针对算力不一致的问题,我们内部在做微服务调用时有一个 P2C 的算法,它的核心逻辑是在服务间调用的时候,服务端会返回自己的目前的 cpu 使用率,客户端可以分析服务的响应 RT、等待的 response、成功率等来选择最优的 server 节点来进行动态的权重调整。这就能保证即使不同节点的算力不一样,但最在终实例层面或者容器层面的 CPU 使用率是均衡的。
我们服务也是有熔断的策略,熔断比较常见这边就不展开叙述了。
2)降级
由于我们的服务是部署多可用区的,当一个可用区的服务故障时我们会通过服务发现来调用其他可用区的服务,如果这个服务所有可用区都故障了,我们就会采用内容质量降级的策略,具体分为服务降级、内容降级 2 种具体场景。
服务降级就是本来请求服务 B 的,不请求服务 B 转而请求服务 C;内容降级就是将数据在本地缓存下来,当服务不可用了就从本地缓存来响应服务。比如我们 B 站的首页推荐功能,当推荐服务不可用的时候,我们会降级到一些热门的稿件让用户有视频可以看,虽然这个视频不是用户真正喜欢看的,但至少保证用户有视频可看不会出现白屏或者空窗的情况。
3)限流
在服务间调用的时候遇到这种流量过载一般会采取限流,B 站微服务间限流有两种模式,一是全局流控,是分布式的全局限流,通过微服务的框架拦截层面来实现的。它的限流对象是东西向的流量调用,比如说 HTTP、grpc、mysql 数据库的限流。另一种是单节点的动态流控,叫 BBR 的限流,它是基于 Google BBR 拥塞控制算法来实现的,它会基于我们服务单节点的 cpu 使用率、成功率、RT 去动态决定这个节点现在的请求要不要处理,进行自适应的限流保护。
2.3 中间件的高可用
除了服务间的调用,服务对中间件的高可用也特别重要,因为服务间调用和中间件依赖故障,基本上就是服务中最高频率的两种故障了。
图上可以看到我们核心服务的中间件架构,我们的服务经常会用到缓存来进行数据加速、读 cachemiss、读 DB,写的场景一般是通过 MQ 来做异步持久化,然后会有专门的 Job 去消费。在更新缓存的时候我们会有另一个场景,数据库做了主从同步,有一个 canal 的组件会来消费我们从库的 binlog,再把消费过来的数据写到 MQ 里面,然后再通过 Job 来刷新我们的缓存。
从这个架构里可以看到我们用到的核心中间件有 3 个,一个是缓存,我们内部几乎所有的服务都会用到,还有消息队列和 DB。在这套架构里面我们的数据是保证最终一致的,因为 B 站的场景很多数据对用户来讲并不需要强一致,比如说弹幕、评论、收藏这种数据,只要做到最终一致就可以了,当然电商和支付的场景就不适合这种架构了。
1)缓存中间件的高可用
对于缓存来讲,我们目前支持 Redis Cluster、Redis、Memcache3 种模式,但在我们生产的实际使用里边,我们是建议先用 Redis Ciuster,然后再用 Redis 的单节点,最后再用 Memcache。因为 Memcache 是不支持集群架构的,同时它也没有数据持久化的模式,日常中使用中也没有办法进行数据迁移。
B 站在 2017-2018 年的时候,很多业务是直连 Redis Cluster 的,用的不同的 SDK,有 java、Php、C++等。每一种语言当时都出过 bug,每个部门使用 Redis Cluster 的业务基本都炸过一次。
Redis 在 3.0 和 4.0 版本的时候是一个单线程的服务模型,升级到 6.0 之后正式引入了 I/O Thread 多线程,在之前单线程的模型下用短连接的形式去请求 Redis,它的性能下降会特别严重。我们在物理机上做过测试,单核的场景,长连接能跑到十几万的 QPS,如果是短连接的话会下降近 10 倍,只有一两万的 QPS。
当时我们有个拜年纪的活动,2016、2017 年参加过的同学应该知道,它是一个点播的活动,活动一开始用户会给视频充电,这个充电的服务是一个 java 的服务用到的是 Jedis SDK,SDK 的连接池有 maxtotal 和 maxidle 两个配置,当连接数量超过 maxidle 的值以后,后续的连接就变成短连接了,导致活动一开始用户大量充电,服务就挂了。
从那之后我们就开始建设 proxy 代理,通过 sidecar 模式来部署,近两年我们的 Redis Cluster 就再也没有出现过那种大规模的故障了。因为 sidrcar 模式部署需要容器,为了降本增效我们今年又新增了 proxyless sdk 的部署模式。
2)MQ 中间件的高可用
MQ 主要是做数据的临时持久化和异步写 DB 的,它是一个集中式代理部署的模式,提供 redis 以及 gRPC 协议的访问,当时我们的代理是使用 go 语言的 kafka 的 sarama sdk 来实现的,也是各种炸。2020 年我们 B 站经常做一些运营活动所以用户的流量增长的很快,MQ 层面的连接数、topic 增长的都很快,代理方面压力也很大,动不动就故障。后来我们微服务团队优化了 sarama 的机制,核心服务也做了降级能力,可以绕过 MQ 通过 RPC 方式跟 job 直接通信,实现容灾。
3)Mysql 数据库的高可用
数据库 mysql 这一层是由我们专门的数据库 DBA 负责的,现在也是用一个 proxy 代理、 sidecar 模式去部署的。这种模式上线之后实现了读写分离,对业务侧是透明的。我们最常用的就是对异常 SQL 的拦截能力,比如说管理后台有运营同学查了一个数据触发了 mysql 或者未压测上线的慢 sql,然后导致数据库过载,影响到我们 C 端的服务的时候,可以通过 SQL proxy 来把这个 SQL 做一个拦截,及时恢复我们的数据库,让 C 端用户可以正常使用。同时通过 proxy 联动内部的 BRM 的高可用组件,当我们数据库或者某个节点不可用了,它可以自动感知并切换。
二、高可用方案能防止系统不出故障吗?
B 站有了上面这么多高可用能力,我们的业务还会不会炸呢?
答案是肯定的。
因为 2021 年“713”的时候,我们就出现了一个线上大规模的故障。这里给大家再简单的同步一下,我们这个故障的过程,感兴趣的可以回顾一下详细的文章《2021.07.13 我们是这样崩的》。
在事故的过程中我们也发现了许多的问题,从结果来看可以知道我们服务快速止损靠的就是多活,所以说多活是我们机房级别故障时容灾止损最快的一个方案了。在这次故障之后呢,我们详细复盘了我们的多活架构,发现里面存在一些问题:
多活元信息是没有平台管理的。我们哪个业务做了多活业务、是什么类型的多活、是同城的还是异地单元化、哪些业务是哪些 url 的规则支持多活的、当前多活的流量调度策略是什么、用户是随机回到我们多个机房的,还是要基于用户的 ip 或者用户的一些设备 ID 来做路由的策略……这些没有地方维护,我们当时只能用文档来维护,这导致信息割裂的特别严重,信息的更新也不及时。
多活切量能力是完全依赖我们 CDN 运维。因为我们 B 站的多活流量调度是在边缘的 DCDN 侧来实现的,常见的流程是,SRE 提出一个切量的需求来告诉 CDN 的同学,同时告诉他们要切哪个域名、哪一个 URL 规则,然后 CDN 发起变更开始切量,同时同步到 SRE 和研发,让 SRE 和研发同学一起来观察服务的状态和多机房的流量。
假如在过程中漏了一个接口,或者漏了一条规则,那整个流程要再走一遍,导致我们当时的多活切量效率极差,并且容灾的效果也不好,基于这些背景我们定了优化两个方向:一是多活的基础能力建设,二是我们多活的管控能力提升。
三、“713”事故后,多活做了哪些优化?
1、梳理相关元信息
我们哪些业务做了多活?应用的是什么多活模式?在多活里每个机房的定位是什么?这些相关的元信息我们是没有的,所以我们首先做的事情就是梳理相关的元信息。
1.1 业务信息
前面有提到我们哪些业务做了多活,这些信息我们是没有的,所以一开始就着手于业务模型的梳理与定义。我们之前的业务模型应用的是三级结构,是一级是部门,二级是项目,三级是应用。项目这个级别有时代表服务的组织,有时代表服务的业务,信息比较混乱导致不能反映出业务真实的组织和相关信息。
后来我们对模型进行了拆分,应用以业务维度做聚合,业务再向上关联的时候,分为两个视角,一个视角是组织,这个组织就是你真实的组织架构,用于我们的权限、审批、预算和成本相关的场景。另一个视角是业务所属的业务域。对业务来讲它是一个多活架构的管理单元,也是多活的一个治理单元。比如说我们要推多活覆盖的时候就会按业务维度进行。
1.2 多活模式
CRG 的概念是蚂蚁多活的概念,CRG 代表了 Gzone、Rzone、Czone 三种模式,B 站引入了这个概念。
Gzone 的模式下用户间的数据都是共享的,B 站的视频播放、番剧播放、稿件信息、直播间,都偏向于平台侧的数据,这种业务场景,都可以做成 Gzone 的模式。
Rzone 的模式也就是单元化的模式,他更适用于用户侧的流水型的数据,比如评论、弹幕、动态、支付都是流水型的数据。
Czone 模式介于 Gzone 和 Rzone,也是在用户间做数据共享,但它的可用区是可以做本地读写的,可以接受一定的数据延迟与不一致性。
1.3 机房定位
B 站有多个机房,但定位是有些混乱,在多活的场景里我们做了重新的梳理。将物理机房按照逻辑进行划分,映射到不同的可用区,以上海机房为例,四个机房分为两个可用区,这两个可用区就用来做 Gzone 的服务部署。因为两个可用区都在上海,做同城双活时网络延时特别低,一般情况下 1 毫秒左右,基本就不存在服务延时的问题了。
2、组件的多活能力建设
B 站的很多业务场景都是适合做同城双活的,因为很多数据都是偏向于用户间共享的。下图右边是我们现在的同城双活模式架构图:由我们的 DCDN 层面也就是多活的 Router 层面来做我们的用户侧的请求路由分发(目前是基于用户的 Mid 或者用户的设备 ID 来做路由分发)分发到不同的可用区。不同可用区的服务会访问我们的缓存、KV 存储和 DB 存储,这两个存储都会通过我们 Proxy 的模式去访问。中间还有一个 GZS 的组件,通过它来进行多活的全局管控。
在 713 故障之后,我们也对这些组件都做了一定优化。
DCDN 层面:我们已经支持通过用户的 ID 或者用户设备 ID 来 Hash 路由到不同的机房,同时支持用户流量在多机房的动态权重,1/100 到 1/99,或者 2/98 都是可以动态支持的。
Service 层面:之前我们做同城双活时没有实现写,那个时候还没有 Proxy,现在有了 Proxy 就可以让业务方来做本地的写改造了,然后通过存储的 Proxy 来路由到主可用区。
Proxy 层面:通过 proxy 同时支持 KV 和 DB 两种存储模式,并实现读写分离和 failover。
GZS 层面:前面有讲到我们多活是没有全局管控的,那 GZS 就是来做我们的多活业务、业务应用和 API 的元信息管理的,包含业务、多活、切量的编排。
3、Invoker 管控平台的搭建与应用
为了做好 GZS 我们内部研发了一个 Invoker 管控平台,它会与 B 站内部的业务树、Dashboard、Comet 多个平台联动,从 4 个层面来优化我们之前的多活流程。
多活规则元信息获取方式优化
针对平台本身进展多活和演练
多活编排、接入流程的优化
审批、巡检、可观测能力的加强
3.1 通过平台规范多活流程
那基于这个平台我们是怎么落地多活的切量的呢?
基本的流程是编排—切量—审批—巡检—可观测,接下来挑选几个重点跟大家具体分享。
1)多活定义编排
多活编排首先要确定这次多活的编排业务范围是一个业务还是一个业务域,评论、弹幕这些都是业务,他是边界比较明确的产品可以进行编排,而业务域是业务的集合,比如直播、电商这些就是业务域,它下面由哪些规则、哪些 DB 与 KV 的存储要全部编排下来工作量就会非常大,所以业务域是不支持编排的,我们采取的方式是业务先做好编排,再基于业务的元信息来聚合业务域。
其次要确认业务它用了哪些 DB,具体的多活模式是什么,Gzone 还是 Rzone。
到了接入层的编排:它使用的多活规则是什么,一个服务/产品在对外的时候一定有多个 url 以及规则,这里需要编排出哪些规则是支持多活的;这个多活规则在 CDN 层的调度它是使用用户的 MID 还是设备 ID,还是基于其他方式来进行多活的流量分发;这些信息都是要明确编排的。
B 站目前消息层面的编排是暂不支持的,而数据层面的编排主要就是我们的 KV 和 DB 了,当这些编排都完成之后就会触发一个审批,由研发和 SRE 同学来确认这次的编排是否正确。
2)多活切量编排
编排完成之后我们就可以发起切量了,切量时候你还是要先选择你的业务范围,是业务还是业务域。然后你要选择你切量的权重,每个可用区权重是多少,1:1 甚至是 1:99 都是可以精确控制的。
切量的编排还要明确这一次切量是否要切我们的存储,存储是否要切 DB 是否要切 KV。大多数都是只切 DCDN 的流量层面就可以了,切 DB 的话可能会产生数据冲突和修数据的问题。
切量编排完成之后会触发切量审批,审批会过 SRE 以及 CDN 的负责人,同时通过工单的形式来明确一些操作,把每一个编排每一个层面的内容有哪些变更都会 Diff 出来,让大家直观地看到我们这次变更了哪些内容。
3)切量可视化
上面的工作都完成之后就正式进入切量了,那第一步我们会进行一些风险预检,它主要分为 3 个部分:
容量巡检:容量巡检的是我们应用的 cpu,缓存的 cpu、DB,包括 KV 的 cpu 等。在这里我们还会进行连接池的巡检,看看我们应用访问 DB 的连接池怎么样,应用访问缓存的连接池怎么样。之前我们在内部切量的时候踩过很多次坑,我们发现服务侧在不同的可用区是有不同的限流阈值的,切量后可用区流量翻倍导致限流阈值被触发影响到用户,所以我们现在还会做一些限流配置的巡检。
延迟巡检:由于我们现在使用了 Gzone 的架构,所以 DB 和 KV 的同步延迟都比较低,像同城双活一般延迟就在一毫秒左右,这部分没有遇到什么问题。
隔离巡检:隔离巡检主要是看我们的 DB 和 KV 的切换是否有跨业务的混用,比如说我要切评论的业务,结果我发现弹幕的业务也在引用这套 DB,那我这个切量的影响范围就不止仅是评论了,那还包含弹幕,这就需要通知到弹幕相关的业务研发同学。
可观测这一块,我们会监控前面巡检的相关指标、业务多活动流量、业务/应用 SLO 指标等内容。巡检相关指标在实际切量过程中也会发生变化,所以需要监控,而实时关注多活流量的状态是为了了解切量是不是符合预期。业务和应用 SLO 的指标的监控主要是应用的成功率、延迟这些应用侧核心指标的一个可视化。
3.2 建设管控平台自身的高可用
切量平台本身我们也有做高和用,是按照同城双活的模式来部署的。针对切量平台本身的高可用工作分为三部分:
第一部分是多活,切量平台按照业务架构的 Gzone 模式做了部署,平台所依赖的核心平台,比如 DCDN、KV 和 DB 平台也做了 Gzone 模式的部署。我们在进行切量演练的时候也会对 Invoke 平台的数据层来做切换的演练,比如切一下 DB 来验证平台否真正做到同城双活的容灾。
第二部分是降级,713 的时候因为登录不了鉴权系统导致我们不能及时处理问题,所以现在我们认证是可降级的,不强依赖我们的登录态。同样我们不能因为审批挂了,导致多活切不了,所以当审批平台挂了,审批是可以降级跳过的;同理假如说我预检不通过,是有风险的,但没有更好的应对措施,也是可以降级强制来做一个切量的。我们也会定期通过对工具平台做这些故障演练,模拟审批挂了或者是预检挂了来看我们平台的降级是否符合预期。
第三部分是效率层面的,DCDN 是肯定要切的,他是部署在全国各地的边缘节点,公网的稳定性一定是比不上内网的,所以针对这块场景我们也做了相关优化。比如说失败可以快速重试、部分节点失败可以先跳过,先保证可以切的节点全部切掉,以达到我们的预期,最后再来统一处理失败的节点。在切量的过程中如果发现服务有性能/容量问题,或者多机房承载不了流量导致不符合预期都可以做快速的一键回滚。
4、优化的效果:多活效率提升 10 倍
目前我们全部的多活业务都已经接入 Invoke 切量平台了,包括我们的主站、直播、OGV、电商等。我们在生产做了很多次演练,因为不在生产进行演练,当故障真正发生的时候,你是不敢切量的。H1 期间我们就在生产大概切量演练了 60+,过程没有出现过什么问题,也发现了很多持续优化的点,比如说巡检可观测。
现在的切量流程是 SRE 跟研发确认我们切量的业务、组件、流量、执行,现在一个切量三到五分钟就完成了,而在此之前我们的切量都是半个小时起步的,效率大大提升。
写在最后,故障其实并不可怕,我们要做的也不仅仅是修复故障,更重要的是要善于从踩过的坑中总结经验,找到优化和提升的方向,只有这样系统才能稳健发展。
关注公众号回复关键词【7161】获取讲师 PPT
讲师演讲视频回放链接:https://news.shulie.io/?p=5019&cnel=ff726
评论