微服务可用性设计之过载保护和限流
过载保护
微服务在保证自己存活之后,再可以通过策略保护自己。例如过载保护和限流。
一些常见的算法
令牌桶算法
是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。
令牌桶算法的描述如下:
- 假设限制
10r/s
,则按照100ms
的固定速率往桶中添加令牌,每100ms
往桶中添加1
个令牌。 - 桶中最多存放
b
个令牌,当桶满时,新添加的令牌被丢弃或拒绝。 - 当一个
n
个字节大小的数据包到达,将从桶中删除n
个令牌,接着数据包被发送到网络上。 - 如果桶中的令牌不足
n
个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
token-bucket rate limit algorithm
: /x/time/rate
优势:
- 简单
- 灵活
劣势:
- 阈值难设定,需要大量的测试
漏桶算法
作为计算工具(The Leaky Bucket Algorithm as a Meter
)时,可以用于流量整形(Traffic Shaping
)和流量控制(Traffic Policing
),漏桶算法的描述如下:
- 一个固定容量的漏桶,按照常量固定速率流出水滴
- 如果桶是空的,则不需流出水滴
- 可以以任意速率流入水滴到漏桶
- 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的
leaky-bucket rate limit algorithm
: go.uber.org/ratelimit
总结
令牌桶和漏斗桶确实能够保护系统不被拖垮,但不管漏斗桶还是令牌桶,其防护思路都是设定一个指标,当超过该指标后就阻止或减少流量的继续进入,当系统负载降低到某一水平后则恢复流量的进入。但其通常都是被动的,其实际效果取决于限流阈值设置是否合理,但往往设置合理不是一件容易的事情。
这些问题是需要考虑的:
- 集群增加机器或者减少机器,限流阈值是否需要重新设置?
- 设置限流阈值的依据是什么?
- 人力运维成本是否过高?
- 当调用方反馈
429
时,这个时候重新设置限流,其实流量高峰已经过了,重新评估限流是否有意义?
这些其实都是采用漏斗桶/令牌桶的缺点,总体来说就是太被动,不能快速适应流量变化。
因此需要的是一种自适应的限流算法,即:过载保护,根据系统当前的负载自动丢弃流量。
自适应
计算系统临近过载的峰值吞吐作为限流的阈值来进行流量控制,达到系统保护。
- 服务器临近过载时,主动抛弃一定量的负载,目标是自保
- 在系统稳定的前提下,保持系统的吞吐量
常见做法:利特尔法则
CPU、内存作为信号量进行节流
队列管理:队列长度、LIFO
可控延迟算法:CoDel
CoDel背后的理论基于对缓冲区影响下分组交换网络中数据包行为的观察。 这些观察中的一些与排队的基本性质和缓冲膨胀的原因有关,另一些与队列管理算法的弱点有关。 CoDel的开发旨在解决缓冲膨胀问题。 [1]
缓冲膨胀[编辑]
数据包在快速网络和慢速网络之间的网络链路传输时速度会变慢,尤其是在TCP会话开始时,突然出现数据包激增并且较慢的网络可能无法快速接受该突发时足够。缓冲区的存在是为了缓解这个问题,它给了快速网络一个存储数据包的地方,让慢速网络以自己的速度读取数据包。 [2] 。换句话说,缓冲区的作用就像减震器一样,将突发到达信号转换为平稳平稳的信号。但是,缓冲区的容量有限。理想的缓冲区大小可以处理突发的通信,并使突发的速度与较慢网络的速度相匹配。理想情况下,冲击吸收情况的特征是在传输突发期间缓冲器中的分组的暂时延迟,之后延迟迅速消失,并且网络在提供和处理分组方面达到平衡。 [2]
TCP拥塞控制算法依赖于数据包丢弃来确定两个通信设备之间的可用带宽 。 它可以加快数据传输速度,直到数据包开始丢失为止,然后降低传输速率。 理想情况下,当它在链接速度上找到平衡时,它会继续加速和减速。 为此,必须及时发生丢包,以便算法可以响应地选择合适的传输速度。 如果数据包保存在一个过大的缓冲区中,则数据包将到达其目的地,但具有较高的延迟,但不会丢弃任何数据包,因此TCP不会减慢速度。 在这种情况下,TCP甚至可能确定连接的路径已更改,并重复搜索新的平衡点。 [3] [4]
拥有一个大而不断的缓冲区,这会导致传输延迟增加和交互性降低,尤其是在查看同一通道上的两个或多个同时传输时,这种情况称为缓冲区膨胀。 可用的通道带宽也可能最终未被使用,因为某些缓冲区可能被缓冲区阻塞,等待数据传送到较慢的目的地,因此可能无法到达某些快速的目的地。
计算峰值时的吞吐
如何计算接近峰值时的系统吞吐?
CPU:使用一个独立的线程采样,每隔
500ms
触发一次。在计算均值时,使用了简单滑动平均去除峰值的影响。Inflight
:当前服务中正在进行的请求的数量。Pass&RT
:最近5s,pass
为每100ms
采样窗口内成功请求的数量,rt
为单个采样窗口中平均响应时间。蓝色:表示CPU的利用率,CPU 浮动大
橙色:使用过去的样本 + 当前样本的浮点数相乘,作为一个简单的滑动窗口统计算法
绿色:也是一种滑动窗口统计算法
通过不同的计算算法,拟合数据更加准确。
自适应算法
- 使用 CPU 的滑动均值( CPU > 800),作为启发阈值,一旦出发进入到过载保护阶段,算法为
(pass * rt) < inflight
- 限流效果生效后,CPU 会在临界值 (800)附近抖动,如果不使用冷却时间,那么一个短时间的 CPU 下降就可能导致大量请求被放行,严重时会打满 CPU
- 在冷却时间后,重新判断阈值( CPU > 800 ),是否持续进入过载保护。
蓝色代表收到的请求,绿色代表处理的请求。
不使用冷却时间的时候,在区间内瞬间处理大量请求,导致CPU被直接打满,再次触发过载保护。
过载保护是当自己临近过载的时候主动卸载一些流量,剩下一些流量就需要哦通过限流控制。
限流
限流是指在一段时间内,定义某个客户或应用可以接收或处理多少个请求的技术。
例如,通过限流,可以过滤掉产生流量峰值和客户的微服务,或者可以确保应用程序在自动扩展(Auto Scaling)失效前都不会出现过载的情况。
- 令牌桶、漏桶针对单个节点,无法分布式限流,全局的流量无法针对性控制,即使配置也是粗粒度
- QPS 限流
- 不同的请求可能需要数量迥异的资源来处理
- 某种静态 QPS 限流不是特别准
- 给每个用户设置限制
- 全局过载发生时候,针对某些异常进行控制
- 一定程度的超卖配额
- 按照优先级丢弃
- 拒绝请求也需要成本,比如返回状态码
限流前后,CPU 利用率的变化可以非常直观的看到。
分布式限流
分布式限流,是为了控制某个应用全局的流量,而非针对单个节点维度。
- 单个大流量的接口,一般会使用
redis
,虽然简单方便,但是容易产生问题- 容易产生热点
pre-request
模式对性能有一定影响,高频的网络往返,对接口性能有影响
可以优化一下:
- 从获取单个
quota
升级成批量quota
。quota
:表示速率,获取后使用令牌桶算法来限制。
- 每次心跳后,异步批量获取
quota
,可以大大减少请求redis
的频次,获取完以后本地消费,基于令牌桶拦截。 - 每次申请的配额需要手动设定静态值略欠灵活,比如每次要20,还是50
如何基于单个节点按需申请,并且避免出现不公平的现象?
初次使用默认值,一旦有过去历史窗口的数据,可以基于历史窗口数据进行quota
请求。
如果经常面临给一组用户划分稀有资源的问题,他们都享有等价的权利来获取资源,但是其中一些用户实际上只需要比其他用户少的资源,此时应该如何优化?
一种在实际中广泛使用的分享技术称作 最大最小公平分享(Max-Min Fairness)。
直观上,公平分享分配给每个用户想要的可以满足的最小需求,然后将没有使用的资源均匀的分配给需要大资源
的用户。
最大最小公平分配算法的形式化定义如下:
- 资源按照需求递增的顺序进行分配:优先分配最少的用户,例如A
- 不存在用户得到的资源超过自己的需求:将A多出来的 0.5,分配给其他节点
- 未得到满足的用户等价的分享资源:将剩余的 8 份资源,分给 B、C、D 三个节点
重要性
每个接口配置阈值,运营用作繁重,最简单的是配置服务级别 quota
,更细粒度的,可以根据不同重要性设定 quota
,也就是引入 重要性(criticality
)的概念:
- 最重要
CRITICAL_PLUS
,为最终的要求预留的类型,拒绝这些请求会造成非常严重的用户可见的问题。 - 重要
CRITICAL
,生产任务发出的默认请求类型,拒绝这些请求也会造成用户可见的问题,但是可能没有那么严重。 - 可丢弃的
SHEDDABLE_PLUS
,这些流量可以容忍某种程度的不可用性。这是批量任务发出的请求的默认值。这些请求通常可以过几分钟、几小时后重试。 - 可丢弃的
SHEDDABLE
,这些流量可能会经常遇到部分不可用的情况,偶尔会完全不可用。
gRPC 系统之间,需要自动传递重要性信息。如果后端接受到请求A,在处理过程中发出了请求 B 和 C 给其他后端,请求 B 和 C 会使用与 A 相同的重要性属性。
- 全局配额不足时,优先拒绝低优先级的
- 全部配额,可以按照重要性分别设置
- 过载保护时,低优先级的请求先被拒绝
熔断
除了服务端做限流,也可以在客户端主动的、善意的做限流,也就是客户端侧的限流,client side throttling
。
断路器(Circuit Breakers
):为了限制操作的持续时间,我们可以使用超时,超时可以防止挂起操作并保证系统可以响应。因为我们处于高度动态的环境中,几乎不可能确定在每种情况下都能正常工作的准确的时间限制。
熔断器以现实世界的电子元件命名,因为它们的行为都是相同的。断路器在分布式系统中非常有用,因为重复的故障可能会导致雪球效应,并使整个系统崩溃。
服务依赖的资源出现大量错误
某个用户超过资源配额时,后端任务会快速拒绝请求,返回 配额不足 (HTTP code 429)的错误,但是拒绝回复仍然会消耗一定资源。
有可能后端一直忙着不停发送拒绝请求,导致过载。
出现这些报错之后,客户端直接 Fast failing
快速报错,不占用服务端资源。
重置熔断
当下游服务恢复正常,可接收请求,此时就需要重置熔断器,让服务恢复正常。但是这个过程不能恢复之后,立马接收所有的请求,否则会立马又触发熔断机制。
max(0, (requests - K*accepts) / (requests + 1))
Google SRE
request
:总请求数
accepts
:成功请求数量
K
:值愈小,算法越激进
结果的值表示一个概率,请求时生成一个随机值,随机值大于,则放行,小于则继续拒绝。
实际请求和放行的请求,是比较契合的。
Gutter
在某种大流量的场景,如果全局依赖 kafka
集群,但是如果出现集群级别的故障,影响范围会非常大。
如果再搭建一套集群,就需要一倍的资源。这个时候使用 Gutter
(渠道)的思想就可以很好的解决。Gutter
的思路是将小部分流量转移到另外一套集群,只需要 10%
的资源的集群。
先写入 kafka A
,当触发熔断,再将数据写入 kafka B
,当熔断恢复,再切换回 kafka A
基于熔断的 gutter kafka
,用于接管自动修复系统运行过程中的负载,这样只需要付出 10%
的资源就能解决部分系统可用性问题。
我们经常使用 failover
的思路,但是完整的 failover
需要翻倍的机器资源,平常不接受流量时,资源浪费。高负载情况下接管流量又不一定完整能接住。所以这里核心利用熔断的思路,是把抛弃的流量转移到 gutter
集群,如果 gutter
也接受不住的流量,重新回抛到主集群,最大力度来接受。
客户端流控
positive feedback
:用户总是积极重试,访问一个不可达的服务。
- 客户端需要限制请求频次,
retry backoff
做一定的请求退让 - 可以通过接口级别的
error_details
,挂载到每个 API 返回的响应里
请求失败退让,失败次数越多,重试间隔越长。
Case Study
二层缓存穿透,大量回源导致的核心服务故障,出现雪崩
- 使用分布式限流,限制所有
MySQL
的请求数量,让缓存慢慢构建,最终实现服务可用
- 使用分布式限流,限制所有
异常客户端引起的服务故障(
query of death
)- 请求放大,疯狂重试
- 资源数放大,疯狂分页
- 通过限制租户的请求
用户重试导致的大面积故障
- 客户端,APP端,预埋
backoff
策略。
- 客户端,APP端,预埋
推荐阅读
大神讲解微服务治理的技术演进和架构实践
Overload control for scaling WeChat microservices
Overload Control for Scaling WeChat Microservices
Sentinel