# CDR Redis Stream Runbook > 任务:S22 - CDR Redis Stream > 完成时间:2026-06-21 19:34 +08:00 ## 1. 目标 S22 建立原始 CDR 的 Redis Stream 契约和可靠消费基础,不做计费、不扣余额、不写正式 rated CDR。计费与余额扣减留给 S23。 ## 2. Redis Key | Key | 用途 | | --- | --- | | `stream:cdr_payload` | OpenSIPS 写入的原始 CDR Stream | | `stream:cdr_deadletter` | CDR Worker 校验或处理失败后的死信 Stream | | `billing-workers` | `stream:cdr_payload` 的 Consumer Group | | `lock:cdr:{event_id}` | Worker 处理幂等占位,避免同一 `event_id` 重复处理 | ## 3. 事件字段 S22 事件 schema version 为 `1`。必填字段: ```text schema_version event_id idempotency_key call_id node_id opensips_instance ingress_a_ip rtpengine_node source_ip sip_code hangup_reason created_at ``` 多 A 预留字段已经纳入契约: - `node_id`:A 节点 ID,当前单 A 为 `a1`。 - `opensips_instance`:OpenSIPS 实例名,当前为 `opensips-a1`。 - `ingress_a_ip`:接收 SIP 的 A 节点地址,当前为 `100.90.90.90`。 - `rtpengine_node`:媒体节点,当前每台 A 绑定本机 RTPEngine,因此为 `a1`。 S22 阶段 A 端尚未进入真实落地路由,因此失败 CDR 的供应商、录音、应答时间等字段使用 `none` 或 `0` 占位。S23/S24 后续在不破坏 schema 的前提下补充真实计费和录音字段。 ## 4. Producer Server A OpenSIPS 在 `route[S20_CDR_XADD]` 中执行: ```text XADD stream:cdr_payload * schema_version 1 ... created_at ``` 当前 A 端字段已从 S20 冒烟格式升级为 S22 正式契约。A 端配置备份: ```text /var/backups/lisglosips-s22/20260621T113157Z/opensips.cfg ``` ## 5. Consumer `packages/redis/src/cdr-stream.ts` 提供: - `ensureCdrConsumerGroup` - `publishCdrEvent` - `parseCdrStreamEvent` - `processCdrBatch` - `processPendingCdrBatch` `apps/worker-cdr` 已从骨架改为 Redis Stream Worker: 1. 读取 `REDIS_URL`。 2. 确认 `billing-workers` Consumer Group。 3. 先用 `XAUTOCLAIM` 重领 Pending,再用 `XREADGROUP` 读取新消息。 4. 用 `lock:cdr:{event_id}` 做幂等占位。 5. 处理成功或重复消息后 `XACK`。 6. 校验失败或处理异常时写入 `stream:cdr_deadletter` 并 `XACK` 原消息。 当前 handler 只记录并接受原始 CDR,不执行计费和余额扣减。 ## 6. 验证结果 - 本地 `packages/redis/src/cdr-stream.spec.ts` 6 条测试通过,覆盖发布、解析、Consumer Group 幂等创建、`XREADGROUP`/`XACK`、重复 `event_id`、死信和 `XAUTOCLAIM` Pending 重领。 - `corepack pnpm@10.33.0 lint` 通过。 - `corepack pnpm@10.33.0 typecheck` 通过。 - `corepack pnpm@10.33.0 build` 通过。 - A `opensips -C -f /etc/opensips/opensips.cfg` 通过,`opensips.service` active。 - Redis `XGROUP CREATE stream:cdr_payload billing-workers 0 MKSTREAM` 返回 `OK`。 - Redis `XINFO GROUPS stream:cdr_payload` 显示 `billing-workers`,pending 为 `0`。 - T 发起 INVITE 后,A 返回 `SIP/2.0 503 Config Missing`,Redis Stream 最新事件包含 `schema_version=1`、`idempotency_key`、`node_id=a1`、`opensips_instance=opensips-a1`、`ingress_a_ip=100.90.90.90`、`rtpengine_node=a1`。 ## 7. 回滚 代码回滚: - 恢复 `packages/redis/src/index.ts`、`packages/redis/src/cdr-stream.ts`、`packages/redis/src/cdr-stream.spec.ts`、`apps/worker-cdr/src/main.ts`。 - 重新执行 `corepack pnpm@10.33.0 build`。 Server A 回滚: ```bash sudo cp -a /var/backups/lisglosips-s22/20260621T113157Z/opensips.cfg /etc/opensips/opensips.cfg sudo opensips -C -f /etc/opensips/opensips.cfg sudo systemctl restart opensips ``` Redis Consumer Group 可保留,不影响 Producer;如必须移除,应先确认没有未处理 Pending,再使用受控 Redis 维护窗口删除 group。S22 不删除 Stream 数据。