“雪崩”指的是访问服务集群时绝大部分请求都超时，且在流量减少时仍无法恢复的现象。下面解释这个现象的来源。

当流量超出服务的最大qps时，服务将无法正常服务；当流量恢复正常时（小于服务的处理能力），积压的请求会被处理，虽然其中很大一部分可能会因为处理的不及时而超时，但服务本身一般还是会恢复正常的。这就相当于一个水池有一个入水口和一个出水口，如果入水量大于出水量，水池子终将盛满，多出的水会溢出来。但如果入水量降到出水量之下，一段时间后水池总会排空。雪崩并不是单一服务能产生的。

如果一个请求经过两个服务，情况就有所不同了。比如请求访问A服务，A服务又访问了B服务。当B被打满时，A处的client会大量超时，如果A处的client在等待B返回时也阻塞了A的服务线程（常见），且使用了固定个数的线程池（常见），那么A处的最大qps就从**线程数 / 平均延时**，降到了**线程数 / 超时**。由于超时往往是平均延时的3至4倍，A处的最大qps会相应地下降3至4倍，从而产生比B处更激烈的拥塞。如果A还有类似的上游，拥塞会继续传递上去。但这个过程还是可恢复的。B处的流量终究由最前端的流量触发，只要最前端的流量回归正常，B处的流量总会慢慢降下来直到能正常回复大多数请求，从而让A恢复正常。

但有两个例外：

1. A可能对B发起了过于频繁的基于超时的重试。这不仅会让A的最大qps降到**线程数 / 超时**，还会让B处的qps翻**重试次数**倍。这就可能陷入恶性循环了：只要**线程数 / 超时 \* 重试次数**大于B的最大qps，B就无法恢复 -> A处的client会继续超时 -> A继续重试 -> B继续无法恢复。
2. A或B没有限制某个缓冲或队列的长度，或限制过于宽松。拥塞请求会大量地积压在那里，要恢复就得全部处理完，时间可能长得无法接受。由于有限长的缓冲或队列需要在填满时解决等待、唤醒等问题，有时为了简单，代码可能会假定缓冲或队列不会满，这就埋下了种子。即使队列是有限长的，恢复时间也可能很长，因为清空队列的过程是个追赶问题，排空的时间取决于**积压的请求数 / (最大qps - 当前qps)**，如果当前qps和最大qps差的不多，积压的请求又比较多，那排空时间就遥遥无期了。

了解这些因素后可以更好的理解brpc中相关的设计。

1. 拥塞时A服务最大qps的跳变是因为线程个数是**硬限**，单个请求的处理时间很大程度上决定了最大qps。而brpc server端默认在bthread中处理请求，个数是软限，单个请求超时只是阻塞所在的bthread，并不会影响为新请求建立新的bthread。brpc也提供了完整的异步接口，让用户可以进一步提高io-bound服务的并发度，降低服务被打满的可能性。
2. brpc中[重试](client.md#重试)默认只在连接出错时发起，避免了流量放大，这是比较有效率的重试方式。如果需要基于超时重试，可以设置[backup request](client.md#重试)，这类重试最多只有一次，放大程度降到了最低。brpc中的RPC超时是deadline，超过后RPC一定会结束，这让用户对服务的行为有更好的预判。在之前的一些实现中，RPC超时是单次超时*重试次数，在实践中容易误判。
3. brpc server端的[max_concurrency选项](server.md#限制最大并发)控制了server的最大并发：当同时处理的请求数超过max_concurrency时，server会回复client错误，而不是继续积压。这一方面在服务开始的源头控制住了积压的请求数，尽量避免延生到用户缓冲或队列中，另一方面也让client尽快地去重试其他server，对集群来说是个更好的策略。

对于brpc的用户来说，要防止雪崩，主要注意两点：

1. 评估server的最大并发，设置合理的max_concurrency值。这个默认是不设的，也就是不限制。无论程序是同步还是异步，用户都可以通过 **最大qps \* 非拥塞时的延时**（秒）来评估最大并发，原理见[little's law](https://en.wikipedia.org/wiki/Little%27s_law)，这两个量都可以在brpc中的内置服务中看到。max_concurrency与最大并发相等或大一些就行了。
2. 注意考察重试发生时的行为，特别是在定制RetryPolicy时。如果你只是用默认的brpc重试，一般是安全的。但用户程序也常会自己做重试，比如通过一个Channel访问失败后，去访问另外一个Channel，这种情况下要想清楚重试发生时最差情况下请求量会放大几倍，服务是否可承受。
