7AA7 Blog

服务治理:注册发现、熔断与限流的工作原理

苏念禾 UID:79
2026-10-11 17:22
4 2 0

单机应用里调用就是一次函数调用,失败立即抛异常。服务拆开后调用跨了网络,故障从”失败”扩展成”慢””超时””半死不活”三种新形态,而且往往先慢后挂、局部扩散。注册发现、熔断、限流处理的分别是”调用谁””要不要继续调””允许多少进来”。

注册发现与健康检查

服务实例的 IP 和端口不是固定的:扩缩容、故障迁移、滚动发布都会让实例列表变化。注册中心维护的就是这份动态清单。

健康检查决定一个实例什么时候被摘除,常见两种:

心跳由实例主动向注册中心汇报,连续 N 个周期没收到就标记不健康。实现简单,但只能证明进程还在,不能证明它能正常服务——线程池打满、连接池耗尽时心跳照发。

主动探测由注册中心发起 TCP 连接或 HTTP 探活请求。能反映真实服务能力,代价是探测本身有成本,实例规模大时探测链路会成为负担,且探测失败可能是网络抖动而非实例故障。实践里通常两者结合:心跳做主体,健康检查接口做辅助,摘除前要求多个探测节点同时判定失败,避免单点误判。

摘除策略要留缓冲。常见状态机是:不健康先降权减少流量,持续失败才摘除,恢复时先进预热期逐步放量。直接摘除的风险是雪崩——网络抖动导致大量实例同时被摘,流量全压到少数实例上。

推拉模型的差别在于实例列表变更如何到达调用方:

模型 实现方式 优点 缺点
推 注册中心变更时主动推送 变更秒级生效 连接维护复杂,推送失败要补偿
拉 调用方定时轮询拉取 实现简单,容错好 有延迟,轮询间隔要权衡
长轮询 拉取请求挂起等变更 接近实时且容错 实现复杂度居中

延迟与实时性是主要权衡点。故障摘除要求秒级生效,纯拉取(例如 30 秒一次)在故障场景下会多损失几十秒的请求量。

客户端发现与服务端发现

客户端发现是调用方从注册中心拿到实例列表,自己在本地做负载均衡,然后直连目标实例。少一跳网络,性能好,负载策略可以按需定制。代价是每个语言都要实现一套逻辑,SDK 版本很难统一升级。

服务端发现是调用方固定访问某个入口(负载均衡器或网格 sidecar),由它负责选实例转发。调用方零感知,语言无关,策略集中升级。代价是多一跳带来的延迟与资源占用,入口本身也成了故障域。

选择依据通常是技术栈统一程度:单一语言栈适合客户端发现,多语言混合、且已有基础设施团队时,服务端发现或 Service Mesh 更省事。

熔断器的三态机

熔断器的目的是快速失败:下游已经不可用时,与其让每个请求都卡到超时,不如立刻返回,把资源留给有可能成功的调用。它是一个状态机:

       失败率超过阈值
  Closed ──────────────> Open
     ^                     |
     |                     | 熔断时长到点,放行少量探针
     |                     v
     |___ 探针成功率达标 ___ Half-Open
     |                     |
     |_____________________| 探针失败,立即回到 Open

Closed(关闭):请求正常放行,同时统计最近一段时间的结果。统计窗口的选择决定灵敏度:计数窗口(固定时间 buckets)实现简单但有边界效应,滑动窗口把时间切成小片滚动统计,能更平滑地反映趋势。常用的衡量口径是”最近 N 个请求或最近 T 秒内,失败率超过 X%”——两个条件要同时限制,避免低流量时单个失败就把服务整段切断。

Open(打开):请求直接失败,不再发起调用。这里的关键参数是熔断时长,通常从几十秒起,配合指数退避递增。被调用方返回降级响应(缓存值、默认值、空列表),而不是把错误抛给上游。

Half-Open(半开):熔断时间到之后不是全量恢复,而是放行少量探针请求。成功达到阈值才回到 Closed,任意失败立刻回到 Open。这一步防止了下游刚恢复就被全量流量再次打垮。

// 关键参数:窗口长度、最小请求数、失败率阈值、熔断时长、探针数量
CircuitBreakerConfig cfg = CircuitBreakerConfig.custom()
    .slidingWindow(SlidingWindowType.COUNT_BASED, 100)   // 统计最近 100 次调用
    .minimumNumberOfCalls(20)                            // 低于 20 次不参与判断
    .failureRateThreshold(50)                            // 失败率超 50% 才跳闸
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .permittedNumberOfCallsInHalfOpenState(5)
    .build();

minimumNumberOfCalls 常被忽略,却是低流量场景下防止误熔断的主要防线。

熔断器还要与超时配合:下游超时明显大于熔断窗口时长时,统计严重滞后;超时过长(例如 30 秒)会让线程池在熔断触发前就耗尽。经验做法是让下游超时逐层小于上游超时,形成收敛的超时栈。

限流算法与突发流量

限流回答的是”允许多少进来”。四种常见算法的差别集中在如何处理突发:

算法 实现 突发容忍 主要代价
固定计数器 每段时间计数,超限拒绝 边界处可放行两倍配额 临界突刺,精度差
滑动窗口 窗口切片滚动统计 平滑,精度取决于切片 内存随切片数增长
令牌桶 恒定速率生成令牌,取不到则拒绝 允许突发,突发量等于桶容量 桶容量要按业务调
漏桶 请求入桶,恒定速率出水 不允许突发,超出排队或丢弃 突发场景响应变慢

固定计数器的临界问题是:配额 100/分钟,59 秒时来了 100 个,下一秒又来了 100 个,两秒内实际通过 200 个。滑动窗口通过切片解决但消耗内存。令牌桶的工程平衡最好——桶容量允许一定程度的突发(这对秒杀开头的瞬时流量是必要的),均值仍被令牌生成速率约束。

参数不该拍脑袋:先用压测找到单机容量拐点(延迟开始明显上升时的 QPS),再乘以实例数留出余量,经验上把阈值设在拐点的七到八成。

限流放在哪里

网关层限流集中管理,配置统一,能拦住入口的超额流量。它看不到业务维度——无法区分同一个大客户的批量操作与普通浏览,也感知不到具体租户的异常。

服务内部限流贴近业务,可以按租户、接口、用户等级分别设配额,保护的是自己。

两者通常并存:网关做来源维度的粗粒度保护,服务内部做业务维度的细粒度保护。只做网关限流的隐患是内部流量——重试放大、批处理、内部调用——完全不受约束,更容易打垮脆弱的下游。

被拒绝的请求怎么处理同样要设计。直接返回错误会诱发上游重试从而放大流量,更好的做法是附带退避提示,告诉调用方多久后再试。

降级与恢复

降级是在资源不足或依赖不可用时,主动降低服务质量以保住核心链路。触发条件通常三类:熔断打开、限流命中、资源水位(CPU、连接池、线程池队列)超过阈值。

降级策略要提前按接口分级:核心链路(下单、支付)只做扩容与限流,不降级;边缘能力(推荐、评价、积分)返回空值或默认值;可延迟能力(通知、统计)转为异步或丢弃。这份分级表要在平时定好,而不是故障现场临时决定。

恢复比触发更难。稳妥做法是阶梯放量:先恢复少量流量观察一段时间指标,正常再逐步放开,并把自动恢复与手动开关结合——自动恢复应对短暂抖动,长时间依赖故障需要人来判断。没有指标支撑的自动恢复,容易在”恢复—再次打垮—再降级”的循环里反复。

210Views
4Likes
2Comments
2Participants
9 minread time

Comments (2)

  • 沈让 · 2026-10-11 19:34

    想问下你实际遇到过最大的坑是哪个?

    Reply
  • AsyncLab · 2026-10-12 04:58

    我不太理解为什么大家都默认要用这个方案,感觉成本被低估了。

    Reply