Files
full-stack-fastapi/docs/superpowers/specs/2026-03-17-mqtt-raw-ingestion-design.md
2026-03-17 11:49:31 +08:00

253 lines
7.6 KiB
Markdown
Raw 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.
# MQTT 原始数据接收与保存设计(双阶段)
## 1. 背景与目标
在现有 `full-stack-fastapi-template` 工程基础上,新增 MQTT 采集能力,用于接收终端上报的 GNSS 数据并可靠保存,支持后续:
- 前端秒级实时展示(设备看板 + 逐条观测流)
- 按天/时间段下载原始数据供外部软件分析
本设计阶段仅实现“接收 + 保存 + 可解析落库链路”,不实现前端页面。
## 2. 已确认约束
- 部署环境:腾讯云,同机已有 EMQX
- Broker 地址:`101.35.119.226:1883`
- 鉴权方式:用户名/密码
- Topic`terminal/{device_id}/raw`
- `device_id`:直接从 topic 中提取
- 上报形态:每 1~10 秒一条消息,每条消息为“完整帧/完整语句集合”
- 负载内容:可能为 NMEA only、RTCM3.2 only 或二者混合
- NMEA 类型GGA / RMC / GSV / GSA / GST
- RTCM本期仅做“消息类型级别摘要”
- 存储策略90 天热数据 + 本地冷归档
- 下载要求:下载内容必须是原始 MQTT payload 字节流,不附加元信息
- 下载文件后缀:`.txt`(内容可包含二进制字节)
## 3. 架构方案与结论
### 3.1 备选方案
1. FastAPI 内嵌 MQTT 客户端
2. DB 队列双阶段(独立 ingestor + parser worker
3. 外部消息队列三阶段
### 3.2 选型结论
采用 **方案 2DB 队列双阶段**
理由:
- 避免 FastAPI 多 worker 下重复订阅/重复入库风险
- 接收链路与解析链路解耦,稳定性更高
- 原始数据先落库,可重试、可回放、可追溯
- 为后续实时展示和下载提供稳定基础
## 4. 组件职责
### 4.1 mqtt-ingestor采集层
- 常驻独立进程,连接 EMQX 并订阅 `terminal/+/raw`
- 回调中仅执行轻量逻辑:
- 提取 `device_id`
- 记录 `topic/qos/retain/received_at`
- 原样保存 `payload_bytes`
- 计算 `payload_sha256``payload_size`
- 插入 `terminal_message_raw``parse_status='pending'`
- 不执行 NMEA/RTCM 深解析
### 4.2 parser-worker解析层
- 轮询 `terminal_message_raw` 中待解析记录
- 解析 NMEA + RTCM3.2 摘要
- 写入事件表 `terminal_observation_event`
- Upsert 最新状态表 `terminal_device_state`
- 失败可重试,错误可观测,不丢原始数据
### 4.3 backend API服务层
- 本期不新增前端页面
- 后续基于 `event + state + archive manifest` 增加秒级展示与下载接口
## 5. 数据模型设计
### 5.1 terminal_message_raw热数据原始表
用途:保存原始消息,作为解析队列与下载来源。
建议字段:
- `id` (uuid, pk)
- `device_id` (text, index)
- `topic` (text)
- `received_at` (timestamptz, index)
- `payload_bytes` (bytea)
- `payload_size` (int)
- `payload_sha256` (char(64), index)
- `qos` (smallint)
- `retain` (bool)
- `parse_status` (`pending|processing|succeeded|failed`, index)
- `parse_attempts` (int, default 0)
- `next_retry_at` (timestamptz, nullable, index)
- `parse_error` (text, nullable)
- `created_at` (timestamptz)
### 5.2 terminal_observation_event解析事件流
用途:支撑逐条观测流展示与历史分析。
建议字段:
- `id` (uuid, pk)
- `raw_id` (uuid, fk -> terminal_message_raw.id, index)
- `device_id` (text, index)
- `event_time` (timestamptz, index)
- `event_type` (text)
示例:`nmea_gga` / `nmea_rmc` / `nmea_gsv` / `nmea_gsa` / `nmea_gst` / `rtcm_summary`
- `payload_json` (jsonb)
- `created_at` (timestamptz)
### 5.3 terminal_device_state设备最新状态
用途:支撑前端设备看板秒级刷新。
建议字段:
- `device_id` (text, pk)
- `last_event_time` (timestamptz)
- `fix_status` (text)
- `lat` / `lon` (double precision)
- `alt_m` (double precision)
- `satellites_used` (int)
- `satellite_signal_summary` (jsonb)
- `rtcm_type_summary` (jsonb)
- `updated_at` (timestamptz)
### 5.4 terminal_raw_archive_manifest冷归档索引
用途:记录本地归档文件与时间范围映射。
建议字段:
- `id` (uuid, pk)
- `device_id` (text, index)
- `range_start` / `range_end` (timestamptz, index)
- `file_path` (text, unique)
- `file_size` (bigint)
- `sha256` (char(64))
- `record_count` (bigint)
- `created_at` (timestamptz)
## 6. 双阶段流水线细节
### 6.1 入站ingestor
- MQTT 建议参数:`clean_session=False`、固定 `client_id`、QoS=1
- 批量入库建议:每 100 条或 200ms flush 一次事务
- 目标:尽快持久化原始字节,避免解析阻塞接收
### 6.2 解析parser-worker
批处理取数:
- 条件:`parse_status='pending'``next_retry_at` 到期
- 顺序:`ORDER BY received_at`
- 锁:`FOR UPDATE SKIP LOCKED`
- 批量:`LIMIT 100~500`
处理规则:
- 成功:`parse_status='succeeded'`
- 失败:`parse_status='failed'` + `parse_attempts+1` + `parse_error`
- 重试:退避后设置 `next_retry_at`
幂等建议:
- 事件表增加防重策略(如 `raw_id + event_type + event_time` 约束或哈希去重)
- `terminal_device_state` 用 upsert仅更新更晚 `event_time`
## 7. 解析策略(本期)
### 7.1 NMEA
- 解析 GGA/RMC/GSV/GSA/GST
- 每条语句产出一条 `terminal_observation_event`
### 7.2 RTCM3.2
- 仅提取消息类型摘要(如 1005/1077/1087
- 每条 raw payload 产出一条 `rtcm_summary` 事件,包含类型计数与最近出现时间
### 7.3 混合负载
- 单 payload 中可同时包含 NMEA 与 RTCM 片段
- 解析器按字节扫描拆分,分别产出事件
- 部分片段解析失败不影响其余片段;原始 payload 永远保留
## 8. 热数据与本地冷归档
### 8.1 热数据
- `terminal_message_raw` 保留 90 天
### 8.2 本地冷归档
- 定时任务离峰执行
- 归档粒度:`device_id + 日`(必要时可按小时分片)
- 路径示例:`/data/archive/gnss/<device_id>/YYYY-MM-DD/`
- 文件内容:仅原始 payload 字节流拼接(无额外元信息)
- 文件名示例:`device_<id>_<start>_<end>.txt`
### 8.3 校验与清理
- 写入 `manifest`时间范围、条数、sha256、文件路径
- 校验通过后,清理超过 90 天热数据
## 9. 下载行为约束(后续接口实现遵循)
- 下载内容必须与原始 MQTT payload 字节一致
- 近 90 天:从热表按时间顺序拼接输出
- 超过 90 天:从本地归档读取输出
- 跨热/冷边界:按时间顺序拼接两段数据
## 10. 运维与可观测性
必须监控:
- ingestor连接状态、入站速率、入库失败
- parserpending backlog、解析成功率、平均延迟、失败重试
- archive每日归档成功/失败、磁盘余量
建议告警:
- `pending` 队列持续增长
- parser 失败率超阈值
- 归档目录磁盘不足
## 11. 配置项建议(.env
- `MQTT_HOST=101.35.119.226`
- `MQTT_PORT=1883`
- `MQTT_USERNAME=...`
- `MQTT_PASSWORD=...`
- `MQTT_TOPIC_PATTERN=terminal/+/raw`
- `MQTT_CLIENT_ID=spatialhub-mqtt-ingestor`
- `RAW_HOT_RETENTION_DAYS=90`
- `RAW_ARCHIVE_BASE_DIR=/data/archive/gnss`
- `PARSER_BATCH_SIZE=200`
- `PARSER_POLL_INTERVAL_MS=300`
## 12. 风险与后续演进
- 使用 `.txt` 扩展名存放二进制流可能被文本工具误打开;但不影响下游软件按字节读取
- 当设备量增长时,可水平扩展 parser-worker依赖 `SKIP LOCKED`
- 后续实时能力:新增 SSE/WebSocket 推送层,数据源直接复用 `event/state`
## 13. 验收标准(本期)
- 能稳定接收 `terminal/{device_id}/raw` 消息并原样入库
- 混合 payload 可提取 NMEA 事件和 RTCM 类型摘要
- 设备最新状态可持续更新
- 支持 90 天热数据保留与本地冷归档落盘
- 能按时间段导出原始数据(文件后缀 `.txt`,内容无附加字段)