Go Actor 引擎 zhenyi 压测报告:7 天 121 亿消息,P50 延迟 0.11ms

210 阅读4分钟

Go Actor 引擎 zhenyi 压测报告:7 天 121 亿消息,P50 延迟 0.11ms

基础库:github.com/aiyang-zh/z…(MIT) | 引擎:github.com/aiyang-zh/z… | 官网:zhenyi-site.pages.dev

标签:Go / Actor 模型 / 高性能 / IM / 分布式系统

前言

最近开源了一个用 Go 写的分布式实时应用 Actor 引擎 zhenyi,面向 IM、游戏、实时推送等长连接业务。框架层面的压测跑完了,7 天 121 亿次消息的数据拿出来分享一下。

核心数据

测试环境:阿里云 2 核 4G,客户端服务端同机部署,各占 1 核。

场景吞吐P50 延迟P99 延迟跑了多久总消息量
2000 连接长稳4 万 msg/s0.11ms1.3ms7 天121 亿
500 连接高压16 万 msg/s10ms55ms1 小时3 亿

关键结论:

  • 7 天 121 亿消息,QPS 和延迟零退化
  • GC 总暂停 46 秒,占 0.0076%,每次暂停 0.3 毫秒
  • 25 个 goroutine 跑 2000 连接,7 天零泄漏
  • 消息丢失率 1.9×10⁻⁹

测试架构

测试链路是完整的 4 跳回环:

Client → Gate Actor → IM Actor → Gate Actor → Client

每条消息经历 2 次 Actor 邮箱投递(Gate 入邮箱 → IM 入邮箱,IM 响应回 Gate 走快路径直达连接写回,跳过 Gate 邮箱入队)。客户端看到的 2 万 QPS,实际 Actor 邮箱吞吐是 4 万次/s。

服务端是 Gate + IM 两个 Actor,各配 500 worker pool。压测使用 im_single_demo_benchframework 模式——所有 handler 收到消息后直接返回预编码的固定 bytes,不做反序列化、不做 state 操作。测的是纯框架开销。

// framework 模式:收到就回,零业务逻辑
s.GetHandleMgr().RegisterHandle(MsgLoginReq, func(ctx context.Context, msg *zmsg.Message) {
    if selectedBenchMode == "framework" {
        s.SendToClient(msg, replyLoginOK) // replyLoginOK 是预编码的 []byte
        return
    }
    // business 模式下才有反序列化、state 操作等
})

场景一:2000 连接 × 7 天长稳

吞吐

QPS 从第 10 分钟起稳定在 20,000 msg/s,到第 7 天——纹丝不动:

运行 202 秒:  QPS = 20,161
运行 1,002 秒: QPS = 20,032
运行 10,002 秒: QPS = 20,003
运行 604,796 秒: QPS = 20,000

延迟

运行时间RTT P50RTT P99
第 9 秒0.149ms1.376ms
第 8 分钟0.141ms1.354ms
第 83 分钟0.111ms1.279ms
第 7 天0.148ms1.361ms

P50 在 0.11~0.19ms,P99 在 1.27~1.52ms7 天无退化。 单次 Actor 邮箱投递约 0.075ms。

GC 和资源

  • GC 频率:每秒约 0.25 次
  • 每次 GC 暂停:0.3 毫秒
  • 7 天 GC 总暂停:46 秒
  • Goroutine 数:25 个,7 天无增长
  • 内存:69~125MB,占 4G 机器的 ~3%

场景二:500 连接 × 1 小时高压

把发送间隔从 100ms 压到 1ms:

指标
客户端 QPS~83,500 回环/s
服务端吞吐~160,000 msg/s
Actor 邮箱吞吐~167,000 次/s
RTT P508.7~13ms
RTT P9947~88ms

吞吐基本线性增长,邮箱单核 16.7 万次/s。

延迟毛刺分析

7 天里所有延迟毛刺都来自外部干扰。每天凌晨 4 点 cron 定时任务触发,7 天 7 次全部出现异常:

日期RTT 采样数跌至P99 飙升Max
04-0429K(↓63%)17.8ms57ms
04-0535K(↓56%)12.4ms90.8ms
04-0610K(↓87%)22.2ms78.7ms
04-0718K(↓79%)18.0ms81.3ms
04-0825K(↓69%)16.1ms55.8ms
04-092.6K(↓96%)71.6ms142ms
04-103.1K(↓96%)88.4ms88.6ms

全局共 4 个采样窗口延迟超过 100ms,其中 3 个出现在 04-09 cron 触发后的连续窗口中,另 1 个出现在 04-08 下午(疑似外部操作干扰)。 Actor 运行时本身没有任何延迟抖动。

核心设计

1. 消息对象池 + 引用计数

自定义泛型对象池 zpool.Pool + 原子引用计数管理消息生命周期。Retain() 投递,Release() 回收,热路径零分配。池化对象带 Prometheus 可观测指标,双重释放有告警。

func GetMessage() *Message {
    msg := getMessagePool().Get()
    msg.PoolReset()
    atomic.StoreInt32(&msg.RefCount, 1)
    return msg
}

func (m *Message) Release() {
    newRef := atomic.AddInt32(&m.RefCount, -1)
    if newRef == 0 {
        if cap(m.Data) > 4096 {
            m.Data = nil // 大 buffer 不回池
        }
        getMessagePool().Put(m)
    }
}

2. MPSC 邮箱 + 批量处理

每个 Actor 一个 MPSC 邮箱,用 FastAdaptiveBatcher 动态调整批量大小。批量内每 50 条共享一次 time.Now(),节省系统调用。

batchSize := a.batcher.GetBatchSize(int64(lastBatchSize))
n := a.mailBoxQueue.DequeueBatch(msgs[:batchSize])

for i := 0; i < n; i++ {
    if i > 0 && i%50 == 0 {
        batchStartMs = ztime.ServerNowUnixMilli() // 每 50 条刷新
    }
    a.SafeHandleMessage(handleCtx, msgs[i], batchStartMs)
    msgs[i].Release()
}

3. 协程池

ants 协程池固定 500 worker,预分配 + 阻塞模式 + panic 保护。asyncTask 结构体也做了池化,避免闭包分配。

4. 零拷贝路由

Gate 路由到本进程 Actor 只做 Retain() + 指针投递,无序列化。跨进程才走 NATS 消息总线。

总结

指标结果
长稳性7 天 121 亿消息,QPS/延迟零退化
延迟P50 0.11ms,P99 1.3ms
GC7 天总暂停 46 秒(0.0076%)
资源25 goroutine,零泄漏
毛刺全部来自外部干扰

没有黑魔法,就是对象池、批量处理、协程池管控。Go 写高性能服务,方向对了效果很好。

基础库github.com/aiyang-zh/z…(MIT 协议)

项目地址github.com/aiyang-zh/z…(上层引擎 AGPL-3.0 + 商业双授权)