feat: 新增 G4 热表独立双写能力

- 新增配置项以支持旧/新明细表的独立写入开关及目标表名。
- 重构 DatabaseManager,抽象通用批量 COPY 写入内核,支持不同目标表的复用。
- 新增双明细写入编排器,支持旧/新表独立执行、重试及 fallback。
- 调整 HeartbeatProcessor.processBatch(),确保 room_status 独立执行。
- 错误表仅记录新表写入失败,旧表失败不再写入错误表。
- 重新定义消费暂停策略,基于当前启用的关键 sink 判断。
- 补充按 sink 维度的统计项与启动日志。

新增 G4 热表相关的数据库规范与处理逻辑,确保系统在双写模式下的稳定性与可扩展性。
This commit is contained in:
2026-03-09 15:49:12 +08:00
parent f59000f5ef
commit 43fa7505e5
21 changed files with 2546 additions and 154 deletions

View File

@@ -0,0 +1,419 @@
## 最终可执行的实施清单
以下清单基于你已经确认的最终约束整理,可直接作为实施顺序使用。
---
## 一、实施目标
在**不改变数据来源**、**不改变现有批次处理节奏**、**不改变 `room_status` 写法与目标** 的前提下,实现:
- 旧明细表 `heartbeat.heartbeat_events` 可独立开关
- 新明细表 `heartbeat.heartbeat_events_g4_hot` 可独立开关
- 新明细表继续使用**批量 `COPY`**
- 旧/新明细写入**完全独立**
- `room_status` 始终独立执行,不受明细写入开关影响
- 错误表 `heartbeat.heartbeat_events_errors` 只保留一份,**仅记录新表写入失败**
- 支持未来**关闭旧表,仅保留新表**
---
## 二、最终行为规则
### 1. 明细写入规则
- `legacyHeartbeatEnabled=true` 时,写旧表 `heartbeat.heartbeat_events`
- `g4HotHeartbeatEnabled=true` 时,写新表 `heartbeat.heartbeat_events_g4_hot`
- 两者可同时开启,也可单独开启,也可同时关闭
### 2. `room_status` 规则
- 始终执行现有 `upsertRoomStatus()`
- 目标表不变
- 写法不变
- 不依赖旧明细是否开启
- 不依赖新明细是否开启
- 即使旧/新明细都关闭,仍然写 `room_status`
### 3. 错误表规则
- 继续使用现有 `heartbeat.heartbeat_events_errors`
- 字段完全不变
- 只记录**新表写入失败**
- 旧表写入失败**不再写错误表**,只记录日志/统计
### 4. 消费暂停规则
- 不能因为“两路明细都关闭”而暂停消费
- 不能因为“新表表级错误”直接当成全局数据库离线
- 只有在**当前启用且关键的写入路径**发生连接级不可恢复故障时,才触发暂停消费
---
## 三、代码改造清单
### Task 1扩展配置项
**目标文件**
- `src/config/config.js`
- `src/config/config.example.js`
- `README.md` 或部署文档
**新增配置建议**
- `DB_LEGACY_HEARTBEAT_ENABLED`
- `DB_G4_HOT_HEARTBEAT_ENABLED`
- `DB_ROOM_STATUS_ENABLED`
- `DB_G4_HOT_TABLE`
- `DB_LEGACY_TABLE`
**建议默认值**
- `DB_LEGACY_HEARTBEAT_ENABLED=true`
- `DB_G4_HOT_HEARTBEAT_ENABLED=false`
- `DB_ROOM_STATUS_ENABLED=true`
- `DB_LEGACY_TABLE=heartbeat.heartbeat_events`
- `DB_G4_HOT_TABLE=heartbeat.heartbeat_events_g4_hot`
**实施要求**
- 保持与当前 `config.js` 风格一致
- 布尔值走现有 `parseBoolean`
- 表名配置单独放在 `db` 配置节点下
**完成标准**
- 应用启动时可从环境变量读取旧/新明细开关
- `room_status` 开关独立可控
- 新旧目标表名可配置
---
### Task 2抽象通用明细写入内核
**目标文件**
- `src/db/databaseManager.js`
**当前基础**
- 现有 `insertHeartbeatEvents()` 已实现:
- 分区预创建
- `COPY ... FROM STDIN`
- 缺分区补建重试
- fallback 逐条 `INSERT`
**要做的事**
把现有写旧表的强绑定逻辑抽象为通用方法,例如:
- `_insertHeartbeatEventsToTarget(events, targetConfig)`
- `_buildHeartbeatCopySql(targetTable, columns)`
- `_buildHeartbeatInsertSql(targetTable, columns)`
**目标参数至少包含**
- `tableName`
- `columns`
- `logPrefix`
- `enablePartitionEnsure`
- 缺分区识别规则
**实施要求**
- 不能复制两份几乎相同的 `COPY` 代码
- 保证旧表和新表复用同一套批量写入能力
- 保证新表也走 `COPY`
**完成标准**
- 写旧表和写新表都可通过同一个通用写入内核完成
- 旧逻辑行为不变
- 新表能力复用旧逻辑
---
### Task 3实现双明细独立编排
**目标文件**
- `src/db/databaseManager.js`
**新增编排方法建议**
- `writeHeartbeatDetails(events)`
-`insertHeartbeatEventsDual(events)`
**它需要负责**
- 判断旧表是否开启
- 判断新表是否开启
- 分别调用:
- `legacy writer`
- `g4Hot writer`
- 聚合结果并返回结构化结果
**返回结果建议**
至少包含:
- `legacy.enabled`
- `legacy.success`
- `legacy.insertedCount`
- `legacy.failedRecords`
- `legacy.error`
- `g4Hot.enabled`
- `g4Hot.success`
- `g4Hot.insertedCount`
- `g4Hot.failedRecords`
- `g4Hot.error`
**实施要求**
- 两路执行逻辑互不影响
- 一路失败不能吞掉另一路结果
- 两路都关闭时返回“跳过写入”的明确结果,不报错
**完成标准**
- 系统能正确识别四种模式:
- 仅旧
- 仅新
- 双写
- 双关
---
### Task 4调整 `HeartbeatProcessor.processBatch()` 的主流程
**目标文件**
- `src/processor/heartbeatProcessor.js`
**当前逻辑问题**
当前流程中:
- 先写 `insertHeartbeatEvents(batchData)`
- 只有成功后才 best-effort 写 `room_status`
这与当前最终要求不一致。
**改造目标**
把流程改成三段独立逻辑:
#### A. 明细写入
调用新的双写编排方法,获取旧/新明细写入结果
#### B. `room_status` 写入
无论旧/新明细写入开关状态如何,都执行:
- `upsertRoomStatus(batchData)`
注意:
- 只要 `DB_ROOM_STATUS_ENABLED=true`
- 就执行当前逻辑
- 不依赖明细成功与否
#### C. 错误表写入
仅把**新表失败记录**送入 `insertHeartbeatEventsErrors()`
**完成标准**
- `room_status` 从“依赖旧表成功”变成“独立执行”
- 错误表只接新表失败
- 旧表失败不进错误表
---
### Task 5重新定义错误表调用策略
**目标文件**
- `src/db/databaseManager.js`
- `src/processor/heartbeatProcessor.js`
**要求**
- 保留 `insertHeartbeatEventsErrors()` 表结构和 SQL
- 仅调整调用来源:
- 新表失败 → 写错误表
- 旧表失败 → 不写错误表
**实施要求**
建议在双写结果聚合后,只提取:
- `g4Hot.failedRecords`
转换成当前错误表 payload 后调用 `insertHeartbeatEventsErrors()`
**完成标准**
- 错误表字段、表结构、SQL 全不变
- 旧表失败不会污染错误表
- 新表失败可追踪
---
### Task 6调整连接状态与暂停消费策略
**目标文件**
- `src/processor/heartbeatProcessor.js`
- `src/db/databaseManager.js`
**当前问题**
目前 `_isConnectionError()``_scheduleDbCheck()` 基本围绕单一 DB 写入路径设计。
**改造要求**
把“是否需要暂停 Kafka 消费”改成基于**启用中的关键 sink**判断。
**建议规则**
- 仅旧表开启:旧表连接故障才可能触发暂停
- 仅新表开启:新表连接故障才可能触发暂停
- 双开:如果两路都因连接级错误不可写,可触发暂停
- 双关但 `room_status` 开启:只要 `room_status` 还能正常写,就不应因明细关闭而暂停
**重要区分**
以下不能都算成“数据库离线”:
- 连接失败
- 缺分区
- 表不存在
- 表字段不匹配
- 权限不足
其中真正应触发离线处理的优先是:
- `08006`
- `08001`
- `08003`
- `08004`
- `08007`
- `57P03`
- 以及明显的网络连接错误
**完成标准**
- 不会因为新表单独异常而误停整个消费
- 不会因为两路明细关闭而误停消费
---
### Task 7补充启动日志与配置摘要
**目标文件**
- `src/index.js`
**新增日志建议**
启动时输出:
- 旧明细写入是否开启
- 新明细写入是否开启
- `room_status` 是否开启
- 旧表目标
- 新表目标
**完成标准**
- 运行日志中能直接看出当前处于哪种模式
- 运维排障时不用翻配置文件
---
### Task 8补充统计项
**目标文件**
- `src/stats/statsManager.js`
- 如有 Redis 控制台输出,也同步补充
**新增统计建议**
- legacy detail success count
- legacy detail failed count
- g4Hot detail success count
- g4Hot detail failed count
- room_status success count
- room_status failed count
- g4Hot error-table inserted count
**完成标准**
- 双写观察期可以快速对比旧表/新表健康状态
- 能单独看出新表失败是否升高
---
## 四、测试清单
### Task 9补充单元测试
**目标文件**
- `test/smoke.test.js`
- 如有必要新增专门的 `databaseManager` 测试文件
**必须覆盖的场景**
1. 仅旧表开启
2. 仅新表开启
3. 旧新双开
4. 旧新双关,但 `room_status` 开启
5. 旧成功、新失败
6. 旧失败、新成功
7. 双失败
8. 新表失败时写错误表
9. 旧表失败时不写错误表
10. `room_status` 始终执行
11. `COPY` 失败时新表降级逐条 `INSERT`
12. 缺分区时自动补分区重试
**完成标准**
- `npm test` 可覆盖新旧双写与独立逻辑
- `room_status` 独立性有明确断言
---
### Task 10补充数据库 smoke 验证
**目标文件**
- `scripts/db/smokeTest.js`
- 或新增 `scripts/db/smokeG4Hot.js`
**验证项**
- 新表 `heartbeat.heartbeat_events_g4_hot` 能写入
- 新表分区能正确附着
- 新表唯一键/索引存在
- 新表可按主查询维度检索
- 旧表关闭、新表开启时仍正常
- 旧新都关但 `room_status` 开启时系统不报配置错误
**完成标准**
- 有一套上线前可重复执行的 smoke 脚本
- 可快速确认新表接入正确
---
## 五、上线实施顺序
### Step 1代码部署保持现状
配置:
- 旧表开启
- 新表关闭
- `room_status` 开启
目标:
- 确认改造后代码在“旧模式”下行为不变
### Step 2开启新表双写
配置:
- 旧表开启
- 新表开启
- `room_status` 开启
目标:
- 观察新表写入是否稳定
- 检查新表失败是否进入错误表
- 比对旧/新明细数据量
### Step 3观察稳定性
重点观察:
- 新表 `COPY` 成功率
- 新表 fallback 频率
- 错误表记录量
- `room_status` 是否持续正常
### Step 4关闭旧表
配置:
- 旧表关闭
- 新表开启
- `room_status` 开启
目标:
- 验证“仅保留新表”时系统稳定运行
### Step 5保留过渡观察期
目标:
- 持续观察新表、错误表、`room_status`
---
## 六、验收标准
满足以下全部条件即可验收:
1. 新表 `heartbeat.heartbeat_events_g4_hot` 接入成功
2. 新表仍使用批量 `COPY`
3. 旧/新明细可独立开关
4. `room_status` 始终独立执行
5. 两路明细都关闭时,系统仍允许继续写 `room_status`
6. 错误表仅记录新表失败
7. 旧表失败不会写错误表
8. 不修改 Kafka 数据来源
9. 不修改现有数据转换来源
10. 可支持后续关闭旧表,仅保留新表
---
## 七、建议的实施顺序编号版
1. 扩展 `config.js` 配置项
2. 重构 `databaseManager.js`,抽通用 `COPY` writer
3.`databaseManager.js` 增加旧/新明细双写编排
4. 调整 `heartbeatProcessor.js`,让 `room_status` 独立执行
5. 调整错误表调用,仅接新表失败
6. 重构连接异常与暂停消费判定
7. 增加启动配置摘要日志
8. 增加 stats 指标
9. 补测试
10. 补 smoke 验证脚本
11. 按“旧开新关 → 双开 → 关旧留新”上线