# 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//YYYY-MM-DD/` - 文件内容:仅原始 payload 字节流拼接(无额外元信息) - 文件名示例:`device___.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`,内容无附加字段)