brpc 熔断功能(Circuit Breaker)完全指南:默认策略、错误率熔断与节点隔离恢复
【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc
熔断(Circuit Breaker)是 brpc 在客户端侧保障高可用的一项核心能力:当某个下游节点出现故障时,brpc 会自动将其从可用节点列表中剔除,避免请求持续打到故障节点上拖垮整体时延。本文基于 docs/cn/circuit_breaker.md 展开,结合 src/brpc/circuit_breaker.cpp、src/brpc/socket.cpp 等源码实现,系统讲解 brpc 默认熔断策略的触发条件、可选熔断策略的开启方法与 EMA 错误成本算法、全部可调 gflag 参数、熔断的隔离与恢复机制,以及如何在监控页面查看熔断数据。读完本文,你将能够在自己的 brpc 客户端上正确配置并调优熔断策略,让搜索、存储、推荐等高性能在线服务在下游抖动时依然稳定。
brpc 熔断的整体工作流程
在讲解具体策略之前,先明确熔断在整个 RPC 调用链路中的位置。当发起一次 RPC 时,brpc 首先从命名服务(Naming Service)获取可用节点列表,然后根据负载均衡策略从中挑选一个节点作为实际访问目标。当某个节点出现故障时,brpc 需要能够自动把它从可用节点列表中剔除,并且周期性对故障节点进行健康检查,待其恢复后再重新纳入。这一"发现故障 → 剔除 → 隔离 → 健康检查 → 恢复"的闭环就是熔断功能要解决的完整问题。
从源码结构看,熔断逻辑主要由三个组件协作完成:
- src/brpc/circuit_breaker.h 与 src/brpc/circuit_breaker.cpp:定义
CircuitBreaker类及内部EmaErrorRecorder,实现错误率统计与熔断判定; - src/brpc/socket.cpp:每个连接(Socket)的
SharedPart中持有一个circuit_breaker实例(src/brpc/socket.cpp#L199),RPC 结束后通过Socket::FeedbackCircuitBreaker将结果反馈给熔断器(src/brpc/socket.cpp#L1083-L1089); - src/brpc/channel.cpp 与 src/brpc/channel.h:通过
ChannelOptions.enable_circuit_breaker控制是否开启可选熔断策略。
默认的熔断策略
brpc 默认内置一套简单熔断策略:只要检测到某个节点无法建立 TCP 连接,就对该节点实施熔断。具体而言,当一次 RPC 返回以下错误码之一时,brpc 会认为目标节点无法建立连接,从而触发熔断:
ECONNREFUSED:连接被拒绝,通常意味着端口上没有服务在监听;ENETUNREACH:网络不可达;EHOSTUNREACH:主机不可达;EINVAL:参数非法。
连接超时的特殊处理:连续三次视为 ENETUNREACH
一个容易踩坑的细节是:如果 brpc 发现某个节点出现连续三次连接超时(注意是连接超时 connect timeout,而不是 RPC 超时),那么第三次超时会被当作ENETUNREACH处理,同样触发熔断。
这里衍生出一个重要的配置约束:RPC 的超时时间必须大于连接超时时间,即:
ChannelOptions.timeout_ms > ChannelOptions.connect_timeout_ms原因在于:如果 RPC 超时设置得比连接超时更短,那么当节点无法建立连接时,RPC 超时会比连接超时更早触发,请求在还没等到"连接失败"这个结论之前就以 RPC 超时结束了,最终导致永远触发不了熔断。在 src/brpc/channel.h#L48-L58 中可以看到两者的默认值:connect_timeout_ms默认为 200 毫秒(-1 表示无限等待),timeout_ms默认为 500 毫秒,默认配置本身就满足上述约束;但如果你自定义了超时,务必保持这个大小关系。
默认策略的特性
默认熔断策略具有以下特点:
- 一直开启:不需要做任何配置,默认生效;
- 无法关闭:它不依赖
enable_circuit_breaker开关; - 无需维护:由 brpc 框架内部自动完成节点的剔除与恢复。
可选的熔断策略(基于错误率)
仅依赖默认策略有时并不能完全满足需求。举一个极端例子:假设某个下游节点的业务逻辑线程全部卡死,但 IO 线程仍能正常工作。此时所有请求都会超时,但 TCP 连接却能够正常建立——默认熔断策略完全检测不到这种"半死不活"的节点。为此,brpc 在默认策略的基础上提供了一种更激进的熔断策略:开启之后,brpc 会根据节点的出错率来判断其是否处于故障状态,即使 TCP 连接正常,只要错误率超标同样会熔断。
开启方法
可选的熔断策略默认是关闭的(ChannelOptions构造函数中enable_circuit_breaker初始为false,见 src/brpc/channel.cpp#L61)。用户按需在ChannelOptions中开启:
brpc::ChannelOptions option; option.enable_circuit_breaker = true;在 src/brpc/channel.h#L77-L81 中,该字段的注释明确说明:当某个服务节点的错误率过高时,节点会被隔离,且这种隔离是全局的——在隔离期间,该节点对本进程内所有 channel 都不可用。
在 src/brpc/channel.cpp#L511-L513 可以看到,channel 在发起 RPC 时会把该开关翻译为 Controller 的FLAGS_ENABLED_CIRCUIT_BREAKER标志位,用于标记本次调用需要参与熔断统计。
工作原理:EMA 错误成本模型
可选的熔断由CircuitBreaker实现。开启熔断后,CircuitBreaker会记录每一个请求的处理结果,并维护一个累计出错时长acc_error_cost。当acc_error_cost > max_error_cost时,对该节点实施熔断。整个判定过程的核心实现在EmaErrorRecorder::OnCallEnd(src/brpc/circuit_breaker.cpp#L83-L110)。
每次请求返回成功之后,更新max_error_cost:
首先更新 latency 的 EMA(指数移动平均) 值,记为
ema_latency:ema_latency = ema_latency * alpha + (1 - alpha) * latency从源码看,
alpha实际由_smooth = pow(EPSILON, 1.0/window_size)计算而来(src/brpc/circuit_breaker.cpp#L76),其中EPSILON即 gflagcircuit_breaker_epsilon_value;首次请求时ema_latency直接取本次 latency(src/brpc/circuit_breaker.cpp#L125-L129)。之后根据
ema_latency更新max_error_cost:max_error_cost = window_size * max_error_rate * ema_latency源码中的实际实现为
ema_latency * _window_size * (_max_error_percent / 100.0) * (1.0 + EPSILON)(src/brpc/circuit_breaker.cpp#L147-L148),可见阈值在窗口大小 × 错误率 × 平均延迟的基础上还叠加了(1.0 + EPSILON)的松弛系数,避免瞬时波动误伤节点。
每次请求返回之后,更新acc_error_cost:
- 如果请求处理成功:
acc_error_cost = alpha * acc_error_cost,即错误成本按平滑系数指数衰减(src/brpc/circuit_breaker.cpp#L152-L170),成功请求越多,累计错误成本下降越快; - 如果请求处理失败:
acc_error_cost = acc_error_cost + min(latency, ema_latency * 2)。
其中计算acc_error_cost所用的alpha与计算max_error_cost所用的为同一个值。考虑到出现超时等错误时,latency 往往会远远大于ema_latency,所以在累加acc_error_cost时对失败请求的 latency 做了修正,使其不超过ema_latency的两倍。这个倍率同样可以通过 gflag 配置(circuit_breaker_max_failed_latency_mutiple,默认 2,见 src/brpc/circuit_breaker.cpp#L40-L42)。
此外源码中还有一个值得注意的边界处理:如果请求返回ELIMIT(服务端达到最大并发),CircuitBreaker::OnCallEnd会直接返回 healthy(src/brpc/circuit_breaker.cpp#L194-L196)。因为ELIMIT通常意味着整个下游集群过载,此时熔断单个节点反而会加重下游压力,且ELIMIT对应的 latency 往往很小,既不能按成功处理,也不宜按失败累加成本,因此直接忽略。
长短双窗口机制
为了允许某个节点在短时间内抖动,同时又能剔除长期错误率较高的节点,CircuitBreaker同时维护了长窗口和短窗口两个统计窗口(src/brpc/circuit_breaker.cpp#L173-L184):
- 长窗口(
_long_window):阈值较低,主要作用是剔除那些长期错误率较高的服务。可通过circuit_breaker_long_window_size与circuit_breaker_long_window_error_percent调整; - 短窗口(
_short_window):阈值较高,允许我们更精细地控制熔断灵敏度。在一些对抖动很敏感的场景,可以通过circuit_breaker_short_window_size和circuit_breaker_short_window_error_percent缩短短窗口长度、降低短窗口对错误的容忍程度,使得抖动出现时能够快速对故障节点实施熔断。
两个窗口的判定是与关系:_long_window.OnCallEnd(...) && _short_window.OnCallEnd(...),只要任一窗口判定不健康,节点即被标记为熔断(src/brpc/circuit_breaker.cpp#L213-L218)。可以根据实际 QPS 及对错误的容忍程度来调整长窗口参数。
初始化阶段的兜底判定
由于计算 EMA 需要积累一定量的数据,在熔断的初始阶段(即目前已收集到的请求数 < 窗口大小),EmaErrorRecorder会直接使用错误数量来判定是否熔断(src/brpc/circuit_breaker.cpp#L95-L107):
若 acc_error_count > window_size * max_error_rate 为真,则进行熔断。这一兜底逻辑确保熔断器在数据积累不足时依然具备防护能力,而不是在窗口"空转"期完全失效。
熔断参数速查表(gflags)
所有可选的熔断参数均为 brpc 的 gflag 配置项,定义于 src/brpc/circuit_breaker.cpp#L29-L54,支持运行时通过bvar/gflags 动态调整。完整参数如下:
| 参数名 | 默认值 | 说明 |
|---|---|---|
circuit_breaker_short_window_size | 1500 | 短窗口的样本数量 |
circuit_breaker_long_window_size | 3000 | 长窗口的样本数量 |
circuit_breaker_short_window_error_percent | 10 | 短窗口允许的最大错误率百分比,取值范围 0-99 |
circuit_breaker_long_window_error_percent | 5 | 长窗口允许的最大错误率百分比,取值范围 0-99 |
circuit_breaker_min_error_cost_us | 500 | error_cost 的最小值(微秒),当 error cost 的 EMA 小于该值时直接归零,避免长期累积的微小误差导致误判 |
circuit_breaker_max_failed_latency_mutiple | 2 | 失败请求 latency 相对成功请求平均 latency 的最大倍数(即上文ema_latency * 2中的 2) |
circuit_breaker_min_isolation_duration_ms | 100 | 最小隔离时长(毫秒) |
circuit_breaker_max_isolation_duration_ms | 30000 | 最大隔离时长(毫秒),同时作为判断两次熔断是否为"连续熔断"的时间间隔 |
circuit_breaker_epsilon_value | 0.02 | 用于计算 EMA 平滑系数:ema_alpha = 1 - std::pow(epsilon, 1.0 / window_size) |
circuit_breaker_half_open_window_size | 0 | 半开窗口允许放行的请求数量(0 表示关闭半开功能) |
关键参数的调优语义
circuit_breaker_epsilon_value:控制对连续抖动的容忍程度。该值越低,计算公式中的alpha越小,acc_error_cost下降的速度越快。当该值达到 0.001 时,若一整个窗口的请求都没有出错,正好可以把acc_error_cost降低到 0。源码注释(src/brpc/circuit_breaker.cpp#L57-L66)给出了直观示例:当window_size = 100时,EPSILON = 0.1对应smooth = 0.9772,EPSILON = 0.3对应smooth = 0.9880;EPSILON越大,平滑系数越大,早期数据占比越高,错误成本衰减越慢。circuit_breaker_min_error_cost_us:兜底归零阈值。在成功请求的衰减路径上,如果当前ema_error_cost小于该值(默认 500 微秒),会直接原子地归零而不是继续乘系数(src/brpc/circuit_breaker.cpp#L156-L161),避免数值长期残留在"半死不活"的小量级上。circuit_breaker_half_open_window_size:半开窗口(默认关闭)。开启后,节点从熔断状态恢复时先进入半开状态,只允许有限数量的请求放行探活;只有这些请求全部成功才转为闭合状态,否则立刻回到熔断状态(src/brpc/circuit_breaker.cpp#L200-L211 与 src/brpc/circuit_breaker.cpp#L226-L229)。该值需为非负整数,由BRPC_VALIDATE_GFLAG校验。
熔断的范围:连接级熔断与 connection_group
brpc 在决定熔断某个节点时,熔断的是整个连接,而非单个请求或单个 channel:
- pooled 模式熔断所有连接:如果使用
pooled连接模式(同一节点复用连接池),熔断会作用于该节点上的所有连接; - 连接被 channel 共享:brpc 的 TCP 连接会被多个 channel 共享(相同
connection_group的 channel 共享连接),当某个连接被熔断之后,所有共享该连接的 channel 都不能再使用这个故障连接; - 用
connection_group隔离共享范围:如果希望避免第 2 点中"一个故障连接拖累所有 channel"的情况,可以通过设置ChannelOptions.connection_group将 channel 放进不同的 ConnectionGroup。不同 ConnectionGroup 的 channel 不会共享连接(src/brpc/channel.h#L143-L147)。
connection_group区分大小写、忽略首尾空格,默认值为空字符串。从 src/brpc/channel.h#L77-L81 的注释还可以确认一个重要特性:熔断隔离是进程级全局的,在隔离期间该节点对所有 channel 都不可用——这是设计上防止故障节点被不同客户端重复击穿的主动选择。
熔断数据的收集范围
只有通过开启了enable_circuit_breaker的 channel发送的请求,才会把请求的处理结果提交给CircuitBreaker。也就是说,熔断统计是按 channel 粒度收集的。因此,如果你决定对下游某个服务开启可选熔断策略,最好在所有连接到该服务的 channel 上都开启enable_circuit_breaker,否则未开启的 channel 发起的请求不会参与错误率统计,可能造成熔断判定失真。
熔断的恢复与健康检查
目前 brpc 使用通用的健康检查来判定某个节点是否已经恢复:只要能够建立 TCP 连接,就认为该节点已经恢复。
这里存在一个矛盾:如果故障节点只是逻辑卡死(TCP 能连上),健康检查会误判其"已恢复"。为了正确摘除这类"能建立 TCP 连接但实际故障"的节点,brpc 在每次熔断之后会先对故障节点进行一段时间的隔离,隔离期间该节点既不会被负载均衡选中,也不会进行健康检查。
隔离时长遵循指数退避策略(CircuitBreaker::UpdateIsolationDuration,见 src/brpc/circuit_breaker.cpp#L239-L253):
- 初始隔离时间为 100ms(
circuit_breaker_min_isolation_duration_ms); - 若节点在短时间内被连续熔断,则隔离时间翻倍(
isolation_duration_ms * 2),直至达到上限; - 最大隔离时间和"判断两次熔断是否为连续熔断"的时间间隔都由
circuit_breaker_max_isolation_duration_ms控制,默认 30 秒; - 如果两次熔断的时间间隔超过了该上限,则视为非连续熔断,隔离时间重置回最小值。
隔离结束后的恢复路径同样有源码可循:连接恢复时Socket::OnConnectable会调用circuit_breaker.Reset()清空窗口统计并解除熔断标记(src/brpc/socket.cpp#L1052-L1056);而连接失败时Socket::OnFailed会调用MarkAsBroken()并启动健康检查(src/brpc/socket.cpp#L908-L911)。
数据体现:在监控页面观察熔断状态
节点的熔断次数、最近一次从熔断中恢复之后的累积错误数,都可以在监控页面的/connections中找到。即使没有开启可选的熔断策略,brpc 也会对这些数据进行统计。两个关键指标:
- nBreak:表示进程启动之后,该节点的总熔断次数。对应源码中的
isolated_times()(src/brpc/socket.cpp#L1075-L1081); - RecentErr:表示该节点最近一次从熔断中恢复之后,累计的出错请求数。对应
recent_error_count()(src/brpc/socket.cpp#L1067-L1073)。
需要特别说明的是:由于 brpc 默认熔断策略一直开启,即便没有开启可选熔断策略,nBreak 也可能大于 0——此时的熔断通常是 TCP 连接建立失败(如ECONNREFUSED、ENETUNREACH等)导致的。因此排查 nBreak 指标时,应先判断熔断原因属于默认策略(连接失败)还是可选策略(错误率超标),再决定调优方向。
实践建议小结
- 先保证超时配置正确:维持
timeout_ms > connect_timeout_ms,否则默认熔断策略可能因 RPC 超时抢先触发而永远无法生效; - 按业务容忍度开可选熔断:对"逻辑线程卡死但 TCP 正常"的下游服务,务必开启
enable_circuit_breaker = true,并尽量在所有指向该服务的 channel 上统一开启; - 用长短窗口配合调灵敏度:长窗口(默认 3000 样本 / 5% 错误率)负责剔除长期高错误率节点,短窗口(默认 1500 样本 / 10% 错误率)负责快速响应抖动;对抖动敏感的场景可调低短窗口错误容忍度;
- 用
connection_group控制熔断爆炸半径:如果担心共享连接导致故障放大,可为不同业务线设置不同的connection_group; - 结合 /connections 监控验证:通过 nBreak 与 RecentErr 观察熔断是否按预期触发,并据此微调
circuit_breaker_epsilon_value等参数。
【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考