百度网盘防雪崩架构实践
点击蓝字,关注我们
01
1.1 百度网盘业务背景介绍
简要介绍一下百度网盘的业务背景
外部视角:用户规模超过10亿,单纯外部用户请求日pv过千亿;
内部视角:实例数超过60w+个,模块数规模过千,单个功能最多涉及100+个模块节点。
在复杂的链路+高并发情况下,短暂的异常就会使得整个系统出现雪崩,即使异常消失也无法自愈,对用户产生不好的用户体验。
于是百度网盘服务端工程方向设计了一套防雪崩的机制,本文先介绍雪崩的技术背景,再介绍相关的解决方案。
1.2 雪崩发生的背景介绍
初始阶段:各种根因 + 诱因 –> 服务过载; 循环阶段:上游重试 —> 服务持续处理无效请求 —> 上游重试 ……
△server读到已经断开连接的socket连接
△ server读到已经断开连接的socket请求数据
从上下游处理视角来说,雪崩并不是局部行为,它会随着链路漫延到整个系统链路上。如下图,client以300ms的超时时间访问server,server在访问A和B之后,已经用掉了300ms,这个时候client已经断开了和server的连接,但是server却继续访问C和D,进行无效的请求。当这种无效请求在整个链路蔓延开,client又在大量的重试的时候,就是整个系统崩溃的时候。
02
从预防/阻止/止损雪崩三个子方向介绍相关实战经验,以及分析它们的优缺点。
2.1 预防
通过各种手段,规避雪崩初始阶段的出现,例如热点治理、长尾治理、分级操作、容量保障等。
2.2 阻止
基本思路:当重试请求数占当前请求数的X%, 就不会再进行重试; 优点:可以避免大量重试导致雪崩死循环; 不足:设置重试率多少才是合理的?另外,这个还需要持续更新,因为下游的容量是在变化的,可能连正常的流量都无法处理(比如实例被缩容了)。
基本思路:不再基于系统层面的内核队列,应用层自己维护队列(比如用一个线程从系统读取连接数据到队列中),记录请求在队列中等待的时间。如下图,队列中有8个请求,代表他们在队列中等待的时间(单位ms),假设上游的执行是200ms,处理一个请求需要100ms,那么第1个到第4个请求明显是失效请求(最大等待时间( = 上游执行超时 - 服务处理时间) < 当前等待时间),等处理完上游都已经断开连接了,就不需要处理了,可以直接从第5个请求进行处理;
优点:通过维护应用层的队列,加上等待时间,快速抛弃无效请求,避免雪崩的发生;
不足:依赖假设上游的访问超时时间以及自己的执行时间,才能准确判断出请求是否已经失效。但是有可能不同的上游的超时时间不一样,自身的执行时间也不稳定性。同在服务在极端负载下,包括读取系统队列的线程也会受限资源,无法及时从系统队列中读取请求,导致读到了无效请求。
【限流】
基本思路:在服务接入层设置一个静态阈值,表示后端服务最大的请求qps,超过这个qps的流量就丢弃请求;
优点:可以避免瞬间突增的流量打垮服务,导致服务不可恢复,可以解决一部分场景问题;
不足:提前设置限流一个静态阈值,和重试率控制一样,不是所有场景都有效果。另外也有运维维护成本,具体如下:
阈值合理性:需要频繁压测才能知道当前系统的瓶颈qps阈值是多少,设置大了,达不到阻止过多流量的效果,设置小了,又有误伤;
故障期间服务处理能力退化:当花费了很多时间压测出来一个系统的qps阈值是100,但是故障期间服务承载的qps可能会退化成80。会有很多原因会导致,比如一部分实例oom导致不可用,上线导致实例启动失败、长尾流量导致部分实例处理能力不足(比如某些视频转码需要更长的时间,同时还是热门视频,负载均衡重试到不同实例上),最常见的是下游请求执行时间变长了。这样导致每秒都需要承载多余的qps请求,累计下来,这个系统迟早被打垮。本质问题还是静态阈值导致的。
2.3 止损
限流:通过多次人工调整限流阈值,使后端请求减少,逐渐恢复; 重启服务:通过重启服务,快速丢弃系统队列中的无效请求。
03
传统解决方案里,包括预防、阻止、止损三个子方向。
一旦处于雪崩状态,是不可逆的,需要比较长的时间去恢复。
所以重点在于预防和阻止,因为预防会有遗漏,更多的精力在于阻止,即处于雪崩初始阶段,有发生雪崩的可能性,需要阻止雪崩进入循环阶段。
业界的做法,其实可以分成两种做法:
减少过载流量:比如限流和重试率控制,剔除服务不能够承载的流量;
减少无效请求:比如队列控制,使得服务处理有效的流量。
这两种做法需要结合,单个做法是会有问题的。
比如减少过载流量,由于下游的处理能力是动态变化的,实际上还是会出现过载,下游会长期处于处理无效请求,无法处理有效的请求。
减少无效请求,大部分场景下是能搞定的,但是如果瞬间的请求量特别大,请求都是属于有效的,内存可能会先达到瓶颈,导致oom了。
下面介绍一下百度网盘的防雪崩架构实践。
3.1 减少过载流量-基于动态熔断
流程说明1 开始请求: 系统接收到一个外部请求。2 熔断器状态判断:- 闭合状态(Closed): 熔断器允许请求通过,继续执行。- 打开状态(Open): 熔断器阻止请求,直接失败或返回预定义的响应。- 半开状态(Half-Open): 熔断器允许部分请求通过,以测试服务是否恢复。3 执行请求:- 请求成功:- 如果处于闭合状态,则重置失败计数。- 如果处于半开状态,则关闭熔断器,恢复正常。- 请求失败:- 增加失败计数。- 如果失败计数超过预设阈值,则打开熔断器,跳闸。4 冷却时间: 熔断器在打开状态后,会等待一段冷却时间,然后进入半开状态。5 半开状态测试:- 请求成功: 关闭熔断器,恢复正常。- 请求失败: 重新打开熔断器,继续等待冷却时间。
3.2 减少过载流量-基于流量隔离
高优流量:比如用户感知明显流量等; 低优流量:比如后台流量、离线计算流量等。
3.3 减少无效请求-基于请求有效性
上游服务A发送请求:基本可以忽略,机器负载问题不再这里考虑; 请求在网络中的耗时:基本可以忽略,网络故障问题不再这里考虑; 请求在下游服务B系统队列中的等待时间:堆积太多请求会导致请求失效; 下游服务B的处理耗时:处理的慢导致请求长期阻塞在系统队列中。
绝对时间:用绝对时间戳表示(比如1577681783),但是不同设备上的时钟不一致,如下图A和B两台机器的时钟差了2分钟,A访问B的超时时间是5s,于是请求发到机器B上就直接超时失败了,不符合预期; 相对时间:用相对时间表示(比如5s),但是这个时间是从业务接收到请求开始计时的,没有包括在系统队列中的时间,有可能已经等待了很久的时间了,上游已经断开了连接,不符合预期;
△绝对时间的问题
△相对时间的问题
机器之间的系统时钟不一致 需要关注在系统队列中的等待时间
Host1机器上的服务A访问Host2机器上的服务B; 服务A先访问本地的UFC-agent,传递相对超时时间为5S; Host1机器上UFC-agent 访问Host2机器上的UFC-agent,传递相对超时时间为5S; Host2机器上的UFC-agent 把相对超时时间转成绝对超时时间; Host2机器上的UFC-agent 访问本地的服务B。
通过双端的Agent实现了相对时间到绝对时间的转换,对业务解决了上面的那2个问题,对业务也是透明的。
3.4 减少无效请求-基于socket有效性
需要有一个认知:
tcp三次握手之后,server 未accept请求,client直接调用close关闭连接; tcp三次握手之后,server 未accept请求,client先写request请求,然后调用close关闭连接;
server accept后进行请求处理时,client调用close关闭连接。
ssize_t read(int fd, void *buf, size_t count);
04
流量限制:可以分成两部分,一个是流量接入层,解决ddos连接数攻击,另外一部分是流量转发层,通过动态熔断策略将后端能处理的请求数转发给后端; 流量处理:业务基于流量有效性进行处理,避免处理无效请求。
END
推荐阅读
如何在百度百舸部署满血版DeepSeek-V3、DeepSeek-R1模型
首日调用客户破1.5万!DeepSeek-V3/R1上线背后的超低推理成本技术揭秘