253 lines
7.6 KiB
Markdown
253 lines
7.6 KiB
Markdown
# 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 选型结论
|
||
|
||
采用 **方案 2:DB 队列双阶段**。
|
||
|
||
理由:
|
||
|
||
- 避免 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:连接状态、入站速率、入库失败
|
||
- parser:pending 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`,内容无附加字段)
|