Files
lislgosms/docs/phase-4-send-worker-optimization-plan.md

79 lines
9.4 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 第四阶段后续性能优化方案(执行中)
更新日期:2026-08-24
当前状态:已完成第1步“现状复核与恢复准备”、第2步“P0可观测证据”、P1低风险数据库往返收敛和P2计费锁持有时间治理;Worker消息加载微批实测退化已回退,其余P2及P3不再继续。
第1步恢复资产:`/opt/cmpp-platform-backups/phase4-step1-20260824T020501Z`。资产包含PostgreSQL自定义格式备份、测试环境运行目录、systemd/Nginx/PostgreSQL/Redis配置、服务状态、迁移清单与SHA-256清单;`pg_restore --list`、tar目录和哈希复核通过。
## 1. 当前结论
测试环境以客户单价 `0.0325 元/计费条`、三运营商混合号码和六个隔离供应商账号执行最终 100 条/秒档。入口 2999/2999 受理,P50/P95/P99 为 `30/63/96ms`,但从首条消息入队到最后一次供应商提交耗时 `167.991 秒`,完整供应商提交吞吐约 `17.85 条/秒`。负载期一度有 1651 条消息停留在 `submit_queued`,说明瓶颈位于 BullMQ 发送 Worker 到 Gateway 之间的逐消息业务处理,不是六条供应商连接的协议容量。
## 2. 根因说明
当前发送 Worker 对每条消息依次执行完整消息查询、运营商/省份识别及回写、应用路由与通道连接查询、签名报备候选查询、最终通道签名二次确认、Redis 通道限速、会话累计更新、提交记录创建、消息状态更新、Gateway BullMQ 写入、Redis Stream 发布和批次进度聚合。
主要放大点:
1. 一条客户 CMPP 短信对应一个内部 `batchTaskId`,按任务键进行的进度单飞无法跨 2999 个内部任务合并。
2. 路由选择已读取签名报备候选,提交前又对最终通道执行一次报备查询。
3. 每条短信都在事务内更新所属通道的 `CmppSubmitSession.submitTotal`;六通道形成六个高频热点行。
4. 每条短信分别创建提交记录、更新消息并刷新内部任务,数据库往返数量随消息数线性增长。
5. 同一个 Gateway 命令当前同步写 BullMQ 和 Redis Stream;两者的恢复职责需要重新确认,不能未经验证直接删除任一边界。
6. Worker 并发高于数据库有效并发时只会增加连接等待;单纯继续提高 `API_SEND_WORKER_CONCURRENCY` 不能解决问题。
## 3. 后续优化顺序
### P0:补齐可观测证据
- 在测试环境启用 `pg_stat_statements`,或为发送热路径增加固定低基数耗时指标。
- 分别记录消息加载、号码识别、路由、签名报备、限速、提交事务、Gateway 发布和任务进度耗时。
- 同时记录 BullMQ waiting/active/completed、数据库连接池等待、事务耗时和六通道命令供给速率。
执行结果(2026-08-24):测试环境已启用`pg_stat_statements`;发送Worker已增加11个固定阶段直方图、BullMQ状态、Worker槽位、处理结果和PostgreSQL客户端池状态,标签不含业务实体。正价六通道100条/秒诊断档2999/2999受理,P50/P95/P99=`31/68/233ms`2999个唯一号码首次到达供应商覆盖`92.994秒`,约`32.25条/秒`,仍未达到完整100条/秒。阶段均值中`submit_transaction≈47.4ms`最高,其次`route_lookup≈33.5ms``message_load≈22.7ms``phone_routing≈18.5ms``task_progress≈18.2ms`Worker池waiting峰值3。`pg_stat_statements`显示企业账户advisory lock累计`390.932秒/587次`,开放会话累计UPSERT为`32.293秒/3616次`,因此P1仍应优先消除账户批次锁竞争和会话热点写入,再收敛逐消息查询与进度更新。
### P1:低风险数据库往返收敛
1. CMPP 单消息内部任务使用已知状态直接更新计数,不再执行整批 `GROUP BY`
2. 将活跃路由、在线通道和签名报备通过条件合并为一次受数据库事实约束的候选查询,取消第二次等价查询;不使用长期进程缓存替代实时停用/撤销状态。
3. 提交事务只复用已存在的开放会话 ID;`submitTotal`改为异步批量累计或从提交记录聚合,不让统计字段锁住真实发送。
4. 确认 Gateway BullMQ 与 Redis Stream 的消费、重放和死信职责;若存在重复同步发布,改为单一持久入口加可恢复 Outbox。
执行结果(2026-08-24):CMPP单号码内部任务在首次提交时按已知状态直接更新,结果、回执和超时回调在一条`UPDATE ... FROM`中读取唯一消息当前状态并写精确计数;最终压力窗口中应用产生的任务状态`GROUP BY`为0。路由规则、在线连接和签名运营商报备条件已合并到一次Prisma候选查询,取消最终通道二次报备查询且不缓存实时启停/报备事实。Worker按通道首次读取并复用`OPEN-{channelId}`会话ID,提交事务不再更新`submitTotal`热点行。Go Gateway仅消费`gateway.submit.commands` Redis Stream,其PEL自动认领、逐条ACK、结果Outbox和死信上报均有代码与测试;`gateway.submit.queue`没有消费者,因此发送Worker停止写入该BullMQ副本,压力前后遗留wait均为84119且不再增长。Redis Stream发布前的数据库/队列跨介质崩溃窗口仍没有独立PostgreSQL Outbox自动扫描器,本轮不把该残余风险伪称为已消除,留待P2微批Outbox一起治理。
最终正价六通道100档2999/2999受理,零拒绝、零节流、零连接错误,入口P50/P95/P99=`33/77/179ms`2999个唯一号码首次供应商提交覆盖`99.940秒`,约`30.00条/秒`,低于P0对照`32.25条/秒`,因此完整100条/秒仍失败并停止升档。Worker平均总耗时由约175.1ms降至约123.1ms`submit_transaction`由47.4ms降至26.8ms,路由加两次签名查询的原约47.3ms降为32.4ms,BullMQ发布阶段被消除;但同一企业正价回调的`pg_advisory_xact_lock`仍累计373.373秒/841次、均值443.963ms,继续压住共享数据库并发。这证明P1降低了逐消息往返,却没有解除剩余计费账户锁瓶颈;不应仅凭Worker阶段变快宣称吞吐提升。
### P2:发送微批处理
- Worker 在 510ms 窗口内领取最多 32 或 64 条任务。
- 使用 `WHERE id IN (...)` 批量加载消息,按应用、运营商、签名分组预取静态路由候选。
- 在线状态、停用状态和最终报备条件在提交批次内重新核验,不建立跨批长期缓存。
- 一次短事务批量创建独立 `SmsSubmitRecord`、更新独立 `SmsMessageRecord`,保留每条消息唯一 `submitId`、幂等键、补发和计费关联。
- Gateway 命令使用 Redis pipeline/批量发布;任一部分失败必须能根据 PostgreSQL 事实安全补发,不能重复提交或重复计费。
执行结果(2026-08-24):正价CMPP入站冻结仍保持任务、API请求、消息、冻结流水和账户余额同一PostgreSQL事务,但`tenant-account` advisory lock改为在独立业务行写入后、事务提交前才获取。最终正价六通道50档该锁598次累计3.241秒、均值5.420ms;对比P1的373.373秒/841次、均值443.963ms,累计等待下降约99.1%,均值下降约98.8%。
同轮试做5ms、最多20条的Worker消息批量加载,六通道50档完整供应商提交仅约22.20条/秒,低于P1约30.00条/秒;去除批次最慢项栅栏后仍无收益,因此已回退。回退后最终正价六通道50档1499/1499受理,入口P50/P95/P99=`31/77/227ms`;唯一号码首次到达供应商覆盖69.054秒,约21.69条/秒,未达50,按停止线未执100及更高档。最终代码只保留已证明有效的计费锁缩短;PostgreSQL Submit Outbox与批量路由/持久化未继续实施。
最终正价样本1499条,MessageId、号码均1499个且唯一,`unitPrice` min/max均325。冻结/释放各1499笔487175;提交扣费1496笔486200,最终失败退费21笔6825`SmsBillingRecord` charged1475笔479375、refunded21笔6825,净扣与账户流水一致。全部压力档均使用325正价,没有执0计费压测。结束后仅按测试前快照恢复10个应用单价为0并删除3条临时号段规则,恢复后未再压测。
### P3:容量参数复核
- 完成 P1/P2 后再让 Worker 并发与数据库连接预算匹配,初始建议按 24~32 个有效数据库槽验证,不直接扩大到更高并发。
- 按 PostgreSQL `max_connections` 为 API、Worker、Gateway 回调和运维连接保留独立余量。
- 通道 TPS、窗口和六连接继续独立限速,平台业务吞吐不得绕过供应商配置。
## 4. 正确性约束
- 正价短信继续执行真实冻结、接受后扣费、拒绝释放和最终失败退款;不得用单价 0 结果外推计费性能。
- 计费、报备、路由、消息状态、重试认领和回执幂等必须保留 PostgreSQL 稳定事实。
- 不得用进程内缓存保存余额、号码频控、在线状态或最终报备决策。
- 多分片长短信继续保留逐片持久化和聚合结果边界;不能为吞吐恢复重复单分片回调。
- 任何非向后兼容迁移必须同时提供 PostgreSQL 与运行源码恢复路径。
## 5. 后续验收方式
每轮变更均执行:自动化回归 → 正价 smoke → 50 → 100 → 200 → 300 → 500 条/秒。每档必须核对入口受理、Inbox、BullMQ、Gateway Stream、供应商提交、回执、上行、消息终态、冻结/释放/扣费/退款、数据库锁和连接池。出现丢失、重复、账务不一致、队列持续增长或数据库持续不稳定时立即停止升档。
500 条/秒只有在入口和完整供应商提交均持续达到目标、队列可在限定时间稳定排空且零丢重、账务恒等式成立时才判定通过。