brpc/docs/cn/auto_concurrency_limiter.md
2022-12-14 20:13:26 +08:00

157 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 自适应限流
服务的处理能力是有客观上限的。当请求速度超过服务的处理速度时,服务就会过载。
如果服务持续过载,会导致越来越多的请求积压,最终所有的请求都必须等待较长时间才能被处理,从而使整个服务处于瘫痪状态。
与之相对的,如果直接拒绝掉一部分请求,反而能够让服务能够"及时"处理更多的请求。对应的方法就是[设置最大并发](https://github.com/brpc/brpc/blob/master/docs/cn/server.md#%E9%99%90%E5%88%B6%E6%9C%80%E5%A4%A7%E5%B9%B6%E5%8F%91)。
自适应限流能动态调整服务的最大并发,在保证服务不过载的前提下,让服务尽可能多的处理请求。
## 使用场景
通常情况下要让服务不过载只需在上线前进行压力测试并通过little's law计算出best_max_concurrency就可以了。但在服务数量多拓扑复杂且处理能力会逐渐变化的局面下使用固定的最大并发会带来巨大的测试工作量很不方便。自适应限流就是为了解决这个问题。
使用自适应限流前建议做到:
1. 客户端开启了重试功能。
2. 服务端有多个节点。
这样当一个节点返回过载时,客户端可以向其他的节点发起重试,从而尽量不丢失流量。
## 开启方法
目前只有method级别支持自适应限流。如果要为某个method开启自适应限流只需要将它的最大并发设置为"auto"即可。
```c++
// Set auto concurrency limiter for all methods
brpc::ServerOptions options;
options.method_max_concurrency = "auto";
// Set auto concurrency limiter for specific method
server.MaxConcurrencyOf("example.EchoService.Echo") = "auto";
```
## 原理
### 名词
**concurrency**: 同时处理的请求数,又被称为“并发度”。
**max_concurrency**: 设置的最大并发度。超过并发的请求会被拒绝返回ELIMIT错误在集群层面client应重试到另一台server上去。
**best_max_concurrency**: 并发的物理含义是任务处理槽位天然存在上限这个上限就是best_max_concurrency。若max_concurrency设置的过大则concurrency可能大于best_max_concurrency任务将无法被及时处理而暂存在各种队列中排队系统也会进入拥塞状态。若max_concurrency设置的过小则concurrency总是会小于best_max_concurrency限制系统达到本可以达到的更高吞吐。
**noload_latency**: 单纯处理任务的延时不包括排队时间。另一种解释是低负载的延时。由于正确处理任务得经历必要的环节其中会耗费cpu或等待下游返回noload_latency是一个服务固有的属性但可能随时间逐渐改变由于内存碎片压力变化业务数据变化等因素
**min_latency**: 实际测定的latency中的较小值的ema当concurrency不大于best_max_concurrency时min_latency和noload_latency接近(可能轻微上升)。
**peak_qps**: qps的上限。注意是处理或回复的qps而不是接收的qps。值取决于best_max_concurrency / noload_latency这两个量都是服务的固有属性故peak_qps也是服务的固有属性和拥塞状况无关但可能随时间逐渐改变。
**max_qps**: 实际测定的qps中的较大值。由于qps具有上限max_qps总是会小于peak_qps不论拥塞与否。
### Little's Law
在服务处于稳定状态时: concurrency = latency * qps。 这是自适应限流的理论基础。
当服务没有超载时随着流量的上升latency基本稳定(接近noload_latency)qps和concurrency呈线性关系一起上升。
当流量超过服务的peak_qps时则concurrency和latency会一起上升而qps会稳定在peak_qps。
假如一个服务的peak_qps和noload_latency都比较稳定那么它的best_max_concurrency = noload_latency * peak_qps。
自适应限流就是要找到服务的noload_latency和peak_qps 并将最大并发设置为靠近两者乘积的一个值。
### 计算公式
自适应限流会不断的对请求进行采样当采样窗口的样本数量足够时会根据样本的平均延迟和服务当前的qps计算出下一个采样窗口的max_concurrency:
> max_concurrency = max_qps * ((2+alpha) * min_latency - latency)
alpha为可接受的延时上升幅度默认0.3。
latency是当前采样窗口内所有请求的平均latency。
max_qps是最近一段时间测量到的qps的极大值。
min_latency是最近一段时间测量到的latency较小值的ema是noload_latency的估算值。
注意:当计算出来的 max_concurrency 和当前的 max_concurrency 的值不同时,每次对 max_concurrency 的调整的比例有一个上限,让 max_concurrency
的[变化更为平滑](https://github.com/apache/incubator-brpc/blob/master/src/brpc/policy/auto_concurrency_limiter.cpp#L249)。
当服务处于低负载时min_latency约等于noload_latency此时计算出来的max_concurrency会高于concurrency但低于best_max_concurrency给流量上涨留探索空间。而当服务过载时服务的qps约等于max_qps同时latency开始明显超过min_latency此时max_concurrency则会接近concurrency并通过定期衰减避免远离best_max_concurrency保证服务不会过载。
### 估算noload_latency
服务的noload_latency并非是一成不变的自适应限流必须能够正确的探测noload_latency的变化。当noload_latency下降时是很容感知到的因为这个时候latency也会下降。难点在于当latency上涨时需要能够正确的辨别到底是服务过载了还是noload_latency上涨了。
可能的方案有:
1. 取最近一段时间的最小latency来近似noload_latency
2. 取最近一段时间的latency的各种平均值来预测noload_latency
3. 收集请求的平均排队等待时间使用latency - queue_time作为noload_latency
4. 每隔一段时间缩小max_concurrency过一小段时间后以此时的latency作为noload_latency
方案1和方案2的问题在于假如服务持续处于高负载那么最近的所有latency都会高出noload_latency从而使得算法估计的noload_latency不断升高。
方案3的问题在于假如服务的性能瓶颈在下游服务那么请求在服务本身的排队等待时间无法反应整体的负载情况。
方案4是最通用的也经过了大量实验的考验。缩小max_concurrency和公式中的alpha存在关联。让我们做个假想实验若latency极为稳定并都等于min_latency那么公式简化为max_concurrency = max_qps * latency * (1 + alpha)。根据little's lawqps最多为max_qps * (1 + alpha). alpha是qps的"探索空间"若alpha为0则qps被锁定为max_qps算法可能无法探索到peak_qps。但在qps已经达到peak_qps时alpha会使延时上升已拥塞此时测定的min_latency会大于noload_latency一轮轮下去最终会导致min_latency不收敛。定期降低max_concurrency就是阻止这个过程并给min_latency下降提供"探索空间"。
#### 减少重测时的流量损失
每隔一段时间自适应限流算法都会缩小max_concurrency并持续一段时间然后将此时的latency作为服务的noload_latency以处理noload_latency上涨了的情况。测量noload_latency时必须让先服务处于低负载的状态因此对max_concurrency的缩小是难以避免的。
由于max_concurrency < concurrency时服务会拒绝掉所有的请求限流算法将"排空所有的经历过排队等待的请求的时间" 设置为 latency * 2 以确保用于计算min_latency的样本绝大部分都是没有经过排队等待的。
由于服务的latency通常都不会太长这种做法所带来的流量损失也很小。
#### 应对抖动
即使服务自身没有过载latency也会发生波动根据Little's Lawlatency的波动会导致server的concurrency发生波动。
我们在设计自适应限流的计算公式时考虑到了latency发生抖动的情况:
当latency与min_latency很接近时根据计算公式会得到一个较高max_concurrency来适应concurrency的波动从而尽可能的减少“误杀”。同时随着latency的升高max_concurrency会逐渐降低以保护服务不会过载。
从另一个角度来说当latency也开始升高时通常意味着某处(不一定是服务本身,也有可能是下游服务)消耗了大量CPU资源这个时候缩小max_concurrency也是合理的。
#### 平滑处理
为了减少个别窗口的抖动对限流算法的影响同时尽量降低计算开销计算min_latency时会通过使用[EMA](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)来进行平滑处理:
```
if latency < min_latency:
min_latency = latency * ema_alpha + (1 - ema_alpha) * min_latency
else:
do_nothing
```
### 估算peak_qps
#### 提高qps增长的速度
当服务启动时由于服务本身需要进行一系列的初始化tcp本身也有慢启动等一系列原因。服务在刚启动时的qps一定会很低。这就导致了服务启动时的max_concurrency也很低。而按照上面的计算公式当max_concurrency很低的时候预留给qps增长的冗余concurrency也很低(即alpha * max_qps * min_latency)。从而会影响当流量增加时服务max_concurrency的增加速度。
假如从启动到打满qps的时间过长这期间会损失大量流量。在这里我们采取的措施有两个
1. 采样方面,一旦采到的请求数量足够多,直接提交当前采样窗口,而不是等待采样窗口的到时间了才提交
2. 计算公式方面当current_qps > 保存的max_qps时直接进行更新不进行平滑处理。
在进行了这两个处理之后绝大部分情况下都能够在2秒左右将qps打满。
#### 平滑处理
为了减少个别窗口的抖动对限流算法的影响同时尽量降低计算开销在计算max_qps时会通过使用[EMA](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)来进行平滑处理:
```
if current_qps > max_qps:
max_qps = current_qps
else:
max_qps = current_qps * ema_alpha / 10 + (1 - ema_alpha / 10) * max_qps
```
将max_qps的ema参数置为min_latency的ema参数的十分之一的原因是: max_qps 下降了通常并不意味着极限qps也下降了而min_latency下降了通常意味着noload_latency确实下降了
### 与netflix gradient算法的对比
netflix中的gradient算法公式为max_concurrency = min_latency / latency * max_concurrency + queue_size
其中latency是采样窗口的最小latencymin_latency是最近多个采样窗口的最小latencymin_latency / latency就是算法中的"梯度"当latency大于min_latency时max_concurrency会逐渐减少反之max_concurrency会逐渐上升从而让max_concurrency围绕在best_max_concurrency附近
这个公式可以和本文的算法进行类比
* gradient算法中的latency和本算法的不同前者的latency是最小值后者是平均值netflix的原意是最小值能更好地代表noload_latency但实际上只要不对max_concurrency做定期衰减不管最小值还是平均值都有可能不断上升使算法不收敛最小值并不能带来额外的好处反而会使算法更不稳定
* gradient算法中的max_concurrency / latency从概念上和qps有关联根据little's law)但可能严重脱节比如在重测
min_latency前若所有latency都小于min_latency那么max_concurrency会不断下降甚至到0但按照本算法max_qps和min_latency仍然是稳定的它们计算出的max_concurrency也不会剧烈变动究其本质gradient算法在迭代max_concurrency时latency并不能代表实际并发为max_concurrency时的延时两者是脱节的所以max_concurrency / latency的实际物理含义不明与qps可能差异甚大最后导致了很大的偏差
* gradient算法的queue_size推荐为sqrt(max_concurrency)这是不合理的netflix对queue_size的理解大概是代表各种不可控环节的缓存比如socket里的和max_concurrency存在一定的正向关系情有可原但在我们的理解中这部分queue_size作用微乎其微没有或用常量即可我们关注的queue_size是给concurrency上升留出的探索空间: max_concurrency的更新是有延迟的在并发从低到高的增长过程中queue_size的作用就是在max_concurrency更新前不限制qps上升而当concurrency高时服务可能已经过载了queue_size就应该小一点防止进一步恶化延时这里的queue_size和并发是反向关系