77 lines
4.0 KiB
Markdown
77 lines
4.0 KiB
Markdown
|
|
## 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` 列是否需要在写入时由应用层生成?还是有数据库默认值?
|