1. 3.5 熔断器与限流
进程内 Actor RPC 与 连接入站 是两条不同的压力入口:前者慢会把 CallActor 与 Slot 拉长;后者会让 读/解析 与应用层过载。本节分别对应 zactor 内熔断(与 3.3 同源)和 连接上的令牌桶(x/time/rate)。
1.1. 3.5.1 问题:级联故障
典型链路:
用户请求 → Gate → IM Actor → 数据库(慢查询)
数据库响应变慢(3s)
→ IM Actor 的 CallActor 超时(3s)
→ Gate 的 Mailbox 堆积
→ 更多 Actor 被阻塞
→ 整个服务不可用
下游变慢时,若上游仍无限阻塞等待,邮箱与 Slot 会堆积,放大尾延迟。熔断与 入口限流是两条常见防线(语义不同,不能互相替代)。
1.2. 3.5.2 熔断器:快速失败
熔断器的思路和电路保险丝一样:异常太多就断开,保护下游不被压垮。
1.2.1. 三种状态
连续失败 ≥ 5次 冷却 10s 后
┌──────────────────┐ ┌──────────────┐
│ │ │ │
↓ │ ↓ │
Closed ──────→ Open ──┘ HalfOpen ──→ Closed
↑ │ ↑
└───────────────────────────────┘ 成功
一次成功
| 状态 | 行为 |
|---|---|
| Closed | 正常放行 |
| Open | 冷却期内快速失败 |
| HalfOpen | 允许试探请求(成功则回到 Closed) |
1.2.2. zhenyi 的实现
zhenyi 的熔断器是轻量级的,存放在每个发送方 Actor 实例的 circuitBreakers map 中,键为目标 actorId(见 zactor/helper.go 的 getCircuitBreaker)。因此:
- 不是进程级「所有调用共享一只熔断器」;
- 也不是「目标服务全局一只」——多个发送方 Actor 同时调用同一目标时,各自独立计数。
type circuitBreaker struct {
state atomic.Int32 // Closed / Open / HalfOpen
failures atomic.Int32 // 连续失败次数
lastFailMs atomic.Int64 // 上次失败时间
threshold int32 // 失败阈值(默认 5 次)
cooldownMs int64 // 冷却时间(默认 10 秒)
}
关键代码:
// 发起请求前检查
func (cb *circuitBreaker) allow() bool {
switch cb.state.Load() {
case cbClosed:
return true // 正常放行
case cbOpen:
// 冷却期过了?试一下
if now - cb.lastFailMs > cb.cooldownMs {
cb.state.Store(cbHalfOpen) // 切换到半开
return true
}
return false // 还在冷却,快速失败
case cbHalfOpen:
return true // 试探中
}
}
// 请求成功
func (cb *circuitBreaker) recordSuccess() {
cb.failures.Store(0)
cb.state.Store(cbClosed) // 关闭熔断
}
// 请求失败
func (cb *circuitBreaker) recordFailure() {
cb.lastFailMs.Store(now)
n := cb.failures.Add(1)
if n >= cb.threshold { // 默认 5 次
cb.state.Store(cbOpen) // 打开熔断
}
}
1.2.3. 在 CallActor 中的使用
3.3 节已经看到,CallActor 在发 RPC 前检查熔断器:
func (a *Actor) CallActor(actorId uint64, ...) ziface.RpcReply {
cb := a.getCircuitBreaker(actorId)
if !cb.allow() {
return RpcReply{Code: ErrorCode_RpcErr, Msg: "circuit breaker open"}
}
// 正常调用
if ok {
cb.recordSuccess()
} else {
cb.recordFailure()
}
}
1.2.4. 语义上如何理解「按目标分」?
同一发送方对不同目标(不同 actorId)的稳定性可能差异很大;为每个 (发送方 Actor, 目标 actorId) 维护独立熔断状态,可避免「A 下游坏了却把发往 B 的 RPC 也掐断」。
若你需要全局限流下游或全链路熔断,应在网关/业务层另行实现;框架层不代替你做全局策略。
1.3. 3.5.3 限流:保护自己不被压垮
熔断保护的是下游(不发了),限流保护的是自己(只处理这么多)。
zhenyi 的限流用在连接级别,不是 Actor 级别:
// Gate 接受新连接时,给每个连接设置限流器
func (s *Server) OnAccept(ctx context.Context, channel IChannel) bool {
if s.IsLimiter {
channel.SetLimit(zlimiter.NewLimiter(s.Rate, s.Burst))
}
return true
}
基于 golang.org/x/time/rate 实现,使用令牌桶算法:
- Rate:每秒生成的令牌数(每秒允许的请求数)
- Burst:桶的最大容量(突发上限)
type Limiter struct {
limiter *rate.Limiter
}
func NewLimiter(limit, maxLimit int) *Limiter {
return &Limiter{
limiter: rate.NewLimiter(rate.Limit(limit), maxLimit),
}
}
func (l *Limiter) Allow() bool {
return l.limiter.Allow()
}
1.3.1. 为什么限流放在连接层而不是 Actor 层?
| 放置位置 | 效果 |
|---|---|
| 连接层 | 每个连接独立限流,防止单个客户端刷爆服务 |
| Actor 层 | 所有连接共享配额,一个恶意客户端会影响所有用户 |
连接层限流更合理——每个连接有自己的令牌桶,一个客户端发太快只影响自己。
1.3.2. 限流参数热更新
限流参数存储在 ActorConfig 中,可以通过 UpdateRateLimit 运行时调整:
// 检测到流量突增,降低单连接限流
actor.UpdateRateLimit(500, 1000)
// 流量恢复正常,恢复限流
actor.UpdateRateLimit(2000, 5000)
1.4. 3.5.4 熔断 vs 限流 vs 降级
这三个概念容易混淆,理清一下:
| 机制 | 保护谁 | 什么时候触发 | 效果 |
|---|---|---|---|
| 熔断 | 下游 | 目标服务连续失败 | 快速失败,不发起请求 |
| 限流 | 自己 | 请求速率超过阈值 | 排队等待或拒绝 |
| 降级 | 用户 | 服务能力不足 | 返回默认值/缓存,不处理 |
zhenyi 内置了熔断和限流。降级需要业务层自己实现——比如数据库查询失败时返回缓存数据,这取决于具体业务逻辑,框架无法替你决定"降级成什么"。
1.5. 3.5.5 本节要点
- 熔断:
zhenyi/zactor/circuitbreaker.go中 默认连续失败 5 次 → Open,冷却 10s 后进入 Half-Open 试探路径(以源码常量为准)。 - 分桶:发送方 Actor × 目标 actorId;多发送方互不计数合并。
- 限流:示例为 每连接
rate.Limiter;适合限制单会话突发。 - 热更新:3.4 的
UpdateRateLimit可改 Actor 侧限速配置;与连接对象上已绑定的 limiter 是否同步,取决于你的 Server/Channel 封装(以实际代码为准)。 - 降级:框架不负责业务降级策略,需在业务或网关落实。
3.6 节:zmsg 消息池、Retain/Release 与固定头序列化。