← 返回目录

航行日志长期增量分析方案(讨论稿)

项 内容
日期 2026-10-13
状态 讨论稿,未开工
读者 先对齐口径,再决定要不要拆服务
对照 已有 TLA、jetwave-usage-analytics、jetwave-traj-store、analysis/LOG_ANALYTICS_PLATFORM_PLAN.md
规模事实 未删除航次约 2.1 万;本地 OSS 缓存 8,957 条 / 11.84 GB;业务库持续增量

这份稿只回答一件事:正式库里的用户、设备、航行日志会一直增加,而且「同一个字段代表什么」也会变。长期方案怎么让新数据进得来,旧结论还对得上。

不在本稿里做的事:再设计一套故障码、再把轨迹点灌进 MySQL、上数据湖。


0. 先拍板的判断

JetWave 现在缺的不是「增量 ETL」。TLA 已经用 add_time 水位、trajectory_id 一对一、content_fingerprint 跳过未变文件。这条骨架留下。

长期会坏掉的是另一件事:分析口径在变,结果却写在同一行里被盖掉。

已经发生过的例子:

所以长期方案的第一性原理是三条时间分开,两种增量分开。

时间 是什么 现有字段
事件时间 这次航行发生在何时 run_time / JSON startTime
入库时间 业务库何时看到这条日志 add_time
口径时间 这列数是用哪一版规则算的 今天只有一个 analyzer_version,不够
增量 触发 做法
数据增量 新航次、补传、软删 水位 + 主键合并,只碰变化的 trajectory_id
口径增量 公式、阈值、字段含义变了 新口径写新版本,旧版本的数留着,直到明确废弃

业界(dbt 增量 MERGE、Bronze/Silver/Gold、指标注册表)做的也是这两刀。它们的工具是为千万行以上准备的。我们 2 万航次、单航次一个 OSS JSON,用同一套纪律,不换一套湖仓。


1. 数据从哪来,价值长在哪

一条航行在库里不是一张宽表,是三层东西绑在同一个 trajectory_id 上。

用户 play_user_id          会空、会改昵称、近一半航次匿名
设备 remote_id / board_id  固件会升级,SN 在 JSON 里
     remote_sn / board_sn / battery_sn / electrical_sn
航次 history.id            业务只插一行 + 一个 OSS 文件
     object_key → 不可变 JSON(trajectoryList)

价值按这个顺序长出来,不能跳:

  1. 航次事实:这次飞了没有、电流、温度、故障、航时。问的是这一次。
  2. 设备生涯:同一块板、同一台遥控、同一套电驱,固件版本前后有没有变坏。问的是这一台。
  3. 人的习惯:同一遥控器或同一账号,油门、起飞、越界是不是在重复。问的是这一个人。账号空着时,键用 remote_sn,不要把匿名航次算成「这个人退步了」。
  4. 口径资产:某个数为什么是这个值,用的哪段代码、哪批样本。问的是「这数还能不能引用」。

第 4 层是长期方案和「再写一个分析脚本」的分界。没有它,明年换一个问题,今年的表就是一份对不上的历史。

1.1 字段字典已经钉死的口径

来源:surface-link-ui 的 航行日志字段字典.md(对照仿真页真实 OSS JSON,2026-09 新固件样例才带电池电流)。分析侧以后以这份字典为准,缺键就当没有,不补 0。

不要混 字典 对分析的含义
两套电量 batteryElectricity = 遥控电量 %;boardBattery = 板体电量 % 不能把遥控电量写成电池 SOC
两套电流 batteryCurrent = 电池侧,2026-09 新固件才稳定有;electricCurrent = 电调,可负 旧日志没有 batteryCurrent 时指标必须是 NULL,不能写成 0
功率 batteryVoltage × \|batteryCurrent\| / 1000 kW 没有电池电流就没有功率,禁止用电调电流冒充
油门 日志 throttleLeverQuantity 是 0–1000 满油门是 1000,不是 100。表盘才除以 10
里程 头 totalDistance、点上 cumulativeSailingDistance 都是米 旧详情页写成 km 的是展示错误
速度 hourSpeed 是 km/h 不换算成 m/s
温度告警 电池 45°C、电调 electricTemperature 65°C、电机 70°C 与 TLA 现行阈值一致。electricTemperature 是电调,不是 ECU
电芯电压 cellVoltageMax / cellVoltageMin 新固件有键,样例常为 0.0 0 当无效,不当成真实电压。和风浪落水全 0 是同一类
落水 / 最长飞行 头和点上现网常为 0 不进发布指标,直到设备真报
运行状态 status 0 待机 / 1 运行 / 2 故障,但不少场全程 0 故障以三路 event code 为准,不以 status 为准

仿真页和数据面板还不一致:仿真功率用 |batteryCurrent|,油门除以 10;panelUtils.js 的功率用 electricCurrent,油门不除 10。分析只认字典,不认某一页的画法。

max_battery_current 今天的实现是 batteryCurrent 的代数最大。字典要求功率取绝对值,且缺键不能当 0。因此这一列在符号约定改成「放电峰值(绝对值最大,缺键为 NULL)」之前,保持 draft,不能当发布指标。


2. 和已经在跑的系统怎么叠

不新起第四条采集链路。业务仍然只写 surf_motion_trajectory_history 和 OSS。

已有 长期角色 动不动
history + OSS 青铜层。原始航次,分析任务只读 不动写入
TLA surf_motion_trajectory_analysis 故障 / 温度 / 电流这一族的银层 继续增量。新口径不要靠升全局版本去覆盖旧列
JUA(用量 / 翼型 / 负载) 另一族银层,主键仍是 trajectory_id 只读 TLA 的故障结论,不重写
traj-store 点级地图和回放 不把点再灌进业务 MySQL
本地 oss_cache 8,957 已经落地的青铜样本 正式增量仍以线上 OSS 为准,本地缓存是复算样本

后台、App、后续的滑后结论,只读已发布口径的视图。不直接扫 trajectoryList。


3. 长期形态(三层,工具先不换)

用分层,是为了规定「谁可以改、谁只许追加」。名字对齐业界的 Bronze / Silver / Gold,落点仍是现有 MySQL + OSS。

青铜  业务库一行 + OSS 一份 JSON
        只追加。分析服务不改源表,不改 OSS。
        软删只认 history.deleted,分析侧标记 withdrawn,不物理删结论。

银层  按「问题族」分表,每族一个 logic_version
        族 A  故障与安全     TLA 已有
        族 B  用量与工况     JUA 已规划
        族 C  以后的新问题   新表,禁止往 A 的行上无休止 ALTER
        每行:trajectory_id + logic_version + 算出来的列 + computed_at

金层  给人看的视图
        每个指标声明「当前发布版本」。
        列表页只 JOIN 发布版本。
        研究对比可以同时看 v1 和 v2。

点级数据留在 OSS。银层只存航次结论和少量证据(例如最大电流出现的时间戳、命中的故障码计数)。要下钻再按 object_key 读那一个文件。2 万航次这样做是够的;把每个采样点变成数仓事实表,存储和回填都会先于问题本身爆掉。


4. 口径怎么演进(这是长期方案的主体)

一个可引用的指标是一条登记,不是一列注释。

登记项 例子
指标 id esc_current_max
人话 电调最高电流
单位 A
输入 轨迹点 electricCurrent,忽略空值,取代数最大,保留两位
版本 1
生效 从哪天的计算结果开始算这条
空值含义 源字段缺失 → NULL,禁止写成 0
发布状态 draft / published / retired

battery_current_peak_discharge 是另一个指标:输入 batteryCurrent,放电为负时取最小值(最负),展示时可以取绝对值并注明符号约定。它不覆盖 esc_current_max,也不占用「最大」这一个中文词。

规则:

  1. 改公式 = 新版本或新指标 id。已发布版本的数值不原地 UPDATE 成新含义。
  2. 回填是指名的作业:「把指标 X 的版本 2 补到 trajectory_id 小于某水位的行」。补完才允许金层把发布指针从 v1 拨到 v2。
  3. 金层未切指针之前,后台继续显示旧数。避免「跑了一夜,列表悄悄变了口径」。
  4. 和故障冲突时,故障族以 TLA 已发布版本为准。别的族只引用,不自造 has_fault。
  5. 大模型可以读已发布的指标和原始点做解释。分数和阈值必须来自登记过的公式,能用同一份 JSON 重算出来。

这和 dbt 里「模型版本 + 增量回填」是同一纪律。我们用一张 metric_catalog 加每族表上的 logic_version 就够,不必先上 dbt。


5. 用户和设备:缓慢变化,不要焊死在航次行上

航次行上的 remote_sn、固件版本,是这次航行当时的快照,写完不再改。这是对的。

人的昵称、设备归属、经销关系会变。这些不要回写进历史航次行,否则去年的列表会变成今年的组织架构。

对象 键 变了怎么办
航次 trajectory_id 不改。软删只打 withdrawn
当时的设备快照 留在航次分析行 只代表该次 JSON
设备主数据 remote_sn 或 board_id 单独的当前表,可保留变更时间
人 有账号用 play_user_id,没有用 remote_sn 聚合时两套键都显式写出,不合并成一个「用户」

设备生涯类问题(某固件之后堵转是否变多)用航次上的版本快照做分组,不用「这台设备现在的版本」去改写旧航次。


6. 增量作业怎么跑

沿用 TLA 的 15 分钟节奏。每轮只做四步。

  1. 拉变化。 add_time 大于水位,或 deleted 从 0 变 1,或分析行缺失。水位按入库时间,不要按航行时间,否则补传的旧日志会丢。
  2. 合并。 主键 trajectory_id。同一 object_key 再出现是同一条,UPSERT,不插第二行。
  3. 迟到。 水位回看一个小窗口(建议 1 天的 add_time),抓住晚到的更新。窗口只重放仍对得上指纹变化或缺失分析的行,不是每天全表扫。
  4. 对账。 每天一次:未删除航次数、已有 OSS 键、各银层 published 版本覆盖了多少 trajectory_id。对不上就报警,不自动全量重算。

删除:源表 deleted=1 时,银层保留计算结果,金层默认过滤掉。品质追溯还要查得到。

指纹继续用于「文件没变就不要重下 OSS」。指纹未变 不允许 阻止口径回填。回填看的是 logic_version 缺不缺,不是文件变没变。这是这次 max_battery_current 用单独 patch、而不升 analyzer_version 的原因,以后写成正式规则。


7. 参考了什么,为什么不照搬

来源 取来的纪律 不搬的部分
dbt 增量 MERGE(增量模式) 业务主键合并;水位之外留回看;晚到数据重放一小段 2 万行不值得上 dbt 平台。微批(microbatch)是给十亿行时序的
Bronze / Silver / Gold 原始不可变,按主题加工,对外只暴露发布层 不建 S3 Parquet + Databricks。OSS JSON 就是青铜
指标注册 / 语义层 指标有版本、有单位、有空值含义 不买一套指标中台。一张目录表 + 代码里的纯函数
CDC(库的变更流) 软删和更新要能被分析侧看见 航次基本上是插入。MySQL 水位够用,不必上 Debezium
特征平台、实时数仓 训练和在线打分要同一份定义 现在没有在线推理。工况标签仍是「离线标定、在线逐条套用」,见 JUA 方案

顶级公司在这个量级(硬件遥测、几万到几十万会话)也是先把原始日志留住、把指标版本化,而不是先上湖。湖是在点级查询和多人同时改 SQL 变成瓶颈之后才加的。


8. 若讨论通过,落地顺序

只排依赖,不排月份。

  1. 指标目录。 先登记已经在生产列表上的数:max_current、两路温度、has_fault、has_high_current。写明输入字段、空值和版本。max_battery_current 在符号约定确认前保持 draft。
  2. 金层视图。 后台列表改为读视图,视图里写死当前发布版本。分析表可以有多版,页面只看到发布版。
  3. 回填作业模板。 把 patch_max_battery_current.py 收成通用形式:指定指标、指定版本、只 UPDATE 该列、不升全局 analyzer_version。
  4. 每日对账。 航次数、OSS 覆盖、各指标发布版覆盖率。缺数可见。
  5. 新问题族。 滑后教练、翼型负载继续走自己的表,主键仍是 trajectory_id,引用 TLA 发布版,不复制故障引擎。

第 1 步不改线上行为。它的产出是一张人能读的表:「这个列名,今天到底是什么意思。」


9. 讨论时要定的四件事

  1. 电池电流发布值是否按字典改成「|batteryCurrent| 的最大,缺键为 NULL」?现在的代数最大会在放电为负时得到 0,和功率公式相反。本方案建议改,未改前不发布。
  2. 旧口径要不要可查。 故障规则再变时,去年的 has_fault 是留版本,还是允许整行重算后只留新值?本方案建议留版本。
  3. 金层放哪。 继续在业务 MySQL 上建视图,还是分析表单独一个库、只用 trajectory_id 去 JOIN?JUA 方案写的是旁路。本方案同意旁路,但列表接口必须有一个明确的发布视图,不能每个后台自己拼列。
  4. 匿名航次的人维度是否正式定为 remote_sn?不定的话,任何「用户成长」类分析都会把试板和真用户混在一起。

这四件定了,再写表结构和任务。没定之前不加新的分析列。