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.gogetCircuitBreaker)。因此:

  • 不是进程级「所有调用共享一只熔断器」;
  • 也不是「目标服务全局一只」——多个发送方 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 本节要点

  1. 熔断zhenyi/zactor/circuitbreaker.go默认连续失败 5 次 → Open冷却 10s 后进入 Half-Open 试探路径(以源码常量为准)。
  2. 分桶发送方 Actor × 目标 actorId;多发送方互不计数合并。
  3. 限流:示例为 每连接 rate.Limiter;适合限制单会话突发。
  4. 热更新3.4UpdateRateLimit 可改 Actor 侧限速配置;与连接对象上已绑定的 limiter 是否同步,取决于你的 Server/Channel 封装(以实际代码为准)。
  5. 降级:框架不负责业务降级策略,需在业务或网关落实。

3.6 节zmsg 消息池、Retain/Release 与固定头序列化。

results matching ""

    No results matching ""