1. 3.4 热更新与运行时调参

线上常出现「参数只在真实负载下才看清」的情况:worker 池容量、RPC 待处理上限、令牌桶速率等。若每次都 改文件→构建→滚动重启,反馈周期长且打断连接与队列。

zhenyiActor 单线程模型下,通过 safeUpdateCmdTypeSafeFn → 邮箱排队,把参数变更闭包固定排入 处理线程,避免与 Run 并发改同一批字段(见 zhenyi/zactor/handlemsg.gohotupdate.go)。

1.1. 3.4.1 为什么需要热更新?

生产环境中,很多参数在开发阶段很难确定最优值:

参数 难点
协程池大小 取决于实际并发量,线上才能测准
限流阈值 流量忽高忽低,需要动态调整
RPC 并发槽数 取决于下游服务的响应速度

这些参数不应该"一次性定死"。热更新让你可以在服务运行期间观察指标、调整参数、验证效果,不需要每次都重启。

1.2. 3.4.2 问题:怎么在 Actor 单线程模型下安全地改参数?

Actor 的消息是串行处理的,只有一个 goroutine 在跑。如果外部 goroutine 直接修改 Actor 的内部状态,就会破坏单线程不变性。

// ❌ 错误:外部直接修改,可能和 Actor 主循环并发冲突
actor.workerPool.Tune(newSize)
actor.Rate = newRate

解决方案:通过 Mailbox 投递修改请求。

1.3. 3.4.3 SafeFn:安全的运行时修改

zhenyi 提供了 safeUpdate 方法,把修改逻辑封装成消息投递到 Actor 的 Mailbox:

func (a *Actor) safeUpdate(fn func()) {
    cmd := zmodel.ActorCmd{
        Type: zmodel.CmdTypeSafeFn,
        Fn:   fn,
    }
    a.Push(cmd)  // 投递到 Mailbox
}

实际分支在 SafeHandleMessage 内与各类 ActorCmd 一起分发;SafeFn 最终在 mailbox 线程执行闭包(等价于与业务消息同一顺序域)。具体函数名以 handlemsg.go 为准。

1.3.1. 使用方式

// 运行时调整协程池大小
actor.UpdateWorkerPoolSize(50)

// 运行时调整限流参数
actor.UpdateRateLimit(rate, burst)

// 运行时重建协程池(极端情况,比如检测到池死锁)
actor.RebuildWorkerPool(100)

每个方法内部都是 safeUpdate

func (a *Actor) UpdateWorkerPoolSize(newSize int) {
    a.safeUpdate(func() {
        if a.workerPool == nil || newSize <= 0 {
            return
        }
        a.workerPool.Tune(newSize)
        a.GetLogger().Info("Worker pool size updated",
            zap.Int("newSize", newSize))
    })
}

1.4. 3.4.4 具体能调什么?

1.4.1. 协程池大小

actor.UpdateWorkerPoolSize(50)
// 底层调用 ants.PoolWithFunc.Tune()
// 调整协程池容量,正在运行的任务不受影响

适用场景:

  • 线上监控发现协程池利用率持续 > 80%,需要扩容
  • 流量高峰过后,缩容释放资源

1.4.2. 限流参数

actor.UpdateRateLimit(1000, 2000)
// Rate: 每秒允许处理的请求数
// Burst: 突发容量

适用场景:

  • 下游服务变慢,需要降低请求速度
  • 下游恢复后,调高限流

1.4.3. RPC 并发槽(与 CallActor 相关)

actor.UpdateMaxRPCPending(4096)

内部同样走 safeUpdate,用于运行时调整 RPC Slot 数量(见 zhenyi/zactor/hotupdate.go)。

1.4.4. 重建协程池

actor.RebuildWorkerPool(100)
// 完全销毁旧池,创建新池

这是极端操作,适用于:

  • 协程池死锁(任务卡住不退出)
  • Tune 无法满足(需要改更多配置)

重建过程中正在运行的任务会继续执行完,新任务走新池。

1.5. 3.4.5 SafeFn 的设计考量

SafeFn 看起来简单,但有几个设计点值得说:

1.5.1. 为什么不用锁?

用读写锁也能保护参数修改,但锁的粒度不好控制——锁太粗影响性能,锁太细容易漏。通过 Mailbox 投递,修改操作和消息处理天然串行,不需要考虑锁的粒度。

1.5.2. 为什么 SafeFn 闭包可以访问 Actor 状态?

闭包是在调用方构造的,但在 Actor 主线程执行。这意味着:

  • 构造闭包时:在调用方 goroutine(可能在监控线程、定时器线程等)
  • 执行闭包时:在 Actor 主线程(串行,安全)
// 在监控 goroutine 中
actor.UpdateWorkerPoolSize(50)
// → 构造闭包 { a.workerPool.Tune(50) }
// → Push 到 Mailbox
// → Actor 主线程取出执行 { a.workerPool.Tune(50) }  ← 这里是安全的

闭包捕获的 a(Actor 指针)在整个生命周期内有效,不会有悬空引用的问题。

1.5.3. SafeFn 和消息处理的顺序

SafeFn 和普通消息在同一个 Mailbox 中排队,先到先处理。这意味着:

  • 如果你发了一条 SafeFn,后面跟了一条普通消息,SafeFn 先执行
  • 不存在"SafeFn 插队"的问题,顺序是确定的

1.6. 3.4.6 优雅退出:排空邮箱

上一节提到了 Actor 的 Run 方法在收到关闭信号后会排空邮箱。这里补充完整的关闭流程:

                          ctx.Done()
                             ↓
┌──────────────────────────────────────┐
│            Actor.Run                  │
│                                      │
│  shouldExit = true                    │
│  ┌─────────────────────────────┐     │
│  │ 继续处理 Mailbox 中的消息   │     │
│  │ msg1 → msg2 → msg3 → ...   │     │
│  └─────────────────────────────┘     │
│  n == 0 → return                    │
│                                      │
│  Close:取消 ctx、取消远端订阅、     │
│  关闭 mailBoxQueue、释放 workerPool  │
└──────────────────────────────────────┘

关键点:

  1. 不丢消息:关闭信号只是设置标志,不中断处理
  2. 最终一致:Mailbox 中的消息会全部处理完才退出
  3. 资源释放Actor.Close 会取消总线订阅、关闭邮箱队列、释放协程池等;连接侧 readBuffer / RingBuffer 属于 Channel 生命周期,不在此节展开

Group 层面还有 WaitForDrain,在超时内等待所有 superviseActor goroutine 结束(内部 g.wg.Wait()):

func (g *Group) WaitForDrain(timeout time.Duration) bool {
    done := make(chan struct{})
    go func() {
        g.wg.Wait()
        close(done)
    }()
    timer := time.NewTimer(timeout)
    defer timer.Stop()
    select {
    case <-done:
        return true
    case <-timer.C:
        return false
    }
}

1.7. 3.4.7 本节要点

  1. SafeFn 路径safeUpdate 封装为 ActorCmd 入队,与业务处理 FIFO 串行
  2. 可调项示例UpdateWorkerPoolSizeUpdateRateLimitUpdateMaxRPCPendingRebuildWorkerPool(见 hotupdate.go)。
  3. 设计动机:用邮箱顺序代替对外部可见字段的随意写,减少与 Run 的隐蔽竞态。
  4. 关闭ctx.Done() / closeChRun 排空再退出;Group 侧可 WaitForDrain 等待监督协程结束。

3.5 节:进程内 RPC 熔断与连接层限流(与 3.3 的熔断叙述衔接)。

results matching ""

    No results matching ""