Files
lislgosms/docs/phase-5-gateway-capacity-expansion-plan.md

530 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CMPP Gateway 容量扩展与回调降载方案
更新日期:2026-08-25
状态:已实施并完成测试环境验证
适用范围:发送 Worker/Submit Outbox 到供应商 CMPP Submit、SubmitResp、状态报告、上行和计费结算链路
## 1. 已确认的供应商能力边界
本方案按以下已确认条件设计:
- 同一个供应商账号支持建立多条 CMPP 长连接;
- 单通道最多支持 8 条连接;
- 单连接滑动窗口最大支持 64
- 连接数和窗口上限表示技术许可,不等于供应商账号承诺 TPS;
- 实际配置仍必须服从账号总 TPS、通道限速、供应商网关稳定性和生产合同限制。
CMPP 规范建议窗口值为 16;本项目允许使用 32/64,是基于供应商明确支持的扩展能力。禁止未经阶梯验证直接启用 `8 连接 × 64 窗口`
单通道理论最大在途 Submit 数量为:
```text
连接数 × 单连接窗口 = 8 × 64 = 512
```
512 是未收到 SubmitResp 时允许并行等待的最大请求数,不代表可以稳定达到 512 TPS。稳定 TPS 还取决于 SubmitResp 延迟和供应商账号限速,例如平均响应时间为 200ms 时,窗口理论容量不是首要限制;如果平均响应时间接近 1 秒,窗口容量才会直接限制吞吐。
## 2. 改造目标与边界
目标:
1. 逐步提高每个供应商通道可承载的并发 Submit 数量;
2. 在单 Gateway 内完成多连接和窗口扩容,多个 Gateway 水平扩展降为 P2 后续可选项;
3. 将协议日志从发送和回调热路径移出;
4. 将逐事件 HTTP 回调改为可重放、逐事件幂等的批量回调;
5. 在扩容过程中保持消息、Submit、回执和计费不重不漏;
6. 使用非零单价验证真实计费并发,不以零计费压测结果作为容量依据。
本阶段不改变:
- 对外 CMPP 协议和企业账号;
- 签名、模板、余额、频控和企业/应用状态拦截语义;
- 通道组主备补发规则;
- `messageId``submitId`、结果事件和计费幂等口径;
- PostgreSQL Submit Outbox 作为 Gateway 命令事实来源的设计。
## 3. 改造一:单通道多连接与窗口扩容
### 3.1 连接池模型
每个启用通道维护独立连接池,最多创建 8 条连接。每条连接独立维护:
- `connectionId` 和 Gateway 实例标识;
- CMPP CONNECT/CONNECT_RESP 认证状态;
- 独立递增并循环使用的 SequenceId
- 当前未确认 Submit 数量;
- `sequenceId -> submitId` 在途映射;
- 当前窗口大小和剩余窗口;
- SubmitResp 延迟、超时和连续失败次数;
- ACTIVE_TEST 心跳、断链和重连状态;
- 平滑启用、排空和关闭状态。
连接选择采用“可用窗口优先 + 最少在途 + 健康度”策略:
1. 排除未认证、正在重连、正在排空的连接;
2. 排除窗口已满的连接;
3. 优先选择未确认请求最少的连接;
4. 数量相同时优先选择近期 SubmitResp P95 更低的连接;
5. 连续超时连接进入冷却期,不再分配新 Submit。
### 3.2 动态配置
建议新增或明确以下配置:
```text
desiredConnections: 1..8
windowSize: 1..64
connectionWarmupSeconds: 30
connectionDrainTimeoutSeconds: 60
submitResponseTimeoutSeconds: 60
connectionFailureCooldownSeconds: 30
```
配置校验必须同时存在于管理 API 和 Gateway
- `desiredConnections < 1``> 8`:拒绝保存/拒绝启动;
- `windowSize < 1``> 64`:拒绝保存/拒绝启动;
- 扩容时逐条建立连接,不能同时突发 8 次登录;
- 缩容时先停止向目标连接分配新 Submit,等待在途请求完成后断开;
- 修改窗口时只影响新请求,不能丢弃已有在途映射。
### 3.3 阶梯扩容参数
| 阶段 | 每通道连接 | 单连接窗口 | 理论在途/通道 | 用途 |
|---|---:|---:|---:|---|
| M0 | 1 | 16 | 16 | 当前兼容基线 |
| M1 | 2 | 16 | 32 | 优先验证多连接正确性 |
| M2 | 2 | 32 | 64 | 验证窗口扩容 |
| M3 | 4 | 32 | 128 | 中等容量目标 |
| M4 | 4 | 64 | 256 | 高容量目标 |
| M5 | 8 | 64 | 512 | 上限验证,默认不直接用于生产 |
每一级必须单独通过正价 smoke、阶梯压测、故障注入和队列排空后才能进入下一级。
### 3.4 异常与幂等处理
- 收到 SubmitResp 后必须先用连接和 SequenceId 找到稳定 `submitId`,再释放窗口;
- 未找到在途映射的迟到 SubmitResp 进入异常对账,不能静默丢弃;
- TCP 写入前失败可安全重新分配连接;
- TCP 写入成功但未收到 SubmitResp时标记 `unknown`,禁止直接补发;
- 连接断开后只处理该连接的在途请求,不能把整个通道的消息统一重发;
- `submitId` 必须跨连接、跨实例保持唯一;
- 主备通道补发必须使用新的 `submitId`,并通过 `retryOfSubmitRecordId` 关联原提交。
## 4. P2 后续项:Gateway 水平扩展(当前不实施)
多个 Gateway 会引入通道连接所有权、分布式连接配额、租约、fencing token、脑裂、跨实例 pending 接管和“已写入供应商但未收到响应”的 unknown 判定,改造风险明显高于单 Gateway 内扩容。
因此本项调整为**优先级 P2 的后续可选改造**:
- 不纳入当前开发和压测范围;
- 不作为本轮达到目标 TPS 的前置条件;
- 当前先使用单 Gateway 管理同账号多连接,单通道仍遵守最多 8 条连接、单连接窗口最多 64;
- 只有单 Gateway 的 CPU、网络、文件描述符或事件循环成为经验证的主要瓶颈,且单实例优化无法继续提升时,才重新评审本项;
- 未完成独立设计评审、故障模型验证和隔离环境演练前,不得直接部署双 Gateway。
以下内容仅作为 P2 预研设计保留,不代表当前实施计划。
### 4.1 部署模型
部署至少两个 Gateway 实例,每个实例具有唯一的:
- `gatewayInstanceId`
- Redis Stream Consumer Name
- 指标端口和健康检查地址;
- 通道连接所有权和连接标识。
所有实例加入同一个 Submit Stream Consumer Group,共享:
- Submit 命令队列;
- 通道全局 TPS 限流;
- Submit 发布和处理幂等键;
- 结果/回执 Outbox
- 通道启停配置版本;
- 待恢复和死信状态。
### 4.2 推荐的首期分片方式
如果以后启动 P2,首期采用“通道分片 + 故障接管”:
- 正常状态下,一个通道只由一个 Gateway 实例持有连接;
- 不同运营商、主通道和备通道分散到不同实例;
- 实例失联后,备用实例经过租约超时再接管通道;
- 同一个通道不能被两个实例无租约地同时建立全部连接。
后续如果需要同账号跨实例并发连接,必须增加分布式连接配额:同一通道所有 Gateway 实例的连接总数不得超过 8。
### 4.3 接管与脑裂保护
- Redis 或 PostgreSQL 中保存带过期时间的通道所有权租约;
- 租约包含 `channelId``gatewayInstanceId`、配置版本和 fencing token
- 只有持有最新 fencing token 的实例可以领取该通道的新 Submit;
- 旧实例恢复后必须先重新竞选租约,不能直接恢复发送;
- Stream 未确认消息通过 `XAUTOCLAIM` 接管;
- 对“可能已写入供应商 TCP”的请求不得盲目重发,应进入 unknown 对账。
### 4.4 负载均衡边界
Gateway 水平扩展解决进程 CPU、网络、连接和单机故障问题,但不能突破:
- 单通道最多 8 条连接;
- 单连接窗口最多 64
- 供应商账号总 TPS
- PostgreSQL、Redis和回调服务的实际容量。
## 5. 改造三:协议日志降载
### 5.1 日志分级
| 日志等级 | 示例 | 处理方式 |
|---|---|---|
| 必留异常 | 拒绝、超时、断链、认证失败、重复/未知响应 | 100%保留,异步优先写入 |
| 业务摘要 | Submit、SubmitResp、回执、上行、下行应答 | 异步批量写入 |
| 原始协议包 | 完整请求/响应字段或二进制包 | 正常流量采样,异常全量短期保存 |
### 5.2 实现方案
- Gateway 不在 Submit/SubmitResp 热路径同步写协议日志数据库;
- 日志事件写入独立 Redis Stream
- 独立日志 Worker 每批领取 100500 条;
- 使用批量插入和批量状态回写;
- 正常成功事件按通道配置 1%~10% 采样;
- 错误、超时、拒绝和断链事件保持 100%;
- 原始包写滚动文件或对象存储,主库只保存检索摘要和对象地址;
- 正常原始包建议保留 3~7 天,异常日志按审计策略保留;
- Redis 日志 Stream 设置最大长度和积压告警。
日志积压不得阻塞 Submit。积压超过门槛时允许降低正常成功日志采样率,但禁止丢弃错误、安全和账务相关事件。
### 5.3 必须保留的追踪字段
- `messageId``submitId``eventId`
- `channelId``gatewayInstanceId``connectionId`
- SequenceId 和供应商 Msg_Id
- 事件类型、方向、结果码和耗时;
- 重试关联和原始日志对象地址。
## 6. 改造四:Gateway 批量回调协议
### 6.1 接口设计
新增内部接口:
```text
POST /api/gateway/events/batch
```
批次请求包含:
- `batchId`
- `gatewayInstanceId`
- `createdAt`
- 1100 个事件;
- 每个事件独立的 `eventId``type``payload`
首期支持事件类型:
- `submit_segment_result`
- `submit_result`
- `receipt_intake`
- `uplink`
- `dead_letter`
计费不由 Gateway直接生成。计费由 Submit结果/回执状态机依据稳定业务键执行,批量回调只负责可靠传递触发事实。
### 6.2 成批策略
采用“数量或时间先到先发”:
- 默认最大 50 个事件;
- 可配置上限 100
- 默认最大等待 10ms
- 最大请求体 1MB
- Submit结果优先级高于普通协议日志;
- 回执和上行可以使用独立批次,避免不同慢操作互相阻塞。
### 6.3 响应与重试
API 必须返回逐事件结果,而不是只有整批成功或失败:
```json
{
"batchId": "CB-001",
"results": [
{ "eventId": "EV-001", "accepted": true },
{ "eventId": "EV-002", "accepted": false, "retryable": true, "errorCode": "DB_BUSY" }
]
}
```
- Gateway 只 ACK `accepted=true` 的事件;
- 可重试失败使用指数退避;
- 不可重试失败进入死信并告警;
- HTTP 超时后允许重发整个批次,API 依靠 `eventId` 幂等;
- API 可以按事件类型及每 20~50 条拆成短事务;
- 单个坏事件不能造成整批回滚。
### 6.4 数据库批量处理
- `eventId` 建唯一索引或使用既有稳定幂等键;
- Submit结果批量更新 `SmsSubmitRecord`
- 回执先批量写入耐久 Receipt Inbox,再异步完成聚合;
- 计费使用稳定计费键批量创建,重复事件不得重复扣费;
- 消息和任务进度使用集合更新,避免逐事件 `GROUP BY`
- 事务中禁止调用外部 HTTP、Redis或供应商接口。
### 6.5 兼容与回退
- 保留原单事件回调接口;
- 使用开关选择单事件或批量接口,同一事件不能双发;
- 新旧入口共享 `eventId` 幂等逻辑;
- 回退只切换发送方式,不删除回调 Outbox 或幂等记录;
- 回退前必须等待在途批次完成并核对 Redis PEL。
## 7. 当前实施顺序与 P2 后续项
### 当前步骤 1:可观测性和协议日志降载
先补齐连接、窗口、供应商响应延迟和日志积压指标,再将成功协议日志异步批量化。该阶段不提高连接数。
### 当前步骤 2:单实例多连接
实现连接池、窗口隔离、平滑扩缩容和断链恢复,依次验证 `2×16``2×32``4×32`
### 当前步骤 3:批量回调协议
上线批量接口和逐事件幂等,先影子对账,再切换 Submit结果,最后切换回执和上行。
### 当前步骤 4:单 Gateway 容量上限验证
只有前述阶段稳定后,才在单 Gateway 内测试 `4×64``8×64`。生产建议值依据压测结果确定,不以供应商允许的最大参数直接作为生产配置。
### 优先级 P2:多 Gateway 水平扩展
当前不实施、不部署、不纳入本轮压测。将来只有证据表明单 Gateway 自身成为主要瓶颈时,才单独立项,执行通道分片、租约防脑裂、故障接管和 unknown 对账验证。
## 8. 正价压测方案
### 8.1 前置条件
- 仅在隔离测试环境执行;
- 压测前完成数据库、环境文件、部署目录和 systemd 配置备份;
- 测试应用单价设置为非零值,并记录压测前余额;
- 使用可明确识别的移动、联通、电信混合号码;
- 主备六通道均处于预期状态;
- Submit、结果、回执、日志 Stream 和数据库 Outbox 初始无积压;
- 压测前记录 Gateway 实例、连接数、窗口和供应商限速配置。
### 8.2 阶梯
每个连接/窗口档位分别执行:
1. 9 条正价全链路 smoke
2. 20 TPS
3. 30 TPS
4. 50 TPS
5. 70 TPS
6. 100 TPS
7. 达标且无停止条件时再测试 150/200 TPS。
每档结束后等待完整排空并对账,不能只检查入口 SubmitResp。
### 8.3 验收指标
- 客户 SubmitResp 接收数和唯一 MessageId 数;
- Inbox 完成数;
- 首次供应商 Submit 数和每秒吞吐;
- 各通道、各连接的提交数和在途峰值;
- SubmitResp P50/P95/P99
- 供应商接受、拒绝、超时和 unknown 数;
- 主备补发关联完整性;
- 最终 delivered/failed/submitted/unknown 数;
- 回调批次大小、请求数、重试数和积压;
- 数据库连接池等待、锁等待和慢 SQL;
- 协议日志 Stream 积压和丢弃/采样计数;
- 计费记录数、计费单位、扣费金额与余额变化。
正价计费恒等式至少满足:
```text
压测前余额 - 压测后余额
= 有效计费记录金额合计
= 各消息 billingUnits × 应用单价之和
```
### 8.4 停止条件
出现任一情况立即停止升档:
- SubmitResp 缺失或 MessageId 重复;
- `submitId` 重复提交;
- 重复计费、漏计费或余额不平;
- 供应商超时/unknown 持续增加;
- Redis Stream、Inbox、Outbox或回调持续积压且无法在规定时间排空;
- 数据库连接等待、锁等待或 idle transaction 异常;
- Gateway 频繁断链或连接数不能恢复;
- 通道禁用、主备补发或发送拦截语义异常;
- 日志降载导致错误、安全或账务事件丢失。
## 9. 故障回归
必须覆盖:
- 单连接断开,其余连接继续发送;
- 同一通道从 4 条连接缩到 2 条连接,在途请求不丢失;
- 单 Gateway 进程重启后恢复连接、在途状态和 Stream pending
- Redis 短暂不可用;
- 批量回调 HTTP 超时后整批重放;
- 回调批次中单个非法事件不拖累其他事件;
- PostgreSQL 回调连接池耗尽时不阻塞 Submit Outbox
- 主通道 Submit拒绝触发备通道关联补发;
- 主通道禁用后只使用备通道;
- 主备同时禁用时发送在供应商前失败;
- 签名/模板未报备、余额不足、企业/应用删除或禁用、号码频控仍能正确拦截;
- 重启和事件重放不造成重复提交、重复回执或重复计费。
多 Gateway 的实例宕机接管、租约、fencing token 和脑裂测试属于 P2 后续项目,不纳入当前故障回归范围。
## 10. 容量预期
容量提升不能简单相加。最终吞吐由最慢环节决定:
```text
实际 TPS = min(
入口能力,
Worker/Outbox能力,
Gateway连接与窗口能力,
供应商账号总TPS,
回调与计费能力,
PostgreSQL/Redis能力
)
```
以最近一次测试记录的完整供应商首提约 33 TPS 为历史参考,执行前必须重新验证当前基线。工程预期区间如下:
| 完成范围 | 预期稳定首提吞吐 |
|---|---:|
| 独立回调 + 批量路由/持久化 | 50~70 TPS |
| 加协议日志降载 + `2×16`/`2×32` | 60100 TPS |
| 加 `4×32``4×64` | 80150 TPS |
| 单 Gateway `8×64` 上限验证 | 由供应商总TPS及单机实测决定,不预先承诺 |
| 多 Gateway 水平扩展(P2) | 当前不实施,另行立项后评估 |
| 300500 TPS | 不作为当前承诺;未来可能需要供应商总TPS、通道数量和基础设施共同扩展 |
上述数字是容量规划目标,不是承诺值。即使单通道理论支持 512 个在途请求,如果供应商账号总限速为 50 TPS,平台稳定吞吐仍不会超过约 50 TPS。
## 11. 生产推荐原则
- 生产参数使用“最低但足够”的连接数和窗口,不长期顶格运行;
- 初始推荐从 `2 连接 × 16 窗口` 开始;
- 只有监控证明窗口经常耗尽且供应商响应稳定,才提高到 32/64;
- 当前不增加 Gateway 实例;只有单实例 CPU、网络、文件描述符或事件循环成为经验证且无法继续优化的瓶颈,才启动 P2 水平扩展评审;
- 保留连接数、窗口、批量回调、日志采样和 Gateway分片的独立回退开关;
- 每次只改变一个主要容量变量,保留可比较的正价压测基线。
## 12. 2026-08-25 实施与测试结论
本轮已实施本方案中除“多 Gateway 水平扩展(P2)”外的改造,并仅发布到测试环境 `100.93.204.60`。发布前恢复点为 `/opt/cmpp-platform-backups/phase5-capacity-20260825T041622Z`,数据库、运行源码和 systemd/环境配置归档均已校验;预生产环境未操作。
已交付能力:
- Gateway 协议摘要日志改为 Redis Stream 异步写入,成功事件确定性采样 10%,拒绝、超时、断链和非零结果 100% 保留;独立 `cmpp-protocol-log-worker` 使用独立 4 槽 PostgreSQL 连接池批量落库,成功后 ACK/XDEL,失败重放,格式错误进入死信 Stream。
- 单通道支持 1~8 条供应商连接、每连接 1~64 窗口;连接选择按在途数量和近期 RTT,连续失败进入冷却。扩容串行预热;缩容先停止分配、等待在途结束或排水超时后关闭。
- Gateway 结果 Outbox 支持最多 50 条、最长等待 10ms 的批量 HTTP 回调;Submit 结果、分片结果、回执、上行和死信均返回逐事件结果,整批 HTTP 失败留在 PEL 重放,单个非法事件不拖累同批其他事件。
- API、Gateway 控制入口和实际运行状态完成三层校验。运行矩阵覆盖 `1x1``2x16``4x32``8x64``4x64``2x32`,越界 `0/9` 连接及 `0/65` 窗口均被拒绝。
- 上行增加 `eventId` 唯一幂等键;同一批量上行事件重放两次只生成一条 `SmsUplinkMessage`
发布后曾发现协议日志发布器在仅配置 `REDIS_HOST/REDIS_PORT` 的环境中没有对空 `REDIS_URL` 回退。该问题触发停止条件,未进入阶梯;修正为与其他 Gateway Redis 组件一致的本机回退、增加测试并重发后,冒烟显示发布 16、采样丢弃 47、错误 0,日志 Stream 最终 `pending=0/lag=0`
### 12.1 正价阶梯结果
测试单价为 325 分/计费条,三运营商混合号码,六个模拟供应商账号;压测连接配置为每通道 `2x32`。容量按非补发的首次供应商 Submit 统计。
| 目标档 | 客户 SubmitResp | 首次供应商 Submit | 首提跨度/实际速率 | 客户 P50/P95/P99 | 计费 | 结论 |
|---:|---:|---:|---:|---:|---:|---|
| smoke | 9/9 | 9 | 约 8.1 秒/低速冒烟 | 17/1727/1727 ms | 9 笔/2925 | 冷启动尾延迟后链路闭合 |
| 20 TPS | 199/199 | 199 | 约 9.8 秒/约 20 TPS | 14/21/29 ms | 199 笔/64675 | 通过 |
| 30 TPS | 299/299 | 299 | 9.840 秒/30.39 TPS | 16/33/61 ms | 299 笔/97175 | 通过 |
| 50 TPS | 499/499 | 499 | 10.086 秒/49.47 TPS | 22/62/89 ms | 499 笔/162175 | 通过 |
| 70 TPS | 699/699 | 690 | 10.351 秒/66.66 TPS | 94/4099/5323 ms | 690 笔/224250 | 触发停止线,不再升档 |
| 100/150/200 TPS | 未执行 | - | - | - | - | 按停止条件立即停止 |
70 TPS 档少 9 条首次 Submit 和计费并非丢失:9 条均为重复使用的测试号码命中“单号码 24 小时最多 10 条”,最终 `failed/RISK`,供应商提交和计费均为 0。该结果证明频控拦截有效,但 70 TPS 的 P95/P99 已从 50 档的 `62/89ms` 跃升到 `4099/5323ms`,目标 70 TPS 也只完成 66.66 TPS,因此本版本稳定上限定为 **50 TPS**;70 TPS 只能视作非稳定冲击档。
### 12.2 完整回归与恢复
- Submit/回执:各通过档客户 SubmitResp 无缺失,MessageId、submitId 和供应商 MessageId 无重复;模拟器 DELIVRD/失败/无回执比例按配置落库,三条 Stream 最终均 `pending=0/lag=0`
- 计费:20/30/50 档及 70 档未被风控拒绝的消息,计费笔数均等于有效消息数,单价全部 325,金额满足 `条数 x 325`;风控、余额不足和路由失败不计费。
- 主备:随机结果码 8 产生带 `retryOfSubmitRecordId` 的备通道补发;仅主通道停用时 9 条全部走 `LGST-M-B`,主备六通道全停时 9/9 `failed/ROUTE`、供应商提交 0。
- 上行/批量回调:有效上行与非法事件同批时互不影响,非法项为不可重试;同一 eventId 重放两次仅落一条上行记录。
- 拦截:签名未审核、模板强制匹配但未审核、余额不足分别为 9/9 `SIGNATURE/TEMPLATE/BALANCE` 且供应商提交 0;应用或企业停用时客户连接不能鉴权;频控在 70 档真实命中;所有临时状态均恢复。
- 平滑扩缩容:`LGST-M-P` 从 1 扩到 4,再在 20 TPS、20 秒发送期间缩到 1399/399 SubmitResp399 条首次 Submit,无重复 IDP95/P99=`49/142ms`,最终连接恢复 6/6。
- 故障:Gateway 重启后由 API 恢复连接;Redis 在队列排空时短暂停止,Gateway 健康进程保持,Redis 恢复后重启 Gateway/API 并恢复 6/6;整批 HTTP 超时重放和单事件隔离另有自动化测试覆盖。
- 环境恢复:10 个测试应用单价恢复 0,企业/应用/签名/模板/通道均恢复原状态,三条临时号段规则删除,六通道连接恢复 6/6。真实消息、回执和账务事实保留审计,不回写余额。
## 13. 2026-08-25 回执回放风暴修复与正价复压
旧结论中70 TPS的4秒级尾延迟不是前几轮Worker、Outbox或计费优化失效,而是Gateway在每条SubmitResp后并发扫描同账号pending回执;API仅查询pending,直到异步sent回调才改状态,因此多个扫描会读到同一批。旧版100/150冲击档客户回执重复曾达847/5101条,Gateway goroutine在150档峰值超过1.2万。
修复内容:
- Gateway同账号flushPending单飞,SubmitResp触发改为25ms尾随合并、250ms最长等待,登录和周期恢复保留;
- API使用FOR UPDATE SKIP LOCKED原子将pending领取为dispatching,绑定claimId和5120秒可控租约;当前Gateway默认30秒;
- 客户离线或SubmitResp屏障时用claim_released无损释放,不消耗业务重试预算;过期dispatching自动恢复为pending
- API新建回执后的直推也必须先原子取得dispatching + claimId,从根源消除直推与Gateway恢复各发一次的竞态;
- ACK超时定时器在ACK registry锁内完成指针赋值,Linux竞态检查不再报告该竞态。
发布仅到测试机100.93.204.60,预生产未操作。恢复点为/opt/cmpp-platform-backups/phase5-replay-hotfix-20260825T070815ZPostgreSQL custom dump、运行源码、systemd/环境配置和SHA-256均已校验。Linux go test -race ./internal/inbound、Gateway全包测试/go vet、API 44套520项和TypeScript构建通过。最终运行标记为761c123b65f09093bc379096188f3ee9ccb2e618+workspace.phase5.replayhotfix2.directclaim。
正价阶梯(单价325):
| 目标档 | 客户SubmitResp | 客户P50/P95/P99 | 首次供应商Submit跨度/速率 | 结论 |
|---:|---:|---:|---:|---|
| 20 | 199/199 | 5/10/16ms | 9.930s / 19.84 TPS | 通过 |
| 30 | 299/299 | 5/10/18ms | 9.854s / 30.34 TPS | 通过 |
| 50 | 499/499 | 7/35/47ms | 10.067s / 49.47 TPS | 通过 |
| 70 | 699/699 | 14/44/60ms | 10.666s / 65.54 TPS | 入口稳定,首提接近70档 |
| 100 | 999/999 | 24/51/72ms | 13.345s / 74.86 TPS | 入口通过,端到端未达100 |
| 150 | 1498/1498 | 49/87/114ms | 15.781s / 94.86 TPS | 冲击档,显示端到端天花板 |
| 200 | 1998/1998 | 50/84/110ms | 21.661s / 92.24 TPS | 冲击档,端到端不再增长 |
直推领取修复后另执行100 TPS正价复验:999/999P50/P95/P99=54/102/179ms999个唯一MessageId、999次非补发首提在12.488秒完成(80.00 TPS)。972条已产生最终回执的投递全部ACK,生命周期尝试972、重复0、最大尝试1。计费998条:995 charged、3条供应商最终失败后refunded;1条RISK在供应商前失败且未计费。27条模拟器no-receipt保持submitted,不是队列丢失。Inbox、Submit Outbox、下游pending/dispatching/awaiting_ack、未授权锁和idle transaction最终均为0。
最终结论分两种口径:客户入口SubmitResp受理已通过200 TPS冲击;完整供应商首提天花板约95 TPS。测试环境建议稳定限速70 TPS,保留约25%以上端到端余量;不将入口200 TPS宣称为供应商200 TPS。按用户要求,7个测试应用单价保持325^1380028/^1300028/^1890028三条临时号段规则保留,原100.91.249.119/32和新增127.0.0.1/32白名单均保留,未做测试后删除或归零。
## 14. 95 TPS天花板定向诊断
100/150 TPS正价复验确认,Gateway的6条连接和192个窗口没有饱和;150档连接等待仅0.006ms、供应商RTT约13.761ms、窗口峰值10,命令Stream lag峰值27。真正的平台内排队发生在耐久Inbox之后、BullMQ发送之前的独立CMPP入站工作流。
当前7个压测应用全部归属同一企业。尽管工作流配置为并发96、批次64,泵在一批完成后立即领取当时已有记录,100/150档实际平均批次仅5.84/9.79条,导致同一企业的日限、号码频控、消息持久化和正价账户冻结被大量小事务重复执行。150档Inbox完成延迟P50/P95为3.779/5.620秒,1498条首次供应商Submit用15.547秒完成,即约96.35 TPS;因此当前约95 TPS应定义为“单企业正价完整首提上限”,不能定义为多企业平台总上限。
下一阶段优先级:
1. 为入站工作流增加按企业分组的有界微批,目标批次32~64、最大等待20~50ms;
2. 不同企业允许并行,同企业账户冻结保持串行和幂等;
3. 继续合并配额、频控、消息及冻结流水的集合事务;
4. 分别验证单企业100/150 TPS,以及至少两个独立企业合计200 TPS;
5. 只有单Gateway资源被实测打满后才启动多Gateway P2。
诊断还发现Outbox原生SQL使用`CURRENT_TIMESTAMP`,与Prisma UTC时间混用时会产生8小时审计偏差。该问题不影响本次吞吐和队列排空结论,但必须在下一次代码改造中统一为UTC并回归租约、重试和发布时延。
## 15. 单企业工作流微批治理实施范围
本轮只在单Gateway架构内处理95 TPS定向诊断暴露的工作流问题,不实施多Gateway P2。具体改造为:领取前最多等待40ms把小批合并到目标32条;按企业分组,不同企业并行、同企业各批串行;领取SQL排除正在执行的企业;Submit Outbox及关联消息的原生SQL时间统一为UTC。环境参数必须显式配置并由发布脚本校验,目标批量不得超过实际批次上限。
发布验收保持应用单价325以及现有临时号段规则和白名单。先做单企业100/150 TPS,以确认原约95 TPS边界是否改善;再使用至少两个独立企业做聚合200 TPS,用于区分单企业账户一致性边界和平台总吞吐。所有档位都按非补发首次供应商Submit计算,并对账Submit、回执、上行、账务、主备补发、拦截规则与队列排空;停止条件沿用第五阶段,不以入口SubmitResp替代完整链路结论。
## 16. 单企业微批与双企业并行实测结果
本轮提交`76e7c8c`实现按企业分组、有界40ms聚合、同企业串行/跨企业并行和Outbox UTC;压测中先后暴露两个独立连接问题:Node默认keep-alive短于Gateway连接池导致2条loopback reset/Result 9,提交`394949f`将API keep-alive/headers timeout固定为120/125秒;同一connectionId的connected/submit并发回调被误计为第二条连接,提交`b5005f2`改为同连接豁免且首次状态幂等upsert。每次修复均重新备份、发布和从100 TPS重测,多Gateway P2始终未实施。
| 场景 | 客户入口 | P50/P95/P99 | Inbox微批 | 非补发首次供应商Submit | 结论 |
|---|---:|---:|---:|---:|---|
| 单企业100 TPS | 999/999,全0 | 72/243/466ms | 23批,平均43.43、最大6410.721秒排空 | 999条/10.423秒=`95.85 TPS` | 入口完整,完整链接近100 |
| 单企业150 TPS冲击 | 1498/1498,全0 | 53/83/117ms | 33批,平均45.39、最大6412.453秒排空 | 1498条/12.192秒=`122.87 TPS` | 新单企业短窗上限约123,未达150 |
| 双企业合计200 TPS冲击 | 1999/1999,全0 | 80/148/287ms | 两企业各22批,平均44.55/46.32 | 1999条/15.449秒=`129.39 TPS`;两企业64.69/65.96 | 跨企业并行生效但受共享Worker/DB资源限制,未达200 |
双企业200档1999条均为单价3251988条charged金额646100、11条refunded金额35752145个包含主备补发的Submit/Outbox全部唯一且publishedOutbox最大发布时延0.111秒。1936条delivered、49条模拟器no-receipt保持submitted、11条最终失败已退款、3条首次接受后补发结果码8仍保留charged事实。压测客户端断开后形成的973条pending回执通过14账号零发送排空,最终该档1950条终态回执投递对应1950次ACK,重复0、最大尝试1。
容量结论需限定为10秒短窗口:单企业完整首提由原约96.35提高到122.87 TPS,约提升27.5%;双企业平台总量短窗上限约129.39 TPS,并未线性翻倍。未执行30分钟以上稳态,因此生产建议仍留余量:单企业限速90 TPS、当前单Gateway平台总限速100 TPS;入口可承受150/200冲击不能宣称完整链达到150/200。