B站高可用架构实践

265次阅读  |  发布于3年以前

【导读】本文整理了 B 站在云+社区沙龙分享的高可用架构,一起来学习小破站的稳定性实践吧!

流量洪峰下要做好高服务质量的架构是一件具备挑战的事情,从Google SRE的系统方法论以及实际业务的应对过程中出发,分享一些体系化的可用性设计。对我们了解系统的全貌上下游的联防有更进一步的了解。

负载均衡

BFE 就是指边缘节点,BFE 选择下游 IDC 的逻辑权衡:

当流量走到某个 IDC 时,这个流量应该如何进行负载均衡?

问题:RPC 定时发送的 ping-pong,也即 healthcheck,占用资源也非常多。服务 A 需要与账号服务维持长连接发送 ping-pong,服务 B 也需要维持长连接发送 ping-pong。这个服务越底层,一般依赖和引用这个服务的资源就越多,一旦有任何抖动,那么产生的这个故障面是很大的。那么应该如何解决?

解决:以前是一个 client 跟所有的 backend 建立连接,做负载均衡。现在引入一个新的算法,子集选择算法,一个 client 跟一小部分的 backend 建立连接。图片中示例的算法,是从《Site Reliability Engineering》这本书里看的。

如何规避单集群抖动带来的问题?多集群。

如上述图片所示,如果采用的是 JSQ 负载均衡算法,那么对于 LBA 它一定是选择 Server Y 这个节点。但如果站在全局的视角来看,就肯定不会选择 Server Y 了,因此这个算法缺乏一个全局的视角。

如果微服务采用的是 Java 语言开发,当它处于 GC 或者 FullGC 的时候,这个时候发一个请求过去,那么它的 latency 肯定会变得非常高,可能会产生过载。

新启动的节点,JVM 会做 JIT,每次新启动都会抖动一波,那么就需要考虑如何对这个节点做预热?

如上图所示,采用 “the choice-of-2” 算法后,各个机器的 CPU 负载趋向于收敛,即各个机器的 CPU 负载都差不多。Client 如何拿到后台的 Backend 的各项负载?是采用 Middleware 从 Rpc 的 Response 里面获取的,有很多 RPC 也支持获取元数据信息等。

还有就是 JVM 在启动的时候做 JIT,以前的预热做法:手动触发预热代码,然后再引入流量,再进行服务发现注册等,不是非常通用。通过改进负载均衡算法,引入惩罚值的方式,慢慢放入流量进行预热。

限流

用 QPS 限制的陷阱:

每一个 API 都是有重要性的:非常重要、次重要,这样配置限流、做过载保护的时候,可以使用不同的阈值。

每个服务都要配一个限流,是非常烦人的,需要压测,是不是可以自适应去限流

每个 Client 如何知道自己这一次需要申请多少 Quota ?基于历史数据窗口的 QPS。

节点与节点之间是有差异的,分配算法不够好,会导致某些节点产生饥饿。那么可以采用最大最小公平算法,尽可能地比较公平地去分配资源,来解决这个问题。

当量再大一点的时候,如果 Backend 一直忙着拒绝请求,比如发送 503,那么它还是会挂掉。这种情况就要考虑从 Client 去截流。此处,又提到了 Google 《Site Reliability Engineering》这本书里面的一个算法,即 Client 是按照一定概率去截流。那么这个概率怎么计算?一个是总请求量:requests,一个是成功的请求量:accepts。如果服务报错率比较高,意味着 accepts 不怎么增长,requests 一直增长,最终这个公式求极限,它会等于 1,所以它的丢弃概率是非常高的。基于这么一个简单的公式,不需要依赖什么 ZooKeeper,什么协调器之类的,就可以得到一个概率丢弃一些请求。它尽可能的在服务不挂掉的情况下,放更多的流量进去,而不是像 Netflix 一样全部拒掉。

连锁故障通常都是某一个节点过载了挂掉,流量又会去剩下的 n - 1 个节点,又扛不住,又挂掉,所以最终一个一个挨着雪崩。所以过载保护的目的是为了自保。

B 站参考了阿里的 Sentinel 框架、Netflix 的一些文章等,最终采用的是类似于 TCP BBR 探测的思路和算法。简单说:当 CPU 达到 80% 的时候,这个时候我们认为流量过载,如果此时吞吐量比如 100,用它作为阈值,瞬时值的请求比如是 110,那就可以丢掉 10 个流量。这样就可以实现一个限流算法。

CPU 抖来抖去,使用 CPU 滑动均值(绿色线)可以跳动的没有这么厉害。这个 CPU 针对不同接口的优先级,例如低优先级 80% 触发,高优先级 90% 触发,可以定为一个阈值。

那么吞吐如何计算?利特尔法则。当前的 QPS * 延迟 = 吞吐,可以用过去的一个窗口作为指标。一旦丢弃流量,CPU 立马下来,算法抖动非常厉害。图二右侧黄色线表示抖动非常高,绿色线表示放行的流量也是抖动非常高,所以又加了冷却时间,比如持续几秒钟,再重新判断。

重试

问题:每一层都重试,这一层 3 次,那一层 3 次,会指数级的放大。解决:只在失败这一层重试,如果重试之后失败,请返回一个全局约定好的错误码,比如说:过载,无需重试,发现这个错误码,通通放行,避免级联重试。

重试都应该无脑的重试三次吗?API 级别的重试需要考虑集群的过载情况。是不是可以约定一个重试比例呢?比如只允许 10% 的流量进行重试,Client 端做统计,当发现有 10% 都是重试,那么剩下的都拒绝掉。这样最多产生 1.1 倍的放大,重试 3 次,极端情况下,会产生 3 倍放大。还有在重试的时候,尽量引入随机、指数递增的一个重试周期,大家不要都重试 1 秒钟,有可能会堆砌一个重试的波峰

重试的统计图和记录 QPS 的图分开。问题诊断的时候,可以知道它是来自流量重试导致的问题放大。

某个服务不可用的时候,用户总是会猛点,那么这个时候,需要去限制它的频次,一个短周期内不允许发重复请求。这种策略,有可能会根据不同的过载情况经常调这种策略,那么可以挂载到每一个 API 里面。

超时

大部分的故障都是因为超时控制不合理导致的。

某个服务需要在 1 秒返回,内部可能需要访问 Redis,需要访问 RPC,需要访问数据库,时间加起来就超过 1 秒,那么访问完每一层,应该计算供下一层使用的超时时间还剩多少可用。在 go 语言里,可能会使用 Context,每一个网络请求开始的阶段,都要根据配置文件配置的超时时间,和当前剩余多少,取一个最小值,最终整个超时时间不会超过 1 秒。

通过 RPC 的元数据传递,类似 HTTP 的 request header,带给其它服务。例如在图中,就是把 700ms 这个配额传递给 Service B。

下游服务作为服务提供者,在他的 RPC.IDL 文件中把自己的超时要配上,那么用 IDL 文件的时候,就知道是 200 ms,不用去问。

应对连锁故障

优雅降级:一开始千人千面,后来只返回热门的

QA

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8