航行日志长期增量分析方案(讨论稿)
| 项 | 内容 |
|---|---|
| 日期 | 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 跳过未变文件。这条骨架留下。
长期会坏掉的是另一件事:分析口径在变,结果却写在同一行里被盖掉。
已经发生过的例子:
batteryCurrent放电为负。代数max得到 0,放电峰值是最负的那个数。两种「最大电流」不能共用一列。0x400000、温度阈值、has_fault的收口都改过。一升analyzer_version,历史行要么被整行重算,要么永远停在旧口径上,后台分不清自己看到的是哪一版。- 风浪、落水字段全是 0。把「列存在」当成「有测量」,会把空值分析成年年不变的结论。
所以长期方案的第一性原理是三条时间分开,两种增量分开。
| 时间 | 是什么 | 现有字段 |
|---|---|---|
| 事件时间 | 这次航行发生在何时 | 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)
价值按这个顺序长出来,不能跳:
- 航次事实:这次飞了没有、电流、温度、故障、航时。问的是这一次。
- 设备生涯:同一块板、同一台遥控、同一套电驱,固件版本前后有没有变坏。问的是这一台。
- 人的习惯:同一遥控器或同一账号,油门、起飞、越界是不是在重复。问的是这一个人。账号空着时,键用
remote_sn,不要把匿名航次算成「这个人退步了」。 - 口径资产:某个数为什么是这个值,用的哪段代码、哪批样本。问的是「这数还能不能引用」。
第 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,也不占用「最大」这一个中文词。
规则:
- 改公式 = 新版本或新指标 id。已发布版本的数值不原地 UPDATE 成新含义。
- 回填是指名的作业:「把指标 X 的版本 2 补到
trajectory_id小于某水位的行」。补完才允许金层把发布指针从 v1 拨到 v2。 - 金层未切指针之前,后台继续显示旧数。避免「跑了一夜,列表悄悄变了口径」。
- 和故障冲突时,故障族以 TLA 已发布版本为准。别的族只引用,不自造
has_fault。 - 大模型可以读已发布的指标和原始点做解释。分数和阈值必须来自登记过的公式,能用同一份 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 分钟节奏。每轮只做四步。
- 拉变化。
add_time大于水位,或deleted从 0 变 1,或分析行缺失。水位按入库时间,不要按航行时间,否则补传的旧日志会丢。 - 合并。 主键
trajectory_id。同一object_key再出现是同一条,UPSERT,不插第二行。 - 迟到。 水位回看一个小窗口(建议 1 天的
add_time),抓住晚到的更新。窗口只重放仍对得上指纹变化或缺失分析的行,不是每天全表扫。 - 对账。 每天一次:未删除航次数、已有 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. 若讨论通过,落地顺序
只排依赖,不排月份。
- 指标目录。 先登记已经在生产列表上的数:
max_current、两路温度、has_fault、has_high_current。写明输入字段、空值和版本。max_battery_current在符号约定确认前保持 draft。 - 金层视图。 后台列表改为读视图,视图里写死当前发布版本。分析表可以有多版,页面只看到发布版。
- 回填作业模板。 把
patch_max_battery_current.py收成通用形式:指定指标、指定版本、只 UPDATE 该列、不升全局analyzer_version。 - 每日对账。 航次数、OSS 覆盖、各指标发布版覆盖率。缺数可见。
- 新问题族。 滑后教练、翼型负载继续走自己的表,主键仍是
trajectory_id,引用 TLA 发布版,不复制故障引擎。
第 1 步不改线上行为。它的产出是一张人能读的表:「这个列名,今天到底是什么意思。」
9. 讨论时要定的四件事
- 电池电流发布值是否按字典改成「
|batteryCurrent|的最大,缺键为 NULL」?现在的代数最大会在放电为负时得到 0,和功率公式相反。本方案建议改,未改前不发布。 - 旧口径要不要可查。 故障规则再变时,去年的
has_fault是留版本,还是允许整行重算后只留新值?本方案建议留版本。 - 金层放哪。 继续在业务 MySQL 上建视图,还是分析表单独一个库、只用
trajectory_id去 JOIN?JUA 方案写的是旁路。本方案同意旁路,但列表接口必须有一个明确的发布视图,不能每个后台自己拼列。 - 匿名航次的人维度是否正式定为
remote_sn?不定的话,任何「用户成长」类分析都会把试板和真用户混在一起。
这四件定了,再写表结构和任务。没定之前不加新的分析列。