perf: expand gateway capacity and prevent receipt replay

This commit is contained in:
hectorzhao
2026-08-25 16:08:30 +08:00
parent 761c123b65
commit 9292352be1
48 changed files with 2001 additions and 144 deletions
@@ -2148,3 +2148,14 @@
- Gateway供应商事件必须与主API控制面隔离:Submit结果、分片结果、回执、上行、受限协议日志和死信进入仅绑定回环地址的独立进程及独立有界PostgreSQL池;连接状态控制继续进入主API。客户HTTP Webhook使用异步队列Worker,不能在Gateway事实回写请求中同步调用客户地址。
- 独立回调进程不得加载运营/计费管理Controller,不得经Nginx或公网暴露;部署必须验证回调健康、池上限、回环监听、事件URL和控制URL。回调服务先于Gateway启动/重启,回调失败应由既有结果Outbox恢复且不得重复处理、重复扣费。
- 本阶段测试环境以正价325金额单位验证:50 TPS档必须同时满足客户端受理、非补发首次供应商Submit不低于50条/秒、MessageId/submitId/Outbox唯一、账单笔数和金额一致、Inbox/Outbox/Stream排空及数据库无持续锁等待。零计费结果不得作为付费链路容量结论。
## 单 Gateway 容量扩展第五阶段(2026-08-25
- 单个供应商通道必须支持1~8条连接、每连接1~64窗口,并在API校验、Gateway控制入口和实际运行状态三层执行相同边界;扩容需串行预热,缩容需先停止分配并等待在途完成,不能因改配置直接丢弃在途映射。
- Gateway协议摘要日志不得在Submit热路径同步写数据库或逐条HTTP回调。成功事件可确定性采样,拒绝、超时、断链和非零结果必须全量进入Redis Stream,由独立有界数据库池批量持久化;ACK只能发生在数据库提交后。
- Submit结果、分片结果、回执、上行和死信应由结果Outbox聚合为最多100条、默认50条的回环HTTP批次。API必须逐事件返回accepted/retryable/errorCode;整批传输失败重放,单个非法事件不得拖累其他事件,事件必须依赖稳定幂等键。
- Gateway必须暴露连接数、配置/在途窗口、批量回调请求/事件/重试/死信和协议日志发布/采样/错误/Stream长度指标。回调、日志Worker和主API数据库池必须独立有界,PostgreSQL总连接预算不得因扩容失控。
- 正价容量仍以非补发首次供应商Submit和全链路对账为准。2026-08-25测试机实测20/30/50档稳定,70档出现4秒级P95且未达到目标,故当前单Gateway发布建议上限为50 TPS100/150/200按停止线未执行。多Gateway租约、fencing和分片属于P2,仍未实施。
- Gateway拉取待投递回执必须按账号进程内单飞,API必须用FOR UPDATE SKIP LOCKED将pending原子领取为带租约的dispatching;未实际发送的领取必须无损释放,租约过期可恢复。SubmitResp后只能合并调度账号级刷新,不得每条并发扫描同一批pending记录。
- API新建回执后的直推与Gateway恢复拉取必须共享同一dispatching + claimId所有权;直推未抢到记录时必须退出,不得与恢复路径各发一次。ACK超时定时器必须在ACK注册锁内完成指针赋值,避免极短截止时间下的竞态。
- 2026-08-25修复后正价阶梯客户入口20/30/50/70/100/150/200 TPS均零拒绝、零节流、零连接错误;完整供应商首提在150/200冲击档分别约94.86/92.24 TPS,显示端到端容量天花板约95 TPS。二次直推领取修复后100 TPS正价复验为999/999、P95/P99=102/179ms、999次999个唯一首提在12.488秒完成(80.00 TPS),972条已形成终态回执的下游投递生命周期尝试次数恰为972、重复0、最大1。生产建议仍保留容量余量,建议限速70 TPS,不将客户SubmitResp受理200 TPS误作完整供应商TPS。
@@ -0,0 +1,493 @@
# 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白名单均保留,未做测试后删除或归零。
+17
View File
@@ -4864,3 +4864,20 @@ npm run verify:phase8
| TC-CMPP-CALLBACK-001 | Gateway事件进程隔离 | Submit结果、回执、上行、协议日志和死信只进入回环回调进程及独立12槽池;连接状态仍进入主API |
| TC-CMPP-CALLBACK-002 | 回调安全边界 | 回调进程仅监听127.0.0.1,不加载计费管理Controller;健康与指标端点只在回环可见 |
| TC-CMPP-BATCH-003 | 正价50 TPS完整提交 | 单价325499条首次Submit在9.930秒完成(50.25/s);499笔账单162175,无丢重,队列最终排空 |
## TC-CMPP-PHASE5 单 Gateway 容量扩展(2026-08-25
| 用例ID | 场景 | 验收结果 |
| --- | --- | --- |
| TC-CMPP-PHASE5-001 | API、Gateway控制入口、实际运行三层校验1~8连接/1~64窗口 | `1x1/2x16/4x32/8x64/4x64/2x32`通过;越界拒绝 |
| TC-CMPP-PHASE5-002 | 协议日志降载 | 成功日志确定性采样,异常全留;独立Worker批量写库后ACK/XDEL,错误0且Stream排空 |
| TC-CMPP-PHASE5-003 | 批量HTTP回调与重放 | 多事件少请求;逐事件结果;非法事件不影响有效事件;HTTP失败整批重放;重试/死信可观测 |
| TC-CMPP-PHASE5-004 | 上行eventId幂等 | 同一事件投递两次只生成1条SmsUplinkMessage |
| TC-CMPP-PHASE5-005 | 在途平滑缩容 | 单通道1→4→1并发发送399条,SubmitResp/首次Submit均399,重复ID为0,连接恢复6/6 |
| TC-CMPP-PHASE5-006 | 非零单价阶梯停止线 | 20/30/50通过;70档P95/P99=`4099/5323ms`且实得66.66 TPS,立即停止100/150/200,稳定上限50 TPS |
| TC-CMPP-PHASE5-007 | 拦截与主备回归 | SIGNATURE/TEMPLATE/BALANCE/RISK均供应商前拦截;应用/企业停用拒绝鉴权;主停走备、全停ROUTE;拒绝补发retryOf完整 |
| TC-CMPP-PHASE5-008 | Gateway/Redis故障与最终排空 | Gateway重启恢复连接;排空时Redis短停后恢复;Submit/结果/日志Stream pending/lag均0,无锁等待/idle事务 |
| TC-CMPP-PHASE5-009 | 同账号并发pending拉取 | 8个并发刷新只产生1次API领取;记录以FOR UPDATE SKIP LOCKED从pending转dispatching,携带claimId和租约 |
| TC-CMPP-PHASE5-010 | API直推与Gateway恢复同时命中同一回执 | 仅抢到dispatching + claimId的路径发送;100 TPS正价实测972条终态回执对应972次下游ACK,重复0,最大尝试1 |
| TC-CMPP-PHASE5-011 | claim未发送、租约过期和ACK定时器竞态 | SubmitResp屏障或客户离线时释放claim且不增加重试次数;过期dispatching可恢复;Linux go test -race ./internal/inbound通过 |
| TC-CMPP-PHASE5-012 | 修复后正价容量与口径分离 | 20至200 TPS入口均无拒绝、节流或连接错误;999条100 TPS复验入口P95/P99 102/179ms,供应商首提80.00 TPS150/200冲击档首提约95 TPS天花板,两种口径分开报告 |
+19
View File
@@ -3946,3 +3946,22 @@ git diff --check
- 修复后正价结果:smoke 9/9、账单292520 TPS 199/199、P50/P95/P99=`19/51/94ms`、账单6467530 TPS 299/299、`20/48/69ms`、账单9717550 TPS 499/499、`35/76/135ms`、账单162175。四个成功窗口合计1006条、1006笔账单、单价全部325、金额326950。
- 50档499条唯一首次供应商Submit跨度9.930秒,完整提交速率`50.25条/秒`,相对上一版同档33.07条/秒提高约52%。全部尝试/Outbox均542条且ID唯一;状态检查时delivered485、failed4、submitted10,关联补发43次。Inbox、Outbox、双Stream最终排空,数据库无等待锁、无idle in transaction,回调池`max=12,total=1,idle=1,waiting=0`
- 测试环境最终标记`c6f11014d61a8627ae311098ef81d89dd099ffaa+workspace.phase4batchcallback.callback-surface.599393bb6233`;恢复资产为`/opt/cmpp-platform-backups/phase4-batch-callback-20260825T025611Z`。10个隔离应用单价恢复0,三条临时号段规则删除,真实计费事实保留;预生产未操作,未执行100/200/300/500档。
## 2026-08-25 第五阶段单 Gateway 容量扩展与正价压测
- 完成协议日志 Redis Stream 降载及独立日志 Worker、单 Gateway 每通道 18 连接/每连接 1~64 窗口、RTT/在途择优、失败冷却、平滑扩缩容、Gateway 结果 Outbox 批量 HTTP 回调、逐事件确认和上行 eventId 幂等;明确未实施多 Gateway P2。
- API单测、Gateway控制入口和测试机实际运行三层覆盖 `1x1/2x16/4x32/8x64/4x64/2x32``LGST-M-P` 1→4→1 在20 TPS在途发送期间平滑缩容,399/399 SubmitResp、399条首次Submit、重复ID为0P95/P99=`49/142ms`
- 协议日志首次冒烟发现空`REDIS_URL`未回退,按停止线中止。修复、回归并重发后发布16、采样47、错误0,独立Worker排空。批量回调累计事件数大于HTTP请求数,重试/死信为0;同一上行eventId重放两次只落一条。
- 正价325阶梯:20档199条约20 TPS、P95/P99=`21/29ms`、6467530档299条30.39 TPS、`33/61ms`、9717550档499条49.47 TPS、`62/89ms`、16217570档699个SubmitResp690条有效首提在10.351秒完成66.66 TPS`4099/5323ms`690笔224250。另9条为频控`RISK`且供应商/计费均0。
- 70档尾延迟突增且未达到目标,立即停止,未执行100/150/200。稳定TPS上限定为50 TPS。
- 当前部署回归:签名、模板、余额分别9/9在供应商前拦截;应用/企业停用均无法鉴权;主通道停用9条全部走备通道,主备全停9/9`ROUTE`且提交0;随机主拒绝补发均有retryOf;Submit、回执、上行和计费对账通过。
- 发布只到`100.93.204.60`,恢复点`/opt/cmpp-platform-backups/phase5-capacity-20260825T041622Z`;预生产未操作。结束时应用单价0、临时号段规则0、业务配置恢复、六连接在线,队列排空且数据库无锁等待/idle事务。
## 2026-08-25 第五阶段回执回放热修复与正价复压
- 重新验证确认旧70 TPS尾延迟是回执恢复风暴:每条SubmitResp都启动pending扫描,多个扫描在sent回调前取到同一批。实施Gateway账号级单飞与25ms/250ms有界合并、API FOR UPDATE SKIP LOCKED pending到dispatching原子领取、claimId/30秒租约、未发送无损释放及过期恢复。
- 首轮复压还发现6191条中29条有2次下游尝试,进一步定位为API新建回执直推与Gateway恢复拉取之间没有共享所有权。直推改为先取得dispatching + claimId,未抢到则退出。Linux race门禁另发现并修复ACK超时timer指针赋值竞态。
- 门禁:API 44套520项、TypeScript构建、Gateway全包测试、go vet、Linux go test -race ./internal/inbound和git diff --check通过。只发布测试机,预生产未操作;恢复点/opt/cmpp-platform-backups/phase5-replay-hotfix-20260825T070815Z,最终标记761c123b65f09093bc379096188f3ee9ccb2e618+workspace.phase5.replayhotfix2.directclaim。
- 正价325阶梯入口SubmitResp20=199/199、P95/P99 10/16ms30=299/299、10/18ms50=499/499、35/47ms70=699/699、44/60ms100=999/999、51/72ms150=1498/1498、87/114ms200=1998/1998、84/110ms;全部零拒绝、零节流、零连接错误。完整供应商首提速率依次为19.84/30.34/49.47/65.54/74.86/94.86/92.24 TPS,端到端天花板约95 TPS。
- 最终100 TPS正价复验:999/999P50/P95/P99 54/102/179ms999个唯一MessageId1087个含主备补发的submitId全部唯一;999次999个唯一首提在12.488秒完成(80.00 TPS)。972条终态回执全部客户ACK,下游尝试972、重复0、最大1。计费998条中995 charged、3 refunded1条RISK供应商前失败不计费;27条no-receipt保持submitted。Inbox/Outbox/下游pending队列、未授权锁和idle transaction最终均0。
- 最终建议分口径:客户入口可受理200 TPS冲击,不等于端到端供应商200 TPS;供应商首提峰值约95 TPS,建议稳定限速70 TPS保留余量。按用户要求,7个应用单价保持325,三条临时号段规则、Tailscale白名单及VM回环白名单均保留,未做删除或归零。压测原始证据已复制到相邻测试项目lg-cmpp-stress-lab/results下的phase5-replayfix目录。