## Context 当前系统只有一个明细写入目标 `heartbeat.heartbeat_events`。需要新增 `heartbeat.heartbeat_events_g4_hot` 作为第二个独立写入目标,两路完全解耦,支持平滑迁移。 约束: - 同一 PostgreSQL 实例/数据库,共享同一连接池 - 数据来源(Kafka)、解包、校验、转换逻辑完全不变 - `room_status` 始终独立执行,不依赖任何明细写入 - 错误表仅服务新表写入失败 ## Goals / Non-Goals - Goals: - 旧/新明细表可通过启动配置独立开关 - 新表复用现有批量 COPY 写入内核 - 两路写入互不影响(错误隔离、重试隔离、fallback 隔离) - `room_status` 始终执行,不受明细开关影响 - 错误表仅记录新表失败 - 支持"旧开新关 → 双开 → 关旧留新"迁移路径 - Non-Goals: - 不引入第二套 PostgreSQL 连接配置(同库新表) - 不改变 Kafka 消费链路、消息格式、数据转换 - 不做运行时热开关(重启生效即可) - 不在本次改造中删除旧表相关代码 ## Decisions ### Decision 1: 共享连接池,不新建第二个 Pool - **What**: 旧表和新表都使用同一个 `pg.Pool` - **Why**: 两个目标表在同一 PostgreSQL 实例/数据库下,共享连接池避免资源浪费 - **Alternatives**: 为新表创建独立连接池 → 复杂度高,同库下无必要 ### Decision 2: 抽象通用写入内核而非复制代码 - **What**: 把现有 `insertHeartbeatEvents()` 中与表名绑定的逻辑抽象为 `_insertEventsToTarget(events, targetConfig)` - **Why**: 避免维护两份几乎相同的 COPY/INSERT 代码;新表和旧表的列清单不同但写入流程一致 - **Alternatives**: 复制一份改表名 → 双维护成本高 ### Decision 3: 编排层聚合结果,Processor 不感知双写细节 - **What**: `DatabaseManager.insertHeartbeatEventsDual()` 负责编排两路写入并聚合结果;`HeartbeatProcessor` 只消费聚合结果 - **Why**: 保持 Processor 职责单一,不引入 DB 路由逻辑 ### Decision 4: room_status 独立于明细写入 - **What**: `upsertRoomStatus()` 始终在 `processBatch()` 中独立执行,不依赖旧/新明细写入成功 - **Why**: 用户明确要求即使两路明细都关闭,仍需写 `room_status` ### Decision 5: 错误表仅服务新表 - **What**: `insertHeartbeatEventsErrors()` 仅接收新表 (`g4Hot`) 写入失败的记录 - **Why**: 旧表即将下线,错误追踪聚焦新表;旧表失败仅记日志/统计 ### Decision 6: 消费暂停基于"启用中的关键 sink" - **What**: 只有当前启用的写入路径全部发生连接级不可恢复故障时,才暂停 Kafka 消费 - **Why**: 避免新表结构/权限问题误停全局消费 ## Risks / Trade-offs ### Risk 1: 新表列映射不完整 - 新表字段比旧表更宽,旧数据可能缺少部分新表列(如 `svc_*`, `*_1`, `*_2`, `*_residual`, `guid`) - **Mitigation**: 旧数据确认可完整映射到新表必填字段与唯一键;新增列允许 NULL 或有默认值 ### Risk 2: 同库双写增加连接池压力 - 单批数据写两次,连接池使用量翻倍 - **Mitigation**: 两路顺序执行而非并发,复用同一连接池;必要时可调大 `maxConnections` ### Risk 3: 新表分区函数可能与旧表不同 - `heartbeat.ensure_partitions` 是否也涵盖新表的分区维护 - **Mitigation**: 实现时需确认分区保障函数对新表的覆盖,或为新表配置独立的分区保障调用 ## Migration Plan 1. 部署代码,保持 `DB_LEGACY_HEARTBEAT_ENABLED=true`、`DB_G4_HOT_HEARTBEAT_ENABLED=false` 2. 验证旧模式下行为不变 3. 开启 `DB_G4_HOT_HEARTBEAT_ENABLED=true`,双写观测 4. 确认新表写入稳定后,关闭 `DB_LEGACY_HEARTBEAT_ENABLED=false` 5. 过渡观察期后清理旧表相关配置 ## Open Questions - 新表 `heartbeat.heartbeat_events_g4_hot` 的分区预创建是否由同一个 `heartbeat.ensure_partitions` 函数覆盖?还是需要独立的分区保障函数? - 新表的 `guid` 列是否需要在写入时由应用层生成?还是有数据库默认值?