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/s | 0.11ms | 1.3ms | 7 天 | 121 亿 |
| 500 连接高压 | 16 万 msg/s | 10ms | 55ms | 1 小时 | 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_bench 的 framework 模式——所有 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 P50 | RTT P99 |
|---|---|---|
| 第 9 秒 | 0.149ms | 1.376ms |
| 第 8 分钟 | 0.141ms | 1.354ms |
| 第 83 分钟 | 0.111ms | 1.279ms |
| 第 7 天 | 0.148ms | 1.361ms |
P50 在 0.11~0.19ms,P99 在 1.27~1.52ms。7 天无退化。 单次 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 P50 | 8.7~13ms |
| RTT P99 | 47~88ms |
吞吐基本线性增长,邮箱单核 16.7 万次/s。
延迟毛刺分析
7 天里所有延迟毛刺都来自外部干扰。每天凌晨 4 点 cron 定时任务触发,7 天 7 次全部出现异常:
| 日期 | RTT 采样数跌至 | P99 飙升 | Max |
|---|---|---|---|
| 04-04 | 29K(↓63%) | 17.8ms | 57ms |
| 04-05 | 35K(↓56%) | 12.4ms | 90.8ms |
| 04-06 | 10K(↓87%) | 22.2ms | 78.7ms |
| 04-07 | 18K(↓79%) | 18.0ms | 81.3ms |
| 04-08 | 25K(↓69%) | 16.1ms | 55.8ms |
| 04-09 | 2.6K(↓96%) | 71.6ms | 142ms |
| 04-10 | 3.1K(↓96%) | 88.4ms | 88.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 |
| GC | 7 天总暂停 46 秒(0.0076%) |
| 资源 | 25 goroutine,零泄漏 |
| 毛刺 | 全部来自外部干扰 |
没有黑魔法,就是对象池、批量处理、协程池管控。Go 写高性能服务,方向对了效果很好。
基础库:github.com/aiyang-zh/z…(MIT 协议)
项目地址:github.com/aiyang-zh/z…(上层引擎 AGPL-3.0 + 商业双授权)