在做短信平台这些年,CMPP 服务端的性能瓶颈,80% 都不是协议本身,而是并发模型设计的问题。很多系统“能跑”,但一上量就开始抖:连接堆积、消息延迟、窗口阻塞,甚至直接把网关拖垮。
这篇就把 CMPP 服务端并发模型怎么设计讲清楚,偏工程实践。
一、先明确几个核心约束
CMPP 本质是一个长连接 + 请求响应 + 窗口控制的协议,几个关键点决定了并发模型:
- 长连接(TCP) :一个客户端(SP)通常会建立多条链路
- 滑动窗口(Window) :限制未响应消息数量(常见 16/32/64)
- 双向通信:既要处理 Submit,也要下发 Deliver
- 强时序依赖:SequenceId 必须匹配响应
👉 结论:这不是简单的 HTTP 模型,而是一个强状态 + 高并发 + 低延迟系统。
二、常见错误模型(踩坑总结)
很多团队一开始会这么设计:
1. 一连接一线程
- 每个 TCP 连接绑定一个线程
- 问题:连接数一上来(几千级),线程直接爆炸
2. 线程池 + 阻塞 IO
- 用线程池处理请求
- 问题:CMPP 是长连接 + 高频 IO,线程上下文切换成本极高
3. 收发不分离
- 收消息、处理逻辑、发响应在同一线程
- 问题:一旦业务处理慢,直接阻塞整个链路
👉 这些模型在 TPS 上到几千后就会明显掉队。
三、推荐并发模型:Reactor + 多级队列解耦
实际线上稳定跑高并发的方案,一般是这个结构:
1. 网络层:Reactor(NIO/epoll)
-
使用 Netty / 自研 NIO
-
单机支持数万连接没问题
-
线程模型:
- Boss线程:处理连接
- Worker线程:处理读写事件
👉 核心原则:网络 IO 必须非阻塞
2. 协议解码层(轻量)
收到数据后只做三件事:
- 粘包拆包
- CMPP 报文解析
- 投递到业务队列
⚠️ 注意:
这里绝对不能做业务逻辑
3. 业务处理层(核心并发控制)
采用 多级队列 + 线程池隔离
队列拆分建议:
- Submit 队列
- Deliver 队列
- Report 队列
- 心跳处理(独立)
为什么要拆?
因为:
- Submit 是高频写操作
- Deliver 是下行推送
- Report 对时效要求更高
👉 混在一起一定互相拖慢
4. 窗口控制(关键中的关键)
CMPP 并发不是线程数,而是窗口数:
-
每个连接维护:
window_sizeunacked_count
发送时:
if (unacked_count < window_size) {
send();
unacked_count++;
}
收到响应:
unacked_count--;
👉 本质是一个信号量模型
5. 发送模型(异步化)
推荐:
- 发送队列 + 批量刷写(write buffer)
- 避免每条消息一次 syscall
优化点:
- 合并 write(Netty flush 策略)
- 减少锁竞争(无锁队列/MPSC)
四、进阶优化(决定上限)
1. 连接维度限流
- 单 SP 连接数限制
- 单连接 TPS 限制
- 防止“劣质客户拖垮全局”
2. 内存池化
- ByteBuf / DirectBuffer 复用
- 避免频繁 GC
3. SequenceId 生成策略
-
必须线程安全 + 高性能
-
常见方案:
- 原子递增(AtomicLong)
- 分段 ID(减少竞争)
4. 超时重试机制
- 未响应消息超时回收
- 防止窗口被“卡死”
5. 多机水平扩展
- 无状态设计(连接状态本地化)
- 前置 LB(四层负载)
- 按 SP / 账号分片
五、一套可落地架构总结
可以把整个并发模型抽象成:
[Netty Reactor]
↓
[解码 & 协议层]
↓
[多队列分发]
↓
[业务线程池]
↓
[发送队列 + 窗口控制]
↓
[异步写回]
核心设计原则就三条:
- IO 与业务彻底解耦
- 所有阻塞点必须异步化
- 并发控制靠窗口,而不是线程
六、经验结论(很重要)
-
CMPP 的上限不是机器性能,而是窗口设计 + 调度能力
-
真正稳定跑到高 TPS 的系统,一定是:
- 事件驱动(Reactor)
- 队列化(削峰)
- 限流(保护系统)
如果你现在的系统已经出现:
- 延迟抖动
- submit_resp 超时
- deliver 堵塞
基本可以确定:并发模型需要重构了,而不是简单扩容机器。