feat: build vehicle data platform and production pipeline

This commit is contained in:
lingniu
2026-07-14 12:35:33 +08:00
parent b452be3b94
commit bb59303a4b
270 changed files with 88016 additions and 1975 deletions

View File

@@ -1,6 +1,6 @@
# 100K 车辆接入容量基线
更新时间2026-07-03
更新时间2026-07-13
## 目标
@@ -40,6 +40,7 @@
- 已新增可重复的 TCP 连接压测工具,后续需要用它跑 10K/50K/100K 阶段测试并记录结果。
- 仍缺读超时按协议维度计数Gateway frame duration 已提供 histogram可用于 parse + enqueue + response 的 p95/p99 估算。
- TDengine history writer 和 NATS fast writer 已使用 batch 写入;后续需要用带帧率压测验证 batch size、flush latency 和存储端承载能力。
- History、realtime、stat、identity writer 均支持单进程多个同 group consumer默认分别为 `HISTORY_WORKERS=3``REALTIME_WORKERS=3``STATS_WORKERS=3``IDENTITY_WRITER_WORKERS=3`。四者按 Kafka 分区并行且保持单车分区顺序;后续压测需联合观察 TDengine/MySQL 写延迟、连接数和各 consumer lag再决定是否增加 worker。
- Kafka topic 当前 12 分区100K 目标下需要结合实际 FPS 再评估分区数。
## ECS OS 参数建议
@@ -73,6 +74,8 @@ systemd gateway 已配置 `LimitNOFILE=1048576`,需要持续保持。
| History writer | `vehicle_history_batch_pending_messages` | 稳态接近 0burst 后下降 |
| History writer | `vehicle_history_batch_pending_rows` | 稳态接近 0burst 后下降 |
| History writer | `vehicle_history_batch_flush_duration_ms{status}` | flush 延迟不持续上升 |
| History writer | `vehicle_history_config{setting="workers"}` | 不低于 `3` |
| History writer | `vehicle_history_worker_active` | 每个配置 worker 均为 `1` |
| NATS bridge | `vehicle_bridge_nats_consumer_ack_pending` | 稳态为 0 |
| NATS bridge | `vehicle_bridge_nats_consumer_pending` | burst 后下降 |
| NATS bridge | `vehicle_bridge_batch_pending_messages` | 稳态接近 0burst 后下降 |
@@ -86,7 +89,12 @@ systemd gateway 已配置 `LimitNOFILE=1048576`,需要持续保持。
| Realtime Redis | `vehicle_realtime_store_update_duration_ms_histogram_bucket{store="redis"}` | p99 毫秒级且不持续上升 |
| Realtime MySQL | `vehicle_realtime_store_update_duration_ms_histogram_bucket{store="mysql"}` | p99 不持续上升 |
| Realtime MySQL | async queue depth | 不持续增长 |
| Realtime MySQL | `vehicle_realtime_config{setting="workers"}` | 不低于 `3` |
| Realtime MySQL | `vehicle_realtime_worker_active` | 每个配置 worker 均为 `1` |
| Stat writer | `vehicle_stat_write_duration_ms_histogram_bucket` | p99 不持续上升 |
| Stat writer | `vehicle_stat_config{setting="workers"}` | 不低于 `3` |
| Stat writer | `vehicle_stat_worker_active` | 每个配置 worker 均为 `1` |
| Identity writer | `vehicle_identity_writer_worker_active` | 每个配置 worker 均为 `1` |
## Retention Guardrails
@@ -96,7 +104,8 @@ systemd gateway 已配置 `LimitNOFILE=1048576`,需要持续保持。
| --- | --- |
| NATS JetStream `VEHICLE_INGEST` | `NATS_STREAM_MAX_BYTES=21474836480``NATS_STREAM_MAX_AGE_HOURS=24``NATS_STREAM_ENSURE_TIMEOUT_SECONDS=60` |
| Kafka `vehicle.raw.go.*` / `vehicle.fields.go.*` | `retention.ms=21600000``segment.ms=600000``segment.bytes=268435456` |
| Fast writer | `FAST_WRITER_OPERATION_TIMEOUT_MS=1000`,避免 TDengine 批量写尾延迟导致整批重投 |
| Gateway NATS publisher | `NATS_ASYNC_RAW_WORKERS=128`,吸收平台按秒集中上报形成的瞬时突发;以 `vehicle_async_sink_queue_wait_recent_p99_ms` 校验,不按稳态 queue depth 猜测 |
| Fast writer | `FAST_WRITER_WORKERS=16``FAST_WRITER_BATCH_SIZE=100``FAST_WRITER_FETCH_WAIT_MS=5``FAST_WRITER_OPERATION_TIMEOUT_MS=1000`;继续使用手工 ACK避免以可靠性换延迟 |
| Root disk | 使用率低于 `80%`,超过 `85%` 进入容量告警 |
如果 NATS `consumer_pending` 下降但仍高,说明系统在追历史积压;如果 `ack_pending=0` 且 Kafka lag 为 `0`,不要重启服务,重点观察 NATS data size 和 pending 下降斜率。
@@ -122,6 +131,8 @@ systemd gateway 已配置 `LimitNOFILE=1048576`,需要持续保持。
- JT808 会变更包头手机号、流水号和 `0x0200` 位置时间,并重新计算转义和校验码。
- GB32960 会变更 VIN 和实时数据时间,并重新计算 BCC。
- JT808 发送模式默认持续读取服务端通用应答;禁止关闭 `-drain-responses` 后用应答写阻塞产生的延迟评价服务端性能。
- JT808 使用 `-cleanup-registration` 时会在结束后只清理指定模拟手机号区间且来源为 loopback 的注册记录RAW 仍保留作为压测证据。每轮发送压测必须使用未在 identity-writer 10 分钟 touch 节流窗口内使用过的新号段,否则注册不会重复写入,`rows_deleted` 可以为 `0`
- 低 FPS 全链路压测必须使用该模式,避免重复静态帧导致 event id 冲突。
- 对生产端口执行 `-send=true` 会写入合成 raw 数据,只能在明确隔离标识和压测窗口后执行。
@@ -175,8 +186,42 @@ systemd gateway 已配置 `LimitNOFILE=1048576`,需要持续保持。
- `2026-07-03 18:15:36 CST` 5W 压测后 JT808 active 回落到约 `238``ss``239`
- `2026-07-03 18:16:49 CST` NATS ack pending 回到 `0`history/stat/realtime Kafka lag 总和均为 `0`
- `2026-07-03 18:23:02 CST` 10W 总连接压测后 JT808 active 回落到 `236`GB32960 active 回落到 `2`NATS ack pending 为 `0`history/stat/realtime Kafka lag 总和均为 `0`
- `20211/20212/20213/20214/20200` readyz 均 OK。
- `20211/20212/20213/20214/20215/20216/20200` readyz 均 OK。
- 压测窗口未出现 gateway `error|failed|panic|fatal|rejected` 日志。
- `vehicle_gateway_connection_rejections_total` 未输出,表示本轮没有连接拒绝计数。
结论:单 ECS 已通过本机 10W 总连接 hold-only 基线测试。该结果证明当前内核参数、gateway 连接上限和 systemd 文件句柄设置可以承载 10W 空闲长连接;下一阶段必须进入带真实帧率的低 FPS 压测验证解析、NATS、Kafka、Redis、TDengine、MySQL 全链路吞吐。
## 2026-07-13 1000 FPS 全链路压测记录
压测参数JT808 `0x0200`1000 个连接,每连接 1 FPS持续 60 秒,手机号区间 `139000000000-139000000999`;开启服务端应答读取和注册自动清理。每轮均打开 1000 个连接、失败 0发送 59,500 帧、写错误 0读取应答约 1.19 MB、读错误 0结束后清理 1000 条模拟注册。
未读取 JT808 应答的早期结果不作为延迟基线:客户端接收缓冲区会填满并反向阻塞 Gateway 写应答,测到的是压测器缺陷而非服务端真实链路。
| 版本 | 观测点 | Gateway 应答 p99 | Redis p99 | Bridge RAW p99 | TDengine history p99 | MySQL stat p99 |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| topic 串行写 Kafka | 压测中段 | 约 32ms | 约 212ms | 约 304ms | 约 466ms | 约 332ms |
| topic 串行写 Kafka | 压测结束 | 约 10ms | 约 162ms | 约 326ms | 约 427ms | 约 313ms |
| topic 并行写 Kafkaconcurrency=6 | 压测中段 | 约 24ms | 约 207ms | 约 311ms | 约 409ms | 约 247ms |
| topic 并行写 Kafkaconcurrency=6 | 压测结束 | 约 29ms | 约 184ms | 约 288ms | 约 368ms | 约 316ms |
并行版本在同一 NATS 批次内按 Kafka topic 分组并发写RAW 与派生 fields 全部成功后才 ACK 原 NATS 消息;任一 topic 失败仍保留对应源消息待重放。低负载 Bridge p99 从约 110ms 降到约 65ms1000 FPS 下 history p99 下降约 14%Bridge RAW p99 下降约 12%。压测中段 NATS pending 短时峰值 71Gateway async queue 短时 285结束时均归零Kafka lag 和各 writer retry 均为 0。主机为 4 核,瞬时 load average 约 9.68,但采样时 CPU 仍约 52% idle、无 D 状态进程、可用内存约 6.4 GiB。
增加队列等待指标后,使用五个全新 JT808 号段进行了 worker A/B每轮仍是 59,500 帧、发送/读取错误为 0、清理注册 1000 条。以下均为压测中段最近 512 样本窗口,秒级集中发送会比均匀流量更严格:
| Gateway RAW worker | Fast writer worker / fetch | Gateway queue wait p99 | Gateway 应答 p99 | Redis p99 | 结论 |
| ---: | --- | ---: | ---: | ---: | --- |
| 32 | `8 / 20ms` | 约 95ms | 约 92ms | 约 234ms | 入口突发排队明显 |
| 64 | `8 / 20ms` | 约 84ms | 约 79ms | 约 254ms | Gateway 有小幅收益,下游未改善 |
| 128 | `16 / 5ms` | 约 42ms | 约 17ms | 约 151ms | 当前最佳组合 |
| 128 | `32 / 5ms` | 约 50ms | 约 32ms | 约 221ms | pull worker 过多产生调度竞争,回退 |
最终保留 `NATS_ASYNC_RAW_WORKERS=128``FAST_WRITER_WORKERS=16``FAST_WRITER_FETCH_WAIT_MS=5`。Redis 批写自身 p99 低于 `5ms`,约 151ms 的剩余尾延迟主要位于 JetStream 持久化发布和 consumer deliveryNATS/Kafka pending、Kafka lag 与 writer retry 在每轮结束后均归零。
## 2026-07-14 WAL Outbox 验证
Gateway 已切换为分段 WAL + 异步 JetStream PubAck。WAL 使用 16MiB/5s 分段、1ms/256 条 group commit、`fsync=true`、10000 最大 inflight本地 Apple M4 并发基准约 1926 records/s`519144 ns/op`),没有以关闭 `fsync` 换取吞吐。
ECS 生产使用 JT808 `0x0200`、1000 个连接、每连接 1 FPS、持续 60 秒验证:连接成功 1000、失败 0发送 59500 帧、写错误 0、读错误 0WAL `submitted=acked=66776`(含同期真实三协议流量),结束后 WAL backlog/inflight、NATS pending、Kafka lag 全部为 0。压测结束 Gateway JT808 响应 recent p99 约 80ms。
随后在约 1000 FPS 下对 Gateway 执行真实 `SIGKILL`。systemd 约 5 秒后自动拉起;启动后立即从崩溃前 WAL 重放并确认约 563 条记录,最终 WAL backlog/inflight、NATS pending 和 history/stat/realtime/identity Kafka lag 均回到 0日志没有 CRC 损坏、publish error 或 panic。故障窗口内压测客户端的写/读错误是 TCP 连接被强制中断的预期结果,不代表已持久接受的服务端数据丢失。

View File

@@ -14,6 +14,7 @@ The Go services expose local-only health and metrics endpoints. They are intende
| History writer | `lingniu-go-history-writer.service` | `127.0.0.1:20212` | `/healthz` | `/readyz` | `/metrics` |
| Stat writer | `lingniu-go-stat-writer.service` | `127.0.0.1:20213` | `/healthz` | `/readyz` | `/metrics` |
| NATS Kafka bridge | `lingniu-go-nats-kafka-bridge.service` | `127.0.0.1:20214` | `/healthz` | `/readyz` | `/metrics` |
| Identity writer | `lingniu-go-identity-writer.service` | `127.0.0.1:20217` | `/healthz` | `/readyz` | `/metrics` |
| Realtime API | `lingniu-go-realtime-api.service` | `0.0.0.0:20200` | `/healthz` | `/readyz` | `/metrics` |
## Quick Checks
@@ -26,6 +27,7 @@ curl -fsS http://127.0.0.1:20211/metrics
curl -fsS http://127.0.0.1:20212/readyz
curl -fsS http://127.0.0.1:20213/readyz
curl -fsS http://127.0.0.1:20214/readyz
curl -fsS http://127.0.0.1:20217/readyz
curl -fsS http://127.0.0.1:20200/readyz
```
@@ -35,7 +37,19 @@ curl -fsS http://127.0.0.1:20200/readyz
/opt/lingniu-go-native/current/capacity-check
```
它会抓取 Gateway、History writer、Stat writer、NATS bridge、Realtime API、NATS fast writer 的本地 `/metrics`,输出 JSON。退出码 `0` 表示当前关键 backlog 和拒绝计数正常,退出码 `2` 表示存在 pending、Kafka lag、连接拒绝或 metrics 抓取失败,适合接入 cron/告警。
它会抓取 Gateway、History writer、Stat writer、Identity writer、NATS bridge、Realtime writer/API、NATS fast writer 的本地 `/metrics`,输出 JSON。退出码 `0` 表示当前关键 backlog 和拒绝计数正常,退出码 `2` 表示存在超阈值 pending、Kafka lag、连接拒绝或 metrics 抓取失败,适合接入 cron/告警。
Identity writer 必须消费 `vehicle.raw.go.jt808.v1`,且 Gateway 指标 `vehicle_gateway_jt808_registration_gateway_writes_enabled` 必须为 `0`。两者同时写 `jt808_registration` 会被 `capacity-check` 判为重复 writer。
Gateway 的 NATS 接受边界由 WAL outbox 提供。`vehicle_durable_outbox_backlog_records` 表示已经 `fsync` 但尚未 PubAck 的记录,`vehicle_durable_outbox_inflight` 表示正在等待 PubAck 的记录;健康时两者最终回到 `0``vehicle_durable_outbox_publish_total``submit_error``ack_error``remove_error``close_timeout` 默认按 outbox 错误门禁处理。稳定 event ID/NATS MsgId 负责崩溃恢复期间的 JetStream 去重。
默认还会用 histogram 估算链路 p99Gateway frame `250ms`、Gateway identity `100ms`、async sink `100ms`、bridge batch `5000ms`、fast-writer stage `100ms`、history flush `1000ms`、realtime store `100ms`、stat write `100ms`history/stat/realtime/identity 的 Kafka lag 总和超过 `100` 时降级。Gateway async sink 队列深度默认超过 `10000` 降级,可用 `-async-sink-queue-depth-max` 调整Gateway 身份快照必须 ready 且最近成功刷新时间不能超过 `180s`,分别用 `-gateway-identity-snapshot-required``-gateway-identity-snapshot-max-age-seconds` 调整。Gateway 身份解析缓存默认容量阈值是 `300000`Stat writer 统计缓存默认容量阈值是 `1000000`Realtime API 的 MySQL 写入节流缓存和 VIN 车牌缓存默认容量阈值分别是 `1000000``200000`,可分别用 `-gateway-identity-cache-entries-max``-stat-cache-entries-max``-realtime-throttle-cache-entries-max``-realtime-plate-cache-entries-max` 调整,设为 `0` 表示关闭该项容量检查。history、realtime、stat、identity writer 的 worker 数默认都不得低于 `3`,分别由 `-history-workers-min``-realtime-workers-min``-stat-workers-min``-identity-workers-min` 检查。低样本数会跳过 p99 判断,阈值设为 `0` 可临时关闭对应延迟检查。
批处理内部 in-flight pending 默认允许 `100` 条窗口:`-fast-writer-batch-pending-max=100``-history-batch-pending-max=100``-history-rows-pending-max=100``-stat-batch-pending-max=100`。这类指标用于发现批写卡住,不再因为高频流量下几十条正在 flush/ack 的瞬时批次直接降级NATS consumer pending、ack pending 和 Kafka lag 仍按独立阈值判断真实积压。
topic 流转检查默认只抓完全断层history/realtime 必须消费三类 raw topicstat 必须消费三类 fields topicbridge 未配置 subject 的 `route_error` 默认超过 `0` 条就降级,可用 `-bridge-route-error-max=-1` 临时关闭fast-writer `invalid_json` 默认超过 `0` 条就降级,可用 `-fast-writer-invalid-json-max=-1` 临时关闭history-writer `invalid_json` 默认超过 `0` 条就降级,可用 `-history-invalid-json-max=-1` 临时关闭realtime projector `invalid_json` 默认超过 `0` 条就降级,可用 `-realtime-invalid-json-max=-1` 临时关闭stat-writer `invalid_json` 默认超过 `0` 条就降级,可用 `-stat-invalid-json-max=-1` 临时关闭;某协议 gateway raw publish 达到 `100` 条,扣除 `skipped_non_realtime` 后仍然没有 fields publish 时降级;实时 fields 缺失或 publish error 比例超过 `20%` 时也会降级;某协议 gateway raw/fields publish 达到 `1000` 条但 bridge 没有写对应 Kafka topic 时降级;某协议 gateway raw publish 达到 `1000` 条但 fast-writer 没有消费同 raw subject 并成功写 Redis/ack 时降级;某 raw topic bridge 写 Kafka 达到 `1000` 条但 history 未收到/写入或 realtime 未收到/更新时降级;某 fields topic bridge 写 Kafka 达到 `100` 条但 stat-writer 未配置或未收到该 topic 时降级。解析质量检查默认在单协议 gateway frame 样本达到 `100` 条后启用,`OK` 以外的 `PARTIAL/BAD_FRAME/other` 比例超过 `5%` 时降级,用于发现协议解析器或上游报文质量突然恶化。响应质量检查默认在单协议 gateway response 尝试样本达到 `100` 条后启用,`build_error/write_error` 比例超过 `5%` 时降级;`skipped` 表示该消息类型无需响应,不计入分母。身份解析质量检查默认在单协议 identity 样本达到 `100` 条后启用,`resolved` 以外的 `unresolved/error/timeout` 比例超过 `20%` 时降级,用于发现 VIN/车牌/phone 映射突然失效。history-writer 会对实时数据帧检查 raw envelope 是否携带 `parsed_fields`,默认单 topic/protocol 达到 `100` 帧后,缺失比例超过 `5%` 降级用于发现历史证据层丢失扁平化解析字段。Redis 快路径字段质量检查默认在单 raw subject 写入字段达到 `1000` 个后启用,`skipped_stale/seen` 超过 `20%` 时降级用于发现大面积旧帧补发或设备时间漂移。stat-writer 还会在单个 fields topic 成功 append 达到 `100` 条但未抽取到任何里程样本时降级,或 VIN 缺失、里程缺失、非正里程、source 缺失这类可行动跳过比例超过 `60%` 时降级,用于发现字段映射或身份/source 关联断裂;单 topic 找到 `100` 条里程样本但没有 source tracking或 source endpoint 缺失比例超过 `60%` 时也会降级,用于发现 `vehicle_data_source` 来源管理断层;单 topic 已写入 `100` 条里程样本但最终 `vehicle_daily_mileage` 投影写入仍为 `0` 时也会降级,用于发现 source 候选层到最终查询层的断层;`skipped_same_mileage` 是重复总里程去重,不计入可行动跳过比例。已出现过的成功/received `*_last_*_unix_seconds` 如果超过 `300s` 未刷新,也会降级;刚启动尚未出现过该指标时不会因缺失 last activity 降级。`capacity-check` JSON 会额外派生 `*_last_*_age_seconds`,便于直接读取距今秒数。特殊环境可用 `-required-consumer-topics=''` 临时关闭消费 topic 合同检查。
`capacity-check` 还会默认请求 Realtime API 的 `/api/stats/daily-metrics/diagnostics/reasons`,把当天每日里程诊断聚合放到 JSON 的 `daily_mileage_diagnostics`。这项默认只展示 `vehicle_total``actionable_issue_total` 和原因分布,不会因为存在业务缺口直接退出 `2`;需要把缺口纳入发布/巡检阻断时,显式传 `-daily-mileage-diagnostics-max-actionable=0` 或其他阈值。诊断 API 不可访问会被视为观测能力故障并降级;特殊环境可用 `-daily-mileage-diagnostics-url=''` 关闭。
ECS 上通过 systemd timer 每分钟执行一次:
@@ -56,32 +70,106 @@ systemctl start lingniu-go-capacity-check.service
| `vehicle_gateway_connection_closes_total` | TCP connection closes by protocol and reason. Labels: `protocol`, `reason`. Reasons include `eof`, `read_timeout`, `read_error`, `extract_error`, `context_cancelled`, `max_connections`. |
| `vehicle_gateway_connection_rejections_total` | TCP connection rejections by protocol and reason. Labels: `protocol`, `reason`. |
| `vehicle_gateway_frames_total` | Protocol frames received and parsed by the gateway. Labels: `protocol`, `status`. |
| `vehicle_gateway_last_frame_unix_seconds` | Last observed gateway frame time by protocol and parse status. Labels: `protocol`, `status`. |
| `vehicle_gateway_frame_duration_ms` | Last observed Gateway frame handling duration and histogram buckets for p95/p99. Labels: `protocol`, `status`. |
| `vehicle_gateway_publish_total` | Gateway publish result for raw events. Unified appears only when the compatibility stream is explicitly enabled. Labels: `protocol`, `kind`, `status`. |
| `vehicle_gateway_identity_total` | Gateway identity resolve results. Labels: `protocol`, `status`; status includes `resolved`, `unresolved`, `error`, `timeout`. |
| `vehicle_gateway_identity_duration_ms` | Last observed identity resolve duration and histogram buckets for p95/p99. Labels: `protocol`, `status`. |
| `vehicle_gateway_identity_cache_entries` | Gateway identity resolver cache entry count. Labels: `cache`; values include runtime `lookup`/`registration`/`source_code`/`location_touch` and `snapshot_binding`/`snapshot_identifier`/`snapshot_registration`/`snapshot_source`. |
| `vehicle_gateway_identity_cache_max_entries` | Gateway identity resolver cache entry cap per cache. Controlled by `IDENTITY_LOOKUP_CACHE_MAX_ENTRIES`, default `300000`. |
| `vehicle_gateway_identity_snapshot_ready` | `1` after at least one complete atomic identity snapshot refresh; `0` means frames continue ingesting but unknown JT808 identities cannot be resolved. |
| `vehicle_gateway_identity_snapshot_entries` | Current identity snapshot entry counts. Labels: `kind` = `binding`, `identifier`, `registration`, `source`. |
| `vehicle_gateway_identity_snapshot_refresh_total` | Snapshot refresh attempts. Labels: `status` = `ok` or `error`; an error keeps the last known good snapshot. |
| `vehicle_gateway_identity_snapshot_last_success_unix_seconds` | Last successful complete snapshot publication time. |
| `vehicle_gateway_publish_total` | Gateway publish/delegation result. In NATS mode raw is published and fields uses `status="delegated"`; unified appears only when explicitly enabled. Labels: `protocol`, `kind`, `status`. |
| `vehicle_gateway_last_publish_unix_seconds` | Last gateway publish time by protocol/kind/status, useful for protocol freshness checks. Labels: `protocol`, `kind`, `status`. |
| `vehicle_gateway_fields_total` | Gateway fields eligibility result. Labels: `protocol`, `status`; NATS mode uses `delegated_to_bridge`, while compatibility direct mode may use `published`/`publish_error`. |
| `vehicle_gateway_response_total` | Gateway protocol response build/write result. Labels: `protocol`, `message_id`, `status`; status includes `ok`, `skipped`, `build_error`, `write_error`. |
| `vehicle_gateway_last_response_unix_seconds` | Last gateway protocol response build/write result time. Labels: `protocol`, `message_id`, `status`. |
| `vehicle_gateway_response_e2e_recent_p99_ms` | Bounded recent p99 from frame receipt through successful protocol response. Labels: `protocol`; capacity target defaults to `100ms`. |
| `vehicle_gateway_response_e2e_recent_samples` | Samples in the bounded response-latency window. Labels: `protocol`. |
| `vehicle_gateway_authentication_total` | Protocol authentication decisions. Labels: `protocol`, `mode`, `source`, `status`; `source=configured` means a configured credential matched, `source=device` means the JT808 phone's snapshot token matched, and `source=none` means no credential matched. `observe` records mismatches without rejecting, while `enforce` returns a protocol failure and closes the connection. |
| `vehicle_gateway_authentication_mode` | Active authentication mode by protocol. Labels: `protocol`, `mode`. |
| `vehicle_gateway_authentication_credentials` | Configured account count, never credential values. Labels: `protocol`. |
| `vehicle_async_sink_publish_duration_ms_histogram` | Async sink publish duration histogram covering worker publish calls to the delegate sink. Labels: `sink`, `kind`, `status`. |
| `vehicle_async_sink_queue_capacity` | Gateway async sink queue capacity. Labels: `sink`. NATS uses `NATS_ASYNC_QUEUE_SIZE`; Kafka fallback uses `KAFKA_ASYNC_QUEUE_SIZE`. |
| `vehicle_bridge_messages_total` | NATS messages fetched by the bridge. Labels: `subject`, `status`. |
| `vehicle_bridge_fields_projection_total` | Fields projection result from canonical raw. Labels: `protocol`, `status`; `published` is healthy, error/skip labels explain why no fields event was emitted. |
| `vehicle_bridge_fields_projection_count` | Latest flattened field count emitted by bridge per protocol/status. |
| `vehicle_bridge_last_message_unix_seconds` | Last NATS message fetched by subject/status. Labels: `subject`, `status`. |
| `vehicle_bridge_kafka_writes_total` | Bridge writes to Kafka. Labels: `topic`, `status`. |
| `vehicle_bridge_nats_acks_total` | NATS ack results after Kafka write. Labels: `subject`, `status`. |
| `vehicle_bridge_last_kafka_write_unix_seconds` | Last bridge Kafka write by topic/status. Labels: `topic`, `status`. |
| `vehicle_bridge_nats_acks_total` | NATS ack results after Kafka write or unrouted-subject isolation. Labels: `subject`, `status`; status includes `ok`, `error`, and `dropped_route_error`. |
| `vehicle_bridge_last_ack_unix_seconds` | Last NATS ack by subject/status. Labels: `subject`, `status`. |
| `vehicle_bridge_nats_consumer_pending` | JetStream messages pending for the bridge durable consumer. |
| `vehicle_bridge_nats_consumer_ack_pending` | JetStream messages delivered to bridge but not yet acked. |
| `vehicle_bridge_nats_consumer_waiting` | Pull requests waiting on the bridge durable consumer. |
| `vehicle_bridge_batch_pending_messages` | NATS messages fetched by the bridge but not yet written to Kafka and acked. |
| `vehicle_bridge_batch_pending_kafka_messages` | Kafka messages prepared for the current bridge batch. |
| `vehicle_bridge_batch_duration_ms_histogram` | Bridge batch duration histogram covering Kafka write and NATS ack. Labels: `status`. |
| `vehicle_history_batch_flush_duration_ms_histogram` | History writer TDengine batch flush duration histogram. Labels: `status`. |
| `vehicle_realtime_store_update_duration_ms_histogram` | Realtime MySQL/Redis projection duration histogram. Labels: `store`, `protocol`, `status`. |
| `vehicle_stat_write_duration_ms_histogram` | Stat writer MySQL write duration histogram. Labels: `topic`, `status`. |
| `vehicle_stat_samples_total` | Stat writer mileage sample results. Labels: `topic`, `protocol`, `status`; status includes `found`, `written`, `skipped_missing_fields`, `skipped_missing_vin`, `skipped_missing_mileage`, `skipped_non_positive_mileage`, `skipped_same_mileage`, `skipped_missing_source`, `event_time_future_adjusted`. `skipped_missing_fields` means the fields topic carried an envelope without flattened fields and should be treated as a stream-contract issue. The last status means device event time was more than 10 minutes ahead of received time and stats used received time instead. |
| `vehicle_fast_writer_nats_consumer_pending` | JetStream messages pending for the fast writer durable consumer. |
| `vehicle_fast_writer_nats_consumer_ack_pending` | JetStream messages delivered to fast writer but not yet acked. |
| `vehicle_fast_writer_nats_consumer_waiting` | Pull requests waiting on the fast writer durable consumer. |
| `vehicle_fast_writer_batch_pending_messages` | NATS fast writer messages fetched but not yet written to TDengine/Redis and acked. |
| `vehicle_fast_writer_batch_pending_envelopes` | Valid parsed envelopes in the current NATS fast writer batch. |
| `vehicle_fast_writer_messages_total` | Fast writer message results by NATS subject. Labels: `subject`, `status`; status includes `ok`, `error`, `invalid_json`, and `ack_error`. `invalid_json` is acked and isolated so it will not block the raw fast path, but it indicates the stream contract is polluted. |
| `vehicle_fast_writer_stage_duration_ms_histogram` | Fast writer stage duration histogram for TDengine, Redis, and NATS ack. The Redis stage is one batch pipeline when the repository supports batch updates. Labels: `subject`, `stage`, `status`. |
| `vehicle_history_writes_total` | TDengine history writes. Labels: `topic`, `status`. |
| `vehicle_fast_writer_redis_envelopes_total` | Redis realtime projection envelope result from the NATS fast path. Labels: `subject`, `status`; status includes `seen`, `updated`, `skipped_non_realtime`, `skipped_missing_vin`, `skipped_missing_vehicle_key`, `skipped_missing_fields`. Missing fields is an ingress contract failure: RAW is retained, but Redis online/KV projection is skipped and never re-flattened downstream. |
| `vehicle_fast_writer_redis_fields_total` | Redis realtime KV field write result from the NATS fast path. Labels: `subject`, `status`; status includes `seen`, `written`, `skipped_stale`. `skipped_stale` means an older event-time frame was allowed to add missing fields but was blocked from overwriting newer field values. |
| `vehicle_fast_writer_last_message_unix_seconds` | Last fast-writer message processing result by subject/status. Labels: `subject`, `status`. |
| `vehicle_fast_writer_last_stage_unix_seconds` | Last fast-writer stage completion by subject/stage/status. Labels: `subject`, `stage`, `status`. |
| `vehicle_history_writes_total` | TDengine history writes for valid envelopes only. Labels: `topic`, `status`; invalid JSON messages are committed after `vehicle_history_kafka_messages_total{status="invalid_json"}` and are not counted as writes. |
| `vehicle_history_parsed_fields_total` | Realtime raw envelopes that did or did not carry precomputed `parsed_fields` before TDengine history write. Labels: `topic`, `protocol`, `status`; status includes `present`, `missing`. |
| `vehicle_history_last_message_unix_seconds` | Last history Kafka message by topic/status. Labels: `topic`, `status`. |
| `vehicle_history_last_write_unix_seconds` | Last history TDengine write result by topic/status. Labels: `topic`, `status`. |
| `vehicle_history_last_commit_unix_seconds` | Last history Kafka commit by topic/status. Labels: `topic`, `status`. |
| `vehicle_history_batch_pending_messages` | Messages fetched by history writer but not yet flushed and committed. |
| `vehicle_history_batch_pending_rows` | Parsed raw envelopes waiting in the current TDengine batch. |
| `vehicle_history_retry_pending_messages` | Fetched messages retained in memory behind a failed TDengine write or Kafka commit. Healthy value is `0`; the writer does not fetch a new batch while this is non-zero. |
| `vehicle_history_batch_retries_total` | Failure-closed history batch retries. Labels: `reason`; `write_error` retries only the uncommitted suffix and `commit_error` retries commit without replaying TDengine writes. |
| `vehicle_history_batch_flush_duration_ms` | Last TDengine batch flush duration. Labels: `status`. |
| `vehicle_history_config{setting="workers"}` | Configured in-process history Kafka consumer count; default and capacity minimum are `3`. |
| `vehicle_history_worker_active` | Active history consumer gauge by `worker`; all configured workers should be `1`. |
| `vehicle_stat_writes_total` | MySQL metric writes. Labels: `topic`, `status`. |
| `vehicle_stat_kafka_messages_total` | Stat writer Kafka message results by fields topic. Labels: `topic`, `status`; `invalid_json` messages are committed and isolated before MySQL mileage statistics. |
| `vehicle_stat_last_message_unix_seconds` | Last stat Kafka message by topic/status. Labels: `topic`, `status`. |
| `vehicle_stat_last_write_unix_seconds` | Last stat MySQL append result by topic/status. Labels: `topic`, `status`. |
| `vehicle_stat_last_commit_unix_seconds` | Last stat Kafka commit by topic/status. Labels: `topic`, `status`. |
| `vehicle_stat_write_duration_ms_histogram` | MySQL stat writer append duration histogram. Labels: `topic`, `status`. |
| `vehicle_stat_config{setting="workers"}` | Configured in-process Kafka consumer count. Default and capacity-check minimum are `3`. |
| `vehicle_stat_worker_active` | Active stat consumer gauge by `worker`; all configured workers should remain `1` while the service is running. |
| `vehicle_stat_batch_pending_messages` | Messages fetched by stat writer but not yet appended to MySQL and committed. Controlled by `STATS_BATCH_SIZE` and `STATS_BATCH_WAIT_MS`; capacity check default threshold is `1000`. |
| `vehicle_stat_retry_pending_messages` | Fetched messages retained in memory behind a failed MySQL write or Kafka commit. Healthy value is `0`. |
| `vehicle_stat_batch_retries_total` | Failure-closed stat batch retries. Labels: `reason`; commit-only retries do not append the mileage sample twice in the same process. |
| `vehicle_stat_cache_entries` | Stat writer in-memory cache entries by cache type. Labels: `cache`; cache includes `last_total_mileage`, `source_seen`, `projection`, and `baseline`. Runtime cap is controlled by `STATS_CACHE_MAX_ENTRIES` and defaults to `1000000`. |
| `vehicle_stat_cache_max_entries` | Stat writer configured per-cache entry cap. `0` means unlimited and should not be used in production without an external memory guard. |
| `vehicle_stat_cache_evictions_total` | Cumulative stat writer cache evictions caused by capacity pressure. Labels: `cache`. |
| `vehicle_stat_samples_total` | Mileage samples extracted and written by stat writer. Use this with `vehicle_stat_writes_total`: write `ok` only means the append call succeeded, while `status="written"` proves a sample reached `vehicle_daily_mileage_source`. |
| `vehicle_stat_sources_total` | Stat writer source tracking results for `vehicle_data_source`. Labels: `topic`, `protocol`, `status`; status includes `attempted`, `written`, `skipped_throttled`, `skipped_missing_endpoint`. This intentionally avoids source IP labels to keep metrics low-cardinality. |
| `vehicle_stat_projections_total` | Final daily mileage projection results from `vehicle_daily_mileage_source` to `vehicle_daily_mileage`. Labels: `topic`, `protocol`, `status`; status includes `attempted`, `written`, `skipped_throttled`. |
| `vehicle_realtime_updates_total` | Redis/MySQL realtime projector updates. Labels: `topic`, `status`. |
| `vehicle_realtime_kafka_messages_total` | Realtime projector Kafka message results by raw topic. Labels: `topic`, `status`; `invalid_json` messages are committed and isolated before Redis/MySQL projection. |
| `vehicle_realtime_last_message_unix_seconds` | Last realtime Kafka message by topic/status. Labels: `topic`, `status`. |
| `vehicle_realtime_last_update_unix_seconds` | Last realtime projection result by topic/status. Labels: `topic`, `status`. |
| `vehicle_realtime_last_commit_unix_seconds` | Last realtime Kafka commit by topic/status. Labels: `topic`, `status`. |
| `vehicle_realtime_store_update_duration_ms_histogram` | Redis/MySQL store update duration histogram. Labels: `store`, `protocol`, `status`. |
| `vehicle_realtime_config{setting="workers"}` | Configured in-process realtime Kafka consumer count; default and capacity minimum are `3`. |
| `vehicle_realtime_worker_active` | Active realtime consumer gauge by `worker`; all configured workers should be `1`. |
| `vehicle_realtime_async_queue_total` | Realtime API async secondary projection queue events for MySQL snapshot/location. Labels: `store`, `protocol`, `status`; status includes `queued`, `dropped`, `closed`. |
| `vehicle_realtime_async_queue_depth` | Realtime API async MySQL projection queue depth. Labels: `store`, `protocol`. |
| `vehicle_realtime_async_queue_capacity` | Realtime API async MySQL projection queue capacity. Labels: `store`; controlled by `MYSQL_REALTIME_ASYNC_QUEUE_SIZE`. |
| `vehicle_realtime_retry_pending_messages` | Fetched messages retained in memory behind a failed realtime projection or Kafka commit. Healthy value is `0`. |
| `vehicle_realtime_batch_retries_total` | Failure-closed realtime batch retries. Labels: `reason`; only the uncommitted suffix is projected again after a write failure. |
| `vehicle_identity_writer_retry_pending_messages` | JT808 raw messages retained behind a failed registration MySQL transaction or Kafka commit. Healthy value is `0`; identity writer does not fetch newer offsets while non-zero. |
| `vehicle_identity_writer_config{setting="workers"}` | Configured identity Kafka consumer count; default and capacity minimum are `3`. |
| `vehicle_identity_writer_worker_active` | Active identity consumer gauge by `worker`; every configured worker should remain `1`. |
| `vehicle_identity_writer_batch_retries_total` | Failure-closed identity retries. Labels: `reason`; `write_error` retries the transaction and `commit_error` retries only Kafka commit. |
| `vehicle_realtime_plate_cache_entries` | Realtime API VIN-to-plate binding cache entry count. Default runtime cap is controlled by `PLATE_CACHE_MAX_ENTRIES` and defaults to `200000`. |
| `vehicle_realtime_plate_cache_max_entries` | Realtime API configured VIN-to-plate cache entry cap. `0` means unlimited and should not be used in production without an external memory guard. |
| `vehicle_realtime_plate_cache_evictions_total` | Cumulative VIN-to-plate cache evictions caused by TTL expiry cleanup or capacity pressure. |
| `vehicle_history_kafka_lag` | Estimated Kafka lag for history writer by topic and partition. |
| `vehicle_stat_kafka_lag` | Estimated Kafka lag for stat writer by topic and partition. |
| `vehicle_realtime_kafka_lag` | Estimated Kafka lag for realtime projector by topic and partition. |

View File

@@ -58,8 +58,8 @@ Go 服务使用 systemd 裸机部署,不使用 Docker。
最近 release
```text
/opt/lingniu-go-native/releases/production-track-telemetry-20260714082451
/opt/lingniu-go-native/releases/100k-hardening-20260703175730
/opt/lingniu-go-native/releases/parsed-fields-once-20260703163821
```
服务重启:
@@ -511,14 +511,28 @@ vehicle_gateway_frame_duration_ms_histogram_sum{protocol,status}
该耗时覆盖单帧从解析、identity resolve、parsed fields 生成、publish enqueue 到协议响应写入的整体入口处理路径。带帧率压测时用它按协议观察 p95/p99不能只看 `vehicle_gateway_frame_duration_ms` 最后一条 gauge。
### Gateway identity resolve duration
2026-07-11 已为 Gateway 身份解析单独增加耗时 histogram 和超时分类:
```text
vehicle_gateway_identity_total{protocol,status}
vehicle_gateway_identity_duration_ms_histogram_bucket{protocol,status,le}
vehicle_gateway_identity_duration_ms_histogram_count{protocol,status}
vehicle_gateway_identity_duration_ms_histogram_sum{protocol,status}
```
生产默认使用周期身份快照,每帧只查本机内存;`status="timeout"` 主要用于兼容关闭 `IDENTITY_SNAPSHOT_ONLY_ENABLED` 后的回源查询模式。快照模式应重点观察 `vehicle_gateway_identity_snapshot_ready` 和最近成功刷新时间RDS 故障时入口继续接收,上一版快照继续服务,未知 phone 暂时 unresolved。
### History writer TDengine 批写
2026-07-03 已实现 history-writer 第一阶段批写:
- `history.Writer.AppendAllBatch` 按 TDengine child table 聚合 raw/location 多行 `INSERT`
- `cmd/history-writer` 默认 `HISTORY_BATCH_SIZE=200``HISTORY_BATCH_WAIT_MS=100`
- `cmd/history-writer` 默认 `HISTORY_WORKERS=3``HISTORY_BATCH_SIZE=200``HISTORY_BATCH_WAIT_MS=20`
- TDengine batch 成功后才批量提交 Kafka messages失败不提交 offset。
- 保留单条 `AppendAll` 路径作为回退。
- `production-track-telemetry-20260714082451` 起,`vehicle_locations` stable 增加 `soc_percent`。history-writer 在建表后执行幂等 `ALTER STABLE ... ADD COLUMN soc_percent DOUBLE`,只忽略 TDengine 明确的重复列错误;其他迁移错误会阻止 ready禁止带病继续写入。单条和批量位置 INSERT 都必须携带 SOC。
新增指标:

View File

@@ -23,9 +23,12 @@
| --- | --- | --- | --- |
| 接入 | Gateway | `lingniu-go-gateway.service` | `127.0.0.1:20211` |
| 桥接 | NATS Kafka bridge | `lingniu-go-nats-kafka-bridge.service` | `127.0.0.1:20214` |
| 字段投影 | Kafka RAW fields projector | `lingniu-go-fields-projector.service` | `127.0.0.1:20218` |
| 历史 | TDengine writer | `lingniu-go-history-writer.service` | `127.0.0.1:20212` |
| 统计 | MySQL stat writer | `lingniu-go-stat-writer.service` | `127.0.0.1:20213` |
| 实时 | Realtime API/projector | `lingniu-go-realtime-api.service` | `127.0.0.1:20200` |
| 实时写入 | Realtime writer | `lingniu-go-realtime-writer.service` | `127.0.0.1:20216` |
| 身份事实 | JT808 identity writer | `lingniu-go-identity-writer.service` | `127.0.0.1:20217` |
| 实时/API | Realtime API | `lingniu-go-realtime-api.service` | `127.0.0.1:20200` |
## Topic 配置基线
@@ -34,20 +37,113 @@
| 服务 | 必要 topic |
| --- | --- |
| `lingniu-go-history-writer.service` | `vehicle.raw.go.gb32960.v1``vehicle.raw.go.jt808.v1``vehicle.raw.go.yutong-mqtt.v1` |
| `lingniu-go-stat-writer.service` | `vehicle.raw.go.gb32960.v1``vehicle.raw.go.jt808.v1``vehicle.raw.go.yutong-mqtt.v1` |
| `lingniu-go-realtime-api.service` | `vehicle.raw.go.gb32960.v1``vehicle.raw.go.jt808.v1``vehicle.raw.go.yutong-mqtt.v1` |
| `lingniu-go-nats-kafka-bridge.service` | NATS raw subjects 到同名 Kafka topic`vehicle.event.go.unified.v1` 只在兼容开关打开时使用 |
| `lingniu-go-stat-writer.service` | `vehicle.fields.go.gb32960.v1``vehicle.fields.go.jt808.v1``vehicle.fields.go.yutong-mqtt.v1` |
| `lingniu-go-realtime-writer.service` | `vehicle.raw.go.gb32960.v1``vehicle.raw.go.jt808.v1``vehicle.raw.go.yutong-mqtt.v1` |
| `lingniu-go-identity-writer.service` | `vehicle.raw.go.jt808.v1` |
| `lingniu-go-fields-projector.service` | 消费三类 `vehicle.raw.go.*`,把 RAW 中已有的 `parsed_fields` 投影到对应 `vehicle.fields.go.*` |
| `lingniu-go-nats-kafka-bridge.service` | NATS canonical raw 到 Kafka raw并从 raw 中已有的 `parsed_fields` 投影同协议 Kafka fields`vehicle.event.go.unified.v1` 只在兼容开关打开时使用 |
如果 stat-writer 少消费某个 raw topic对应协议的每日里程不会进入 `vehicle_daily_mileage`。修正后可能出现短时间 Kafka lag这是在追补历史 backlog只要 `vehicle_stat_writes_total` 持续增长且 lag 下降,就是健康状态
Gateway 在 NATS 模式下设置 `FIELDS_DERIVE_FROM_RAW_ENABLED=true`,每帧只发布 canonical rawbridge 设置 `BRIDGE_DERIVE_FIELDS_FROM_RAW_ENABLED=true`,只复用 raw envelope 中已计算一次的 `parsed_fields`不会重新解析协议原文。bridge 对同一条 NATS raw 生成 Kafka raw 和可选 Kafka fields两个输出都成功后才 ACK任一输出失败会保留 NATS 消息等待重放。登录、注册、鉴权等非实时帧只写 raw不生成 fields
如果 stat-writer 少消费某个 fields topic对应协议的每日里程不会进入 `vehicle_daily_mileage`。stat-writer 启动时会拒绝 `vehicle.raw.*` 这类 RAW topic 配置,避免统计链路重新从原始报文解析并破坏 RAW/fields 解耦。修正后可能出现短时间 Kafka lag这是在追补历史 backlog`vehicle_stat_writes_total{status="ok"}` 只表示 append 调用成功,真正证明里程样本入库的是 `vehicle_stat_samples_total{status="written"}` 持续增长且 lag 下降。
stat-writer 默认 `STATS_WORKERS=3`,每个 worker 是同一 consumer group 的独立 Kafka reader。Kafka 按车辆 key 固定分区,因此单车累计里程仍按分区顺序执行,不同分区并行。调整 worker 后同时核对 `vehicle_stat_config{setting="workers"}`、每个 `vehicle_stat_worker_active{worker}`、MySQL 连接数、write p99 和 stat lagworker 不应超过 fields topic 的有效分区并行度。
history-writer 和 realtime-writer 同样默认分别使用 `HISTORY_WORKERS=3``REALTIME_WORKERS=3`。每个 worker 只顺序处理 Kafka 分配给自己的分区TDengine 表缓存、MySQL 实时节流缓存和车牌缓存均支持并发访问。调整后必须同时核对对应的 `*_config{setting="workers"}``*_worker_active`、pending/retry、后端连接数和 Kafka lag。
history-writer 启动会对既有 `vehicle_locations` stable 幂等补齐 `soc_percent DOUBLE`。发布后除 `/readyz` 外,必须抽查一台支持 SOC 的车辆在 TDengine 历史位置中 `soc_percent IS NOT NULL`;如果迁移报错且不是明确的重复列错误,服务应保持未就绪,先处理 TDengine DDL/权限,不能跳过迁移后强启。
identity-writer 默认使用 `IDENTITY_WRITER_WORKERS=3`。每个 worker 持有独立的位置触达节流器并共享 MySQL 连接池Kafka phone/vehicle key 保证同一终端在单一分区内有序,分区再平衡最多产生一次幂等触达。调整后核对 `vehicle_identity_writer_config{setting="workers"}``vehicle_identity_writer_worker_active`、pending/retry、MySQL 连接数和 identity lag。
Gateway 和 NATS bridge 启动时也会检查 raw/fields 合同Kafka raw topic 必须是 `vehicle.raw.*`fields topic 必须是 `vehicle.fields.*`NATS raw subject 和 fields subject 不允许相同。若服务启动失败并提示 `raw and fields ... must be different``fields kafka topic ... must start with "vehicle.fields."`,应先修正 `/opt/lingniu-go-native/env/*.env` 的 topic/subject 配置。
Realtime API 是当前态投影,默认在没有已提交 offset 时从 latest 开始消费。切换 topic 或新建 consumer group 后,不应让 realtime 追扫历史 raw backlog需要重建当前态时应使用明确的回放任务或手动 reset offset。
history、stat、realtime、identity 四个 Kafka 消费服务都会暴露 `vehicle_kafka_consumer_info{service,group,topic}``capacity-check` 会把缺少该指标的消费服务判为 degraded用来捕获“进程活着但没有挂上 Kafka topic”的配置错误。Identity writer 上线后 Gateway 必须设置 `JT808_REGISTRATION_GATEWAY_WRITES_ENABLED=false`,容量巡检会阻止双写 `jt808_registration`
业务端口:
- GB32960 TCP`0.0.0.0:32960`
- JT/T 808 TCP`0.0.0.0:808`
- 实时 writer`127.0.0.1:20216`
- 身份 writer`127.0.0.1:20217`
- 实时/API`0.0.0.0:20200`
## 协议鉴权配置
Gateway 的鉴权策略分为 `disabled``observe``enforce`。生产新增或变更账号时先使用 `observe`,确认 `vehicle_gateway_authentication_total` 中只有 `accepted` 后再切换 `enforce`
GB32960 多平台账号使用仅 root 可读的 JSON 文件,不把密码提交到 Git 或写入 systemd unit
```json
{
"platform-a": "current-password",
"platform-b": ["migration-old-password", "current-password"]
}
```
`gateway.env` 只配置文件地址和模式:
```bash
GB32960_PLATFORM_CREDENTIALS_FILE=/opt/lingniu-go-native/secrets/gb32960-platform-credentials.json
GB32960_AUTH_MODE=observe
JT808_AUTH_MODE=observe
JT808_REGISTER_AUTH_CODE=issued-auth-code
IDENTITY_RESOLVE_TIMEOUT_MS=50
```
Gateway 生产必须启用独立 WAL outbox不与旧 `NATS_SPOOL_DIR` 混用:
```bash
NATS_DURABLE_OUTBOX_ENABLED=true
NATS_OUTBOX_DIR=/opt/lingniu-go-native/spool/nats-outbox-wal
NATS_OUTBOX_MAX_INFLIGHT=10000
NATS_OUTBOX_ACK_TIMEOUT_MS=3000
NATS_OUTBOX_REPLAY_BATCH_SIZE=1000
NATS_OUTBOX_REPLAY_INTERVAL_MS=1000
NATS_OUTBOX_FSYNC=true
NATS_OUTBOX_CLOSE_TIMEOUT_MS=5000
NATS_OUTBOX_WAL_SEGMENT_BYTES=16777216
NATS_OUTBOX_WAL_SEGMENT_AGE_MS=5000
NATS_OUTBOX_WAL_APPEND_QUEUE_SIZE=100000
NATS_OUTBOX_WAL_COMMIT_BATCH_SIZE=256
NATS_OUTBOX_WAL_COMMIT_INTERVAL_MS=1
```
WAL 在分组 `fsync` 成功后才向协议处理线程返回接受成功网络发布不阻塞设备连接PubAck 前记录始终可恢复。正常值为 `vehicle_durable_outbox_backlog_records=0``vehicle_durable_outbox_inflight=0`,且 `submitted``acked` 最终一致。`submit_error``ack_error``remove_error``close_timeout` 任一增长都必须排查 NATS、磁盘和进程关闭过程。WAL 的 CRC 损坏会拒绝 Gateway 启动,禁止跳过或删除文件后强行启动;应先保留目录副本再恢复。
GB32960 登录密码在鉴权完成后会从 `parsed_fields` 删除,只保留 `password_present`;协议原始帧仍可能包含凭据,因此 RAW 查询接口和 TDengine `raw_hex` 必须按敏感数据控制访问。JT808 的 `0x0102` 会先匹配配置的公共鉴权码,再按规范化手机号匹配 `jt808_registration.auth_token` 的只读内存快照,全程不查询 MySQL指标 `source=device` 表示命中终端历史鉴权码。强制模式拒绝的鉴权帧不会覆盖该可信鉴权码和最近鉴权成功时间。JT808 的 `enforce` 不会拒绝未发注册/认证、直接上报 `0x0200` 的兼容平台;这类来源继续通过身份缺口指标治理。
## 标准发布流程
Go 原生服务发布必须使用 Linux amd64 构建产物,不要直接上传本机默认 `go build` 结果。仓库提供统一脚本:
```bash
cd go/vehicle-gateway
# 首次上线 identity-writer 前准备 systemd、env 和 Gateway 单写开关。
scripts/install-identity-writer-service.sh --host root@115.29.187.205
# 首次上线 fields-projector 前准备 systemd 和 env该步骤不会自动关闭 bridge 的兼容字段投影。
scripts/install-fields-projector-service.sh --host root@115.29.187.205
# 只构建并校验 ELF x86-64 产物。
scripts/deploy-ecs-release.sh --build-only --release verify-$(date +%Y%m%d%H%M%S)
# 构建、上传并校验 release但不切换 current、不重启服务。
scripts/deploy-ecs-release.sh \
--host root@115.29.187.205 \
--release staged-$(date +%Y%m%d%H%M%S) \
--stage-only
# 构建、上传新 release、切换 current、重启服务并执行 readyz/capacity-check。
scripts/deploy-ecs-release.sh \
--host root@115.29.187.205 \
--release production-$(date +%Y%m%d%H%M%S)
```
脚本会构建 `gateway``history-writer``stat-writer``identity-writer``realtime-api``nats-fast-writer``nats-kafka-bridge``fields-projector``capacity-check``load-sim``stats-backfill``identity-import`,并逐个校验 `file` 输出包含 `ELF 64-bit``x86-64`。正式发布按 bridge、fields projector、下游 writer、Gateway 的依赖顺序重启,确保 canonical raw 到 fields 的派生能力先于入口切换生效;发布窗口暂停 `lingniu-go-capacity-check.timer`,发布后恢复 timer、检查 9 个 systemd 服务、9 个 `/readyz``capacity-check``--stage-only` 只落盘 release不改变运行态。生产发布如需把业务统计缺口纳入发布门禁使用 `--capacity-args "-daily-mileage-diagnostics-max-actionable=50"` 这类参数,和 `lingniu-go-capacity-check.service` 的定时巡检口径保持一致。
## 五分钟排查顺序
先回答最核心的问题:数据有没有进来,有没有排队,有没有被消费,最后有没有被查询到。
@@ -56,30 +152,43 @@ Realtime API 是当前态投影,默认在没有已提交 offset 时从 latest
2. 检查 gateway 帧计数和 TCP 活跃连接。
3. 检查 NATS bridge 的 pending 和 ack-pending。
4. 检查 bridge 写 Kafka 与 NATS ack 是否同时增长。
5. 检查 history、stat、realtime类 Kafka consumer lag。
5. 检查 history、stat、realtime、identity 四类 Kafka consumer lag。
6. 检查各 writer 的写入、提交、更新计数。
7. 最后再查存储和业务查询 API。
```bash
for port in 20211 20212 20213 20214 20200; do
for port in 20211 20212 20213 20214 20215 20216 20217 20218 20200; do
curl -fsS "http://127.0.0.1:${port}/readyz"
echo
done
curl -fsS http://127.0.0.1:20211/metrics \
| grep -E 'vehicle_gateway_(active_connections|frames_total|publish_total)|vehicle_async_sink'
| grep -E 'vehicle_gateway_(active_connections|frames_total|last_frame|publish_total|last_publish)|vehicle_async_sink'
curl -fsS http://127.0.0.1:20214/metrics \
| grep -E 'vehicle_bridge_(nats_consumer|kafka_writes_total|nats_acks_total)'
| grep -E 'vehicle_bridge_(nats_consumer|fields_projection|kafka_writes_total|last_kafka_write|nats_acks_total|last_ack|last_message)'
curl -fsS http://127.0.0.1:20212/metrics | grep vehicle_history_kafka_lag
curl -fsS http://127.0.0.1:20212/metrics | grep vehicle_history_batch
curl -fsS http://127.0.0.1:20213/metrics | grep vehicle_stat_kafka_lag
curl -fsS http://127.0.0.1:20200/metrics | grep vehicle_realtime_kafka_lag
curl -fsS http://127.0.0.1:20213/metrics | grep vehicle_stat_samples_total
curl -fsS http://127.0.0.1:20213/metrics | grep vehicle_stat_sources_total
curl -fsS http://127.0.0.1:20213/metrics | grep vehicle_stat_last_
curl -fsS http://127.0.0.1:20216/metrics | grep vehicle_realtime_kafka_lag
curl -fsS http://127.0.0.1:20217/metrics | grep vehicle_identity_writer_kafka_lag
for port in 20212 20213 20216 20217 20200; do
curl -fsS "http://127.0.0.1:${port}/metrics" | grep vehicle_kafka_consumer_info
done
/opt/lingniu-go-native/current/capacity-check
```
`capacity-check` 会解析 histogram 并做链路耗时早期预警,默认 `-gateway-frame-p99-ms=250``-gateway-identity-p99-ms=100``-async-sink-p99-ms=100``-async-sink-queue-wait-recent-p99-ms=100``-bridge-batch-p99-ms=5000``-fast-writer-stage-p99-ms=100``-history-flush-p99-ms=1000``-realtime-batch-p99-ms=500``-realtime-store-p99-ms=100``-stat-write-p99-ms=100``-kafka-lag-max=1000``-histogram-min-samples=100`。其中 realtime batch 是 Kafka projector 的整批 flush 耗时,主要看吞吐是否吃紧;单车实时投影延迟仍看 fast-writer Redis stage、realtime store update、Kafka lag 和 pending。批处理内部 in-flight pending 默认允许 `-fast-writer-batch-pending-max=1000``-history-batch-pending-max=1000``-history-rows-pending-max=5000``-stat-batch-pending-max=1000``-realtime-batch-pending-max=1000`,用于避免高频流量里百条级正在 flush/ack 的批次误报failure-closed 重试则由 `-consumer-retry-pending-max=0` 单独门禁history/realtime/stat/identity 任一 `*_retry_pending_messages` 非零都会降级设置为负数才关闭。bridge/fast-writer 的 unacked ratio 要达到 `-bridge-message-min-received=5000``-fast-writer-message-min-received=5000` 后才判断。刚重启样本不足时不判断 p99少量 Kafka lag 可能只是高频流量里的 in-flight 消息,只有 history/stat/realtime/identity lag 总和超过阈值才降级;压测阶段可以临时调低阈值。
topic 流转检查默认只抓完全断层:`-required-consumer-topics` 要求 history/realtime 消费三类 raw topic、stat 消费三类 fields topic`-gateway-field-min-raw=100` 检查某协议 raw publish 足够多,扣除登录、注册、鉴权等 `skipped_non_realtime` 后仍然没有 fields 直接发布或委托 bridge`-gateway-field-max-missing-ratio=0.20` 检查实时帧缺少预计算 fields 或发布错误比例是否过高;`-gateway-bridge-min-publishes=1000` 检查某协议 raw/委托 fields 已经由 gateway 接受,但 bridge 没有写对应 Kafka topic`-gateway-fast-writer-min-raw-publishes=1000` 检查某协议 raw 已经由 gateway 发布,但 fast-writer 没有消费同 raw subject 并成功写 Redis/ack`-raw-fanout-min-bridge-writes=1000` 检查某 raw topic 已经由 bridge 写入 Kafka但 history 未收到/写入或 realtime 未收到/更新;`-gateway-parse-min-frames=100``-gateway-parse-max-bad-ratio=0.05` 检查某协议入口帧解析质量是否突然恶化;`-gateway-response-min-samples=100``-gateway-response-max-error-ratio=0.05` 检查 32960 登录 ACK、808 注册/通用 ACK 等协议响应是否大量 build/write 失败,`skipped` 表示该消息类型无需响应,不计入比例;`-gateway-identity-snapshot-required=1``-gateway-identity-snapshot-max-age-seconds=180` 检查内存身份快照是否可用且持续刷新,`-gateway-identity-min-samples=100``-gateway-identity-max-unresolved-ratio=0.20` 检查某协议 VIN/车牌/phone 映射质量是否突然失效;`-history-parsed-field-min-frames=100``-history-parsed-field-max-missing-ratio=0.05` 检查 history-writer 收到的实时 raw envelope 是否缺失预计算 `parsed_fields``-fast-writer-redis-envelope-min-seen=100` 检查 Redis 快路径看到足够 raw envelope 后是否完全没有更新当前态,`-fast-writer-redis-envelope-max-bad-ratio=0.50` 检查缺 VIN/vehicle_key 的实时帧比例是否异常;`-fast-writer-redis-field-min-seen=1000``-fast-writer-redis-field-max-stale-ratio=0.20` 检查 Redis 快路径旧帧字段跳过比例是否异常升高;`-stat-topic-min-bridge-writes=1000` 检查某 fields topic 已经由 bridge 写入 Kafka但 stat-writer 未配置或未收到该 topic。统计链路还会用 `-stat-sample-min-writes=100` 检查“某个 fields topic 已成功 append 但完全抽不到里程样本”的配置/字段映射故障,并用 `-stat-sample-max-actionable-skip-ratio=0.60` 检查 fields 缺失、VIN 缺失、里程缺失、非正里程、source 缺失这类可行动跳过是否占比过高;`-stat-source-min-samples=100``-stat-source-max-missing-ratio=0.60` 检查已找到里程样本但来源端点缺失或 `vehicle_data_source` tracking 没有工作;`-stat-projection-min-sample-writes=100` 检查某 fields topic 已写入 source 候选里程样本但最终 `vehicle_daily_mileage` 投影完全没有写入;`skipped_same_mileage` 只是重复总里程去重,不计入该比例。`-last-activity-stale-seconds=300` 检查已出现过的成功/received `*_last_*_unix_seconds` 是否超过 5 分钟未刷新。上述阈值设为 `0` 可临时关闭;特殊环境可用 `-required-consumer-topics=''` 关闭消费 topic 合同检查。
每日里程业务诊断也会进入 `capacity-check` 的 JSON默认通过 `-daily-mileage-diagnostics-url=http://127.0.0.1:20200/api/stats/daily-metrics/diagnostics/reasons` 查询当天原因汇总,输出 `daily_mileage_diagnostics.vehicle_total``actionable_issue_total``items`。二进制默认 `-daily-mileage-diagnostics-max-actionable=-1` 表示只展示不阻断;生产定时巡检和发布门禁应显式设置一个阈值,例如当前已知 MQTT 稀疏总里程期间可先用 `50`,待问题清理后收紧到 `0`。诊断接口不可访问会降级,因为这表示 MySQL/API 观测链路不可用;临时环境可用 `-daily-mileage-diagnostics-url=''` 关闭。
## 健康基线
当前生产环境的健康特征:
@@ -87,10 +196,13 @@ curl -fsS http://127.0.0.1:20200/metrics | grep vehicle_realtime_kafka_lag
- 所有 `/readyz` 都返回 `status=ok`
- `vehicle_bridge_nats_consumer_ack_pending``0`
- history、stat、realtime 的 Kafka lag 为 `0` 或短时间小幅波动后归零。
- history、stat、realtime 都能看到 `vehicle_kafka_consumer_info`
- `capacity-check` 返回 `status=ok` 且退出码为 `0`
- gateway 的帧计数持续增长。
- bridge 的 Kafka write 和 NATS ack 计数同时增长。
- writer 的成功计数增长,同时 Kafka lag 不持续扩大。
- writer 的成功计数增长,同时 Kafka lag 不持续扩大;统计链路还应看到 `vehicle_stat_samples_total{status="written"}` 随有效里程字段流量增长
生产默认日志级别保持 `LOG_LEVEL=info`。Gateway 的 TCP 连接打开、关闭、空闲超时属于高频事件,只保留在 debug 级别;排查单车连接问题时可以临时设置 `LOG_LEVEL=debug`,排查结束后应恢复 info常态监控以 `vehicle_gateway_active_connections``vehicle_gateway_connection_closes_total` 为准。
定时容量巡检由 systemd timer 触发:
@@ -103,6 +215,8 @@ journalctl -u lingniu-go-capacity-check.service --since '10 minutes ago' --no-pa
GB32960 和 JT/T 808 的活跃连接数受上游平台连接方式影响,不能直接等同于车辆数。突然归零或持续异常下降才是信号。
计数器只说明服务启动后累计发生过,不代表当前仍有流量。排查断流时同时看 `*_last_*_unix_seconds`gateway 看 `vehicle_gateway_last_frame_unix_seconds``vehicle_gateway_last_publish_unix_seconds`bridge 看 `vehicle_bridge_last_kafka_write_unix_seconds`fast-writer 看 `vehicle_fast_writer_last_message_unix_seconds``vehicle_fast_writer_last_stage_unix_seconds`history/stat/realtime 分别看对应 `last_message``last_write/update``last_commit`。如果 counter 很高但 last 时间长时间不变,说明对应协议或 topic 已经停止活动。`capacity-check` JSON 会额外派生 `*_last_*_age_seconds`,现场排查优先看 age 是否持续扩大。
## 压测入口
Go 版本提供 `cmd/load-sim` 用于阶段性连接和帧写入压测。压测生产入口前必须先确认上游真实数据窗口,避免和业务流量混淆。
@@ -121,6 +235,26 @@ go run ./cmd/load-sim \
-send=false
```
带帧压测 JT808 时必须读取服务端应答,并使用隔离手机号区间和自动清理:
```bash
export MYSQL_DSN="$(sed -n 's/^MYSQL_DSN=//p' /opt/lingniu-go-native/env/base.env | tail -n 1)"
/opt/lingniu-go-native/current/load-sim \
-protocol jt808 \
-addr 127.0.0.1:808 \
-connections 1000 \
-connect-rate 500 \
-send-interval 1s \
-duration 60s \
-template 0200 \
-jt808-phone-base 139000012000 \
-drain-responses=true \
-cleanup-registration
```
输出必须满足 `connections_failed=0``write_errors=0``response_bytes>0``read_errors=0`。每轮使用未在最近 10 分钟压测过的新手机号区间时,结束后的 `rows_deleted` 应等于模拟连接数;重复号段可能被 identity-writer 的 location touch 节流跳过写入,因此清理数为 `0` 也不表示删除失败。`-cleanup-only` 可用于异常中断后的补清理;删除条件固定受手机号区间和 loopback 来源约束不清理真实注册。Bridge 默认 `BRIDGE_KAFKA_WRITE_CONCURRENCY=6`,同一批次按 topic 并行写 KafkaRAW 和 fields 均成功后才 ACK NATS 源消息。当前生产压测基线使用 `NATS_ASYNC_RAW_WORKERS=128``FAST_WRITER_WORKERS=16``FAST_WRITER_FETCH_WAIT_MS=5`
## 告警阈值建议
| 信号 | 建议阈值 | 含义 |
@@ -130,28 +264,82 @@ go run ./cmd/load-sim \
| `vehicle_gateway_connection_closes_total{reason="read_error"}` | 连续增长 | 入口 TCP 读失败,优先查网络、客户端断连和内核连接状态。 |
| `vehicle_gateway_connection_closes_total{reason="extract_error"}` | 连续增长 | 报文边界或协议提取异常,优先抽查 raw 日志和协议 extractor。 |
| `vehicle_gateway_connection_closes_total{reason="read_timeout"}` | 突然高于历史基线 | 车辆长时间无上报或链路空闲超时,需结合在线数和上游平台状态判断。 |
| `vehicle_gateway_frames_total{status!="OK"}` | 连续 2 分钟增长 | 解析器或上游报文质量异常。 |
| `vehicle_gateway_frame_duration_ms_histogram_bucket` | p99 连续 5 分钟超过容量目标 | Gateway parse、identity resolve、publish enqueue 或响应链路变慢。 |
| `vehicle_async_sink_queue_depth{sink="nats"}` | 持续增长且不回落 | Gateway 到 NATS/Kafka 的异步 publish 队列开始积压。 |
| `vehicle_async_sink_enqueue_total{status="timeout"}` | 任意增长 | Gateway publish 队列已满或 worker 长时间阻塞,入口可能开始丢实时性。 |
| `vehicle_gateway_frames_total{status!="OK"}` | 单协议样本达到 `capacity-check -gateway-parse-min-frames` 后,异常比例超过 `capacity-check -gateway-parse-max-bad-ratio` | 解析器或上游报文质量异常。优先抽查 raw 日志、协议 extractor、最近部署的解析规则和上游转发内容。 |
| `vehicle_gateway_frame_duration_ms_histogram_bucket` | p99 超过 `capacity-check -gateway-frame-p99-ms`连续 5 分钟超过容量目标 | Gateway parse、identity resolve、publish enqueue 或响应链路变慢。 |
| `vehicle_gateway_response_total{status="build_error"}` / `status="write_error"` | 单协议响应尝试样本达到 `capacity-check -gateway-response-min-samples` 后,错误比例超过 `capacity-check -gateway-response-max-error-ratio` | 协议响应构造或 TCP 写回失败。优先查 32960 登录/实时 ACK、808 注册/通用 ACK、连接断开、响应编码和上游是否提前断链。 |
| `vehicle_gateway_response_e2e_recent_p99_ms` | 单协议样本达到 `capacity-check -histogram-min-samples` 后超过 `capacity-check -gateway-response-recent-p99-ms`,默认 `100ms` | 收帧、解析、内存身份映射、RAW 入队或协议写回变慢。结合 identity、async queue 和 NATS 指标定位具体阶段。 |
| `vehicle_gateway_authentication_total{mode="observe",status!="accepted"}` | 任意增长 | 当前登录/认证与配置不一致但仍放行。先核对账号覆盖、上游迁移密码和 JT808 认证码,确认无误后才能切换 `enforce`。 |
| `vehicle_gateway_authentication_total{mode="enforce",status!="accepted"}` | 任意增长 | 网关已返回协议失败并关闭该连接;检查是否为未授权来源、过期账号或配置错误。 |
| `vehicle_gateway_publish_total{kind="fields"}` | 某协议 `raw` 达到 `capacity-check -gateway-field-min-raw` 但 fields 直接发布或 `delegated` 都为 0 | Gateway 收到实时帧但没有可供 bridge 派生的 `parsed_fields`,优先查协议解析和字段映射。 |
| `vehicle_gateway_fields_total{status="skipped_non_realtime"}` | 持续增长但 fields publish 正常 | Gateway 正确跳过登录、注册、鉴权、心跳等非实时帧,不会进入统计 topic如果实时数据也断流再查上游是否只在发控制帧。 |
| `vehicle_gateway_fields_total{status="skipped_missing_fields"}` / `publish_error` | 单协议达到 `capacity-check -gateway-field-min-raw` 后,缺失比例超过 `capacity-check -gateway-field-max-missing-ratio` | 实时帧没有生成扁平字段或兼容直发模式发布失败。NATS canonical raw 模式优先查协议字段映射和 `parsed_fields`。 |
| `vehicle_gateway_publish_total{kind="raw"}` / `vehicle_gateway_publish_total{kind="fields",status="delegated"}` | 某协议某 kind 达到 `capacity-check -gateway-bridge-min-publishes` 但 bridge 无对应 Kafka topic 写入 | Gateway 已接受 raw/fields 派生任务,但 bridge 没有写 Kafka。优先查 bridge projection、route、NATS subject、Kafka write 和 bridge 日志。 |
| `vehicle_gateway_publish_total{kind="raw"}` | 某协议 raw 达到 `capacity-check -gateway-fast-writer-min-raw-publishes` 但 fast-writer 无同 raw subject `ok` | Gateway 已经发布到 NATS但 Redis 快路径未消费或未成功写入/ack。优先查 `vehicle_fast_writer_messages_total``vehicle_fast_writer_stage_duration_ms_histogram_bucket{stage="redis"}``stage="ack"`、NATS durable pending 和 Redis 健康。 |
| `vehicle_gateway_last_frame_unix_seconds` / `vehicle_gateway_last_publish_unix_seconds` | 已出现过的成功 activity 超过 `capacity-check -last-activity-stale-seconds` 未刷新 | 对应协议入口或 publish 已停止活动,先对比连接数、上游转发状态和 bridge last 指标。 |
| `vehicle_gateway_identity_total{status!="resolved"}` | 单协议样本达到 `capacity-check -gateway-identity-min-samples` 后,异常比例超过 `capacity-check -gateway-identity-max-unresolved-ratio` | VIN/车牌/phone 映射质量下降。优先查 `vehicle_identifier` 导入、`vehicle_identity_binding` 基础车牌/VIN、`jt808_registration` phone 状态和 resolver 缓存 TTL。 |
| `vehicle_gateway_identity_total{status="timeout"}` | 任意持续增长 | Gateway 身份解析超过 `IDENTITY_RESOLVE_TIMEOUT_MS`,帧会继续 partial/unresolved 下发,但 VIN/车牌映射可能滞后。 |
| `vehicle_gateway_identity_duration_ms_histogram_bucket` | p99 超过 `capacity-check -gateway-identity-p99-ms` 或接近 `IDENTITY_RESOLVE_TIMEOUT_MS` | MySQL/缓存身份解析开始拖慢入口链路,优先查 `vehicle_identifier``vehicle_identity_binding``jt808_registration` 索引和 RDS 状态。 |
| `vehicle_gateway_identity_cache_entries` | 超过 `capacity-check -gateway-identity-cache-entries-max`,默认 `300000` | Gateway 身份解析缓存超过容量预期。优先确认 `IDENTITY_LOOKUP_CACHE_MAX_ENTRIES``IDENTITY_LOOKUP_CACHE_TTL_SECONDS``IDENTITY_LOOKUP_CACHE_CLEANUP_INTERVAL_SECONDS`,并检查上游是否出现大量异常 phone、source IP 或历史回放。 |
| `vehicle_gateway_identity_snapshot_ready` / `vehicle_gateway_identity_snapshot_last_success_unix_seconds` | ready 不是 `1`,或最近成功刷新超过 `180s` | Gateway 会继续接入并保留 raw但新 808 phone 可能暂时无法映射 VIN。检查 RDS 连通性和四张身份来源表;恢复后周期刷新会自动发布新快照,不需要重启。 |
| `vehicle_durable_outbox_backlog_records` / `vehicle_durable_outbox_inflight` | 持续非 0 或超过容量门禁 | Gateway 已持久接受但尚未收到 JetStream PubAck。短时突发可自动回落持续增长时检查 NATS 可用性、磁盘延迟、ACK timeout 和 inflight 上限。 |
| `vehicle_durable_outbox_publish_total{status=~"submit_error|ack_error|remove_error|close_timeout"}` | 任意增长 | WAL 后的异步发布、PubAck、分段删除或关闭等待失败。记录不会因提交/ACK 失败丢失;修复依赖后由 replay 自动恢复。 |
| `vehicle_async_sink_queue_depth{sink="nats"}` | 持续增长且不回落,或超过 `capacity-check -async-sink-queue-depth-max`,默认 `10000` | Gateway 到 NATS/Kafka 的异步 publish 队列开始积压。结合 `vehicle_async_sink_queue_capacity` 判断是否接近满队列NATS 入队等待由 `NATS_ASYNC_ENQUEUE_TIMEOUT_MS` 控制Kafka fallback 由 `KAFKA_ASYNC_ENQUEUE_TIMEOUT_MS` 控制。 |
| `vehicle_async_sink_queue_wait_recent_p99_ms` | 样本达到 `capacity-check -histogram-min-samples` 后超过 `-async-sink-queue-wait-recent-p99-ms`,默认 `100ms` | 秒级突发曾造成排队,即使当前 queue depth 已回到 0 也会被最近 512 样本窗口捕获。结合 `vehicle_async_sink_workers`、publish duration 和 NATS pending 判断是 worker 不足还是持久化确认变慢。 |
| `vehicle_async_sink_enqueue_total{status="timeout"}` | 任意增长 | Gateway publish 队列已满或 worker 长时间阻塞,入口会快速失败并记录 publish error避免连接处理 goroutine 长时间堆积。优先查 NATS/Kafka 写入延迟、async worker 数、队列容量和 bridge/fast-writer pending。 |
| `vehicle_async_sink_publish_total{status="error"}` | 连续增长 | NATS/Kafka publish 失败,需要先查中间件连接和日志。 |
| `vehicle_async_sink_publish_duration_ms_histogram_bucket` | p99 连续 5 分钟上升 | Gateway async worker 写 NATS/Kafka 变慢,通常会带动 queue depth 增长。 |
| `vehicle_async_sink_publish_duration_ms_histogram_bucket` | p99 超过 `capacity-check -async-sink-p99-ms`连续 5 分钟上升 | Gateway async worker 写 NATS/Kafka 变慢,通常会带动 queue depth 增长。 |
| `vehicle_history_batch_flush_total{status="error"}` | 任意增长 | TDengine 批写失败Kafka offset 不会提交,应先查 TDengine 和 SQL 错误。 |
| `vehicle_history_batch_pending_messages` | 持续非 0 或 burst 后不回落 | history-writer 已拉取但未完成写入/提交,可能卡在 TDengine 或 Kafka commit。 |
| `vehicle_history_batch_pending_rows` | 持续非 0 或 burst 后不回落 | TDengine 有批量写入积压,通常早于 Kafka lag 放大。 |
| `vehicle_history_batch_flush_duration_ms{status="ok"}` | 持续上升 | TDengine 写入延迟增加,可能需要降低 batch size 或扩容 TDengine。 |
| `vehicle_history_parsed_fields_total{status="missing"}` | 单 topic/protocol 实时帧样本达到 `capacity-check -history-parsed-field-min-frames` 后,缺失比例超过 `capacity-check -history-parsed-field-max-missing-ratio` | history-writer 收到的实时 raw envelope 没有携带预计算 `parsed_fields`TDengine RAW 证据层会缺少扁平化解析字段。优先查 gateway `BuildFieldsEnvelope` / raw envelope 构造、协议字段映射和最近部署。 |
| `vehicle_history_batch_pending_messages` | 超过 `capacity-check -history-batch-pending-max` 或 burst 后不回落 | history-writer 已拉取但未完成写入/提交,可能卡在 TDengine 或 Kafka commit。 |
| `vehicle_history_batch_pending_rows` | 超过 `capacity-check -history-rows-pending-max` 或 burst 后不回落 | TDengine 有批量写入积压,通常早于 Kafka lag 放大。 |
| `vehicle_history_batch_flush_duration_ms_histogram_bucket{status="ok"}` | p99 超过 `capacity-check -history-flush-p99-ms` 或持续上升 | TDengine 写入延迟增加,可能需要降低 batch size 或扩容 TDengine。 |
| `vehicle_history_kafka_messages_total{status="invalid_json"}` | 超过 `capacity-check -history-invalid-json-max`,默认 `0` | Kafka raw topic 中出现无法反序列化的 envelope。history-writer 会 commit 并隔离该消息,避免阻塞历史落库;必须检查 NATS bridge 写入 Kafka 的 payload、raw topic 是否混入非 envelope 数据,以及 gateway 发布合同。 |
| `vehicle_bridge_nats_consumer_ack_pending` | 连续 2 分钟 `> 0` | 消息已投递给 bridge但 Kafka 写入后未完成 ack。 |
| `vehicle_bridge_nats_consumer_pending` | 持续增长且 `> 10000` | bridge 消费 NATS 的速度跟不上生产速度。 |
| `vehicle_bridge_batch_pending_messages` | 持续非 0 或 burst 后不回落 | bridge 已拉取 NATS 消息但尚未完成 Kafka 写入和 NATS ack。 |
| `vehicle_bridge_batch_duration_ms_histogram_bucket` | p99 连续 5 分钟上升 | Kafka 写入或 NATS ack 开始变慢,通常会先于 ack-pending 扩大。 |
| `vehicle_fast_writer_nats_consumer_ack_pending` | 连续 2 分钟 `> 0` | 消息已投递给 fast-writer但 TDengine/Redis 写入后未完成 ack。 |
| `vehicle_bridge_batch_duration_ms_histogram_bucket` | p99 超过 `capacity-check -bridge-batch-p99-ms`连续 5 分钟上升 | Kafka 写入或 NATS ack 开始变慢,通常会先于 ack-pending 扩大。 |
| `vehicle_bridge_fields_projection_total` | `invalid_json``invalid_envelope``protocol_mismatch``skipped_missing_fields``marshal_error` 增长 | canonical raw 无法派生统计 fields。raw 仍进入历史链路,但每日里程可能缺样本;按协议检查 Gateway `parsed_fields` 和字段合同。 |
| `vehicle_bridge_messages_total{status="route_error"}` / `vehicle_bridge_nats_acks_total{status="dropped_route_error"}` | route_error 超过 `capacity-check -bridge-route-error-max`,默认 `0` | NATS stream 中出现 bridge 未配置的 subject。bridge 会 ACK 并丢弃该消息,避免 poison message 阻塞正常 subject必须检查 `NATS_STREAM_SUBJECTS``NATS_FILTER` 和 raw/fields subject 到 Kafka topic 的路由配置。 |
| `vehicle_bridge_kafka_writes_total{topic="vehicle.fields.go.*"}` | 某 fields topic 达到 `capacity-check -stat-topic-min-bridge-writes` 但 stat-writer 无同 topic consumer 或 received | fields 已进 Kafka但统计消费配置或消费循环断层。 |
| `vehicle_bridge_kafka_writes_total{topic="vehicle.raw.go.*"}` | 某 raw topic 达到 `capacity-check -raw-fanout-min-bridge-writes` 但 history/realtime 无同 topic received/write/update | raw 已进 Kafka但历史落库或当前态投影断层。先查对应服务 `vehicle_kafka_consumer_info`,再查 `vehicle_history_kafka_messages_total` / `vehicle_history_writes_total` / `vehicle_realtime_kafka_messages_total` / `vehicle_realtime_updates_total`。 |
| `vehicle_bridge_last_message_unix_seconds` / `vehicle_bridge_last_kafka_write_unix_seconds` / `vehicle_bridge_last_ack_unix_seconds` | 已出现过的成功 activity 超过 `capacity-check -last-activity-stale-seconds` 未刷新 | NATS 到 Kafka 桥接某 subject/topic 停止流动;先查 NATS pending、Kafka 写入和 ack。 |
| `vehicle_fast_writer_nats_consumer_ack_pending` | 超过 `100` 或持续不回落 | 消息已投递给 fast-writer但 Redis 写入后未完成 ack短暂个位数/十几条通常只是高频流量里的 NATS ack 飞行窗口,如果显式启用 TDengine stage也可能卡在 TDengine。 |
| `vehicle_fast_writer_nats_consumer_pending` | 持续增长且 `> 10000` | fast-writer 消费 NATS 的速度跟不上入口写入速度。 |
| `vehicle_fast_writer_batch_pending_messages` | 持续非 0 或 burst 后不回落 | fast-writer 已拉取 NATS 消息但尚未完成 TDengine/Redis 写入和 ack。 |
| `vehicle_fast_writer_batch_pending_messages` | 超过 `capacity-check -fast-writer-batch-pending-max` 或 burst 后不回落 | fast-writer 已拉取 NATS 消息但尚未完成 Redis 写入和 ack;如果显式启用 TDengine stage也可能卡在 TDengine。 |
| `vehicle_fast_writer_batch_pending_envelopes` | 持续非 0 或 burst 后不回落 | fast-writer 当前批次已有有效 envelope 在等待落库或 ack。 |
| `vehicle_fast_writer_stage_duration_ms_histogram_bucket` | 某个 stage 的 p99 连续 5 分钟上升 | NATS 快速写链路在 TDengine、Redis 或 NATS ack 某一阶段变慢;TDengine 阶段可小步调整 `FAST_WRITER_TDENGINE_MAX_OPEN_CONNS`Redis 阶段是整批 pipeline 写入耗时,调整后必须观察是否出现选库错误和 ack-pending 增长。 |
| `vehicle_realtime_store_update_duration_ms_histogram_bucket{store="redis"}` | p99 连续 5 分钟上升 | Redis 实时投影变慢,会直接影响 realtime consumer 追平能力。 |
| `vehicle_realtime_store_update_duration_ms_histogram_bucket{store="mysql"}` | p99 连续 5 分钟上升 | MySQL 当前态/位置投影变慢,需结合 async queue depth 和 dropped 计数判断。 |
| `vehicle_stat_write_duration_ms_histogram_bucket` | p99 连续 5 分钟上升 | MySQL 每日里程统计写入变慢,可能导致 stat Kafka lag 增长。 |
| `vehicle_fast_writer_stage_duration_ms_histogram_bucket` | 某个 stage 的 p99 超过 `capacity-check -fast-writer-stage-p99-ms`连续 5 分钟上升 | NATS 快速写链路在 Redis 或 NATS ack 某一阶段变慢;生产默认 `FAST_WRITER_TDENGINE_ENABLED=false`,如果临时启用 TDengine stage再观察 `stage="tdengine"`。 |
| `vehicle_fast_writer_messages_total{status="invalid_json"}` | 超过 `capacity-check -fast-writer-invalid-json-max`,默认 `0` | NATS raw subject 中出现无法反序列化的 envelope。fast-writer 会 ACK 并隔离该消息,避免阻塞 Redis 当前态;必须检查 gateway 发布 payload、NATS stream 是否混入非 envelope 数据,以及 bridge/fast-writer 的 subject 过滤配置。 |
| `vehicle_fast_writer_redis_envelopes_total{status="skipped_non_realtime"}` | 持续增长但 `updated` 正常 | raw topic 中存在登录、注册、鉴权等非实时帧Redis 当前态会跳过它们,不代表污染当前态;如果 `updated=0` 且只有 skip 增长,应检查 gateway 是否停止发送实时帧。 |
| `vehicle_fast_writer_redis_envelopes_total{status="skipped_missing_vin"}` / `skipped_missing_vehicle_key` | 持续增长 | 实时帧进入 Redis 快路径但身份解析不完整,当前态不会写入。优先查 identity resolver、`vehicle_identity_binding``jt808_registration`、MQTT VIN 映射和 GB32960 VIN 字段。 |
| `vehicle_fast_writer_redis_envelopes_total{status="skipped_missing_fields"}` | 任意持续增长 | Gateway 发布了实时 RAW但没有附带一次计算后的 `parsed_fields`。RAW 仍写历史Redis 在线/KV 和 MySQL snapshot/location 均跳过;检查协议 parser、字段映射和 Gateway envelope 构造,禁止在 writer 中增加重新解析兜底。 |
| `vehicle_fast_writer_redis_fields_total{status="skipped_stale"}` | 某协议持续增长且占 `status="seen"` 比例异常升高 | Redis 当前态收到大量旧事件时间字段,旧帧不会覆盖新字段,但会影响上游时序质量判断。优先按 `subject` 查上游平台补发、设备时间漂移、NATS backlog 和 gateway event-time 归一化记录。 |
| `vehicle_fast_writer_last_message_unix_seconds` / `vehicle_fast_writer_last_stage_unix_seconds` | 已出现过的成功 activity 超过 `capacity-check -last-activity-stale-seconds` 未刷新 | Redis 快路径某 raw subject 或具体 stage 停止活动。优先对比 gateway last publish、NATS pending、`vehicle_fast_writer_messages_total`、Redis 健康和 `stage="redis"` / `stage="ack"` 的最近时间。 |
| `vehicle_fast_writer_tdengine_enabled` | 生产非 `0` | fast-writer 正在写 TDengine可能与 history-writer 双写 RAW/位置证据层;`capacity-check` 会降级,除压测或故障切换外应保持 `0`。 |
| `vehicle_kafka_consumer_info` | history/stat/realtime 缺失 | 服务进程可能启动了,但 Kafka broker、group 或 topic 配置没有生效。 |
| `vehicle_history_config{setting="workers"}` / `vehicle_history_worker_active` | 配置低于 `-history-workers-min=3` 或任一 worker 不活跃 | 检查 `HISTORY_WORKERS`、Kafka group 分区分配、进程日志和 TDengine 连接承载。 |
| `capacity-check` finding: `required kafka consumer topic missing` | history/realtime/stat 缺少生产合同 topic | 对照 `/opt/lingniu-go-native/env/*.env``KAFKA_TOPICS``KAFKA_TOPIC` 配置。history/realtime 应有三类 raw topicstat 应有三类 fields topic。 |
| `vehicle_realtime_redis_projector_enabled` | 生产非 `0` | realtime-api 正在从 Kafka 重写 Redis可能与 NATS fast-writer 双写当前态;`capacity-check` 会降级,生产应保持 `0`。 |
| `vehicle_realtime_kafka_messages_total{status="invalid_json"}` | 超过 `capacity-check -realtime-invalid-json-max`,默认 `0` | realtime-api 的 Kafka 当前态投影收到无法反序列化的 raw envelope。服务会 commit 并隔离该消息,避免阻塞 Redis/MySQL 当前态;必须检查 bridge 写入 Kafka 的 payload、raw topic 污染和 gateway 发布合同。 |
| `vehicle_realtime_store_update_duration_ms_histogram_bucket{store="redis"}` | 生产出现或 p99 连续 5 分钟上升 | 仅临时启用 realtime Redis projector 时使用;常规 Redis 写入延迟看 `vehicle_fast_writer_stage_duration_ms_histogram_bucket{stage="redis"}`。 |
| `vehicle_realtime_store_update_duration_ms_histogram_bucket{store="mysql"}` | p99 超过 `capacity-check -realtime-store-p99-ms` 或连续 5 分钟上升 | MySQL 当前态/位置投影变慢,需结合 async queue depth 和 dropped 计数判断。 |
| `vehicle_realtime_config{setting="workers"}` / `vehicle_realtime_worker_active` | 配置低于 `-realtime-workers-min=3` 或任一 worker 不活跃 | 检查 `REALTIME_WORKERS`、Kafka group 分区分配和 MySQL 连接/锁等待。 |
| `vehicle_identity_writer_config{setting="workers"}` / `vehicle_identity_writer_worker_active` | 配置低于 `-identity-workers-min=3` 或任一 worker 不活跃 | 检查 `IDENTITY_WRITER_WORKERS`、JT808 raw 分区分配和 MySQL 连接/锁等待。 |
| `vehicle_realtime_async_queue_total{status="dropped"}` / `status="closed"` | 任意增长 | MySQL 当前态/位置投影的异步二级队列没有接收该帧。`dropped` 表示队列满,优先查 MySQL 写入延迟和 `MYSQL_REALTIME_ASYNC_WORKERS``closed` 只应出现在服务退出窗口,退出时会先 drain 已排队数据再关闭 MySQL 连接。 |
| `vehicle_realtime_plate_cache_entries` | 超过 `capacity-check -realtime-plate-cache-entries-max`,默认 `200000` | Realtime API 的 VIN 到车牌缓存超过容量预期。优先确认 `PLATE_CACHE_MAX_ENTRIES``PLATE_CACHE_TTL_SECONDS``vehicle_identity_binding` 的 VIN 质量;若短时间快速增长,通常说明上游出现大量异常 VIN 或历史回放范围过大。 |
| `vehicle_stat_write_duration_ms_histogram_bucket` | p99 超过 `capacity-check -stat-write-p99-ms` 或连续 5 分钟上升 | MySQL 每日里程统计写入变慢,可能导致 stat Kafka lag 增长。 |
| `vehicle_stat_kafka_messages_total{status="invalid_json"}` | 超过 `capacity-check -stat-invalid-json-max`,默认 `0` | Kafka fields topic 中出现无法反序列化的 envelope。stat-writer 会 commit 并隔离该消息,避免阻塞每日里程统计;必须检查 gateway fields 发布、NATS bridge 路由以及是否误把 RAW/非 envelope 数据写入 fields topic。 |
| `vehicle_stat_batch_pending_messages` | 超过 `capacity-check -stat-batch-pending-max`,默认 `1000` | stat-writer 已从 Kafka 拉取一批 fields 消息,但还没完成 MySQL append 和 Kafka commit。短暂非 0 是批处理窗口;持续升高优先查 MySQL 写入延迟、`vehicle_stat_write_duration_ms_histogram_bucket` 和 stat Kafka lag。 |
| `vehicle_stat_cache_entries` / `vehicle_stat_cache_evictions_total` | entries 超过 `capacity-check -stat-cache-entries-max`,默认 `1000000`evictions 持续增长 | stat-writer 的去重、source touch、投影节流、历史基线缓存达到运行时上限。优先确认 `STATS_CACHE_MAX_ENTRIES``STATS_CACHE_RETENTION_HOURS``STATS_CACHE_CLEANUP_INTERVAL_SECONDS`,并检查是否有大范围历史回放、异常 VIN/source 或 Kafka 积压恢复。 |
| `vehicle_stat_samples_total{status="found"}` | 单 topic `vehicle_stat_writes_total{status="ok"}` 达到 `capacity-check -stat-sample-min-writes``found=0` | fields topic 有消费但完全抽不到里程样本,优先检查 VIN 映射、协议字段映射和总里程字段是否进入 fields该指标带 `protocol` 标签,可直接按 GB32960/JT808/YUTONG_MQTT 聚合。 |
| `vehicle_stat_samples_total{status="skipped_missing_fields"}` | 任意持续增长,或单 topic 可行动跳过数 / `vehicle_stat_writes_total{status="ok"}` 超过 `capacity-check -stat-sample-max-actionable-skip-ratio` | fields topic 收到了 envelope 但没有扁平化 `fields`。优先查 gateway `BuildFieldsEnvelope`、NATS/Kafka bridge topic 映射,以及是否误把 RAW envelope 写进 fields topic。 |
| `vehicle_stat_samples_total{status="skipped_missing_vin"}` / `skipped_missing_mileage` / `skipped_non_positive_mileage` / `skipped_missing_source` | 单 topic 可行动跳过数 / `vehicle_stat_writes_total{status="ok"}` 超过 `capacity-check -stat-sample-max-actionable-skip-ratio` | fields topic 有消费但大量样本无法进入每日里程。按协议和跳过原因分别检查 VIN 映射、总里程字段映射、里程单位/异常值、`vehicle_data_source` 关联。 |
| `vehicle_stat_samples_total{status="event_time_future_adjusted"}` | 连续增长或集中在某协议/source | 设备事件时间比接收时间未来超过 10 分钟统计已回退到接收时间。优先检查上游平台或终端时钟RAW 帧仍保留设备原始时间,业务 location、Redis 当前态和每日里程使用归一后的时间。 |
| `vehicle_stat_samples_total{status="written"}` | stat-writer `ok` 增长但 `written` 不增长 | fields topic 有消费但没有有效里程样本入库,按 `skipped_missing_vin``skipped_missing_mileage``skipped_non_positive_mileage``skipped_missing_source``skipped_same_mileage` 定位原因;如果主要是 `skipped_same_mileage`,通常表示上游总里程未变化,不等同故障。 |
| `vehicle_stat_sources_total{status="written"}` / `skipped_throttled` | `vehicle_stat_samples_total{status="found"}` 增长但 source tracking 无增长 | 来源管理链路没有维护 `vehicle_data_source`。优先查 source endpoint 是否为空、`vehicle_data_source` 表结构、MySQL 写入错误和 `STATS_SOURCE_TOUCH_INTERVAL_SECONDS`。 |
| `vehicle_stat_sources_total{status="skipped_missing_endpoint"}` | 单 topic 缺 endpoint / found 超过 `capacity-check -stat-source-max-missing-ratio` | fields envelope 缺 `source_endpoint`,无法区分平台来源和维护 source。优先查 Gateway 是否填充 TCP remote/MQTT endpoint以及 NATS/Kafka bridge 是否保留 envelope 字段。 |
| `vehicle_stat_projections_total{status="written"}` / `skipped_throttled` | source 候选持续写入但最终日里程更新频率低 | `vehicle_daily_mileage_source` 是每样本候选事实层,`vehicle_daily_mileage` 是最终投影层。`skipped_throttled` 增长通常表示受 `STATS_PROJECT_INTERVAL_SECONDS` 节流,不代表样本丢失;如需强一致核验可临时设投影间隔为 `0`。 |
| `vehicle_history_last_*_unix_seconds` / `vehicle_stat_last_*_unix_seconds` / `vehicle_realtime_last_*_unix_seconds` | 已出现过的成功 activity 超过 `capacity-check -last-activity-stale-seconds` 未刷新 | 下游消费、写入或 commit 某 topic 停止活动;结合 Kafka lag 和对应存储健康判断。 |
| `vehicle_stat_project_interval_seconds` | 生产异常为 `0` 或被调得过小 | `vehicle_daily_mileage_source` 仍每样本更新,但 `vehicle_daily_mileage` 选举投影会被节流;过小会放大 MySQL 压力。 |
| Kafka lag | 连续 5 分钟增长或 `> 10000` | 下游 consumer 或存储存在瓶颈。 |
| Writer 成功计数 | 入口增长但 writer 不增长 | bridge、Kafka、consumer 或存储链路断开。 |
@@ -162,7 +350,7 @@ go run ./cmd/load-sim \
1. 先确认监听端口。
```bash
ss -lntp | grep -E ':(808|32960|20200|20211|20212|20213|20214) '
ss -lntp | grep -E ':(808|32960|20200|20211|20212|20213|20214|20215|20216) '
```
2. 检查 gateway readiness 和日志。
@@ -218,21 +406,129 @@ df -h /
当前建议值:`NATS_STREAM_MAX_BYTES=21474836480`,即 `20GiB``NATS_STREAM_ENSURE_TIMEOUT_SECONDS=60`,避免大 stream 元数据更新时被 NATS 客户端默认 5s 超时误杀。Kafka topic 只作为短期缓冲,当前建议保留 `6h`,不要把 Kafka 或 NATS 当长期历史存储;长期历史和 RAW 查询以 TDengine/MySQL 投影为准。
fast-writer 的 `FAST_WRITER_OPERATION_TIMEOUT_MS` 建议为 `1000`实时链路仍以 100ms 级为目标,但 TDengine 批量写存在尾延迟,过小的超时会造成 NATS 消息反复重投和重复写压力。
fast-writer 的 `FAST_WRITER_OPERATION_TIMEOUT_MS` 建议为 `1000`生产默认设置 `FAST_WRITER_TDENGINE_ENABLED=false`,让 fast-writer 只写 Redis 实时投影TDengine RAW/位置由 history-writer 通过 Kafka raw topic 单路写入;如果临时启用 TDengine stage,过小的超时会造成 NATS 消息反复重投和重复写压力。
生产 Redis 当前态只由 `nats-fast-writer` 写入;`realtime-writer` 应设置 `REALTIME_ROLE=writer``REALTIME_REDIS_PROJECTOR_ENABLED=false`,继续从 Kafka raw 写 MySQL snapshot/location`realtime-api` 应设置 `REALTIME_ROLE=api`,只提供 Redis/MySQL/TDengine 查询 API。若 `capacity-check` 提示 `realtime redis projector enabled`,应先检查 `/opt/lingniu-go-native/env/realtime-writer.env`
stat-writer 默认 `STATS_WORKERS=3``STATS_PROJECT_INTERVAL_SECONDS=15``vehicle_daily_mileage_source` 每条有效里程样本都会更新,用于保留多源事实和最新总里程;`vehicle_daily_mileage` 是对外查询结果层,会按来源优先级/质量从 source 表投影,投影被节流以降低高频帧下的 MySQL 写放大。需要故障回放或强一致核验时可临时设为 `0` 恢复每样本投影,处理完成后应调回正常值。
`vehicle_data_source.latest_seen_at` 表示平台来源最后一次被本系统接收到的时间stat-writer 优先使用 `received_at_ms` 更新,且 SQL 层禁止该字段被补发、乱序或设备时间异常的帧回退。里程统计日期仍按设备事件时间计算,只有事件时间超出接收时间 `10min` 以上时才回退到接收时间。
`vehicle_realtime_snapshot.access_*` 是车辆/协议级接入证据,由 realtime writer 在快照同一条原子 upsert 中维护。`access_latest_received_at` 只按更大的 `received_at_ms` 且不同 event ID 推进,同时把旧值写入 `access_previous_received_at`,计算 `access_report_interval_ms` 并增加 `access_sample_count`;设备事件时间乱序不影响此路径。迁移 `008` 将存量行标为 `snapshot_backfill`,该时间只是上线基线,不得对外描述为历史首次接入。验证上线时至少观察同一 VIN 的样本数递增、间隔非负、首次时间不变,并确认重复/补发帧没有回退最新接收时间。
### Raw 有数据但实时查不到
1. 查 realtime Kafka lag。
1. 查 realtime writer Kafka lag。
2. 查 realtime update 计数。
3. 查 realtime API readiness。
4. 查最新 snapshot/location API。
```bash
curl -fsS http://127.0.0.1:20216/readyz
curl -fsS http://127.0.0.1:20200/readyz
curl -fsS 'http://127.0.0.1:20200/api/realtime/locations?limit=1'
curl -fsS 'http://127.0.0.1:20200/api/realtime/snapshots?limit=1'
```
### 更新 JT808 平台映射
多平台 808 车牌/手机号映射统一进入 `vehicle_identifier`,不要再直接把不同平台的手机号硬塞进 `vehicle_identity_binding` 的唯一列。导入器会先读取映射文件,再用旧 `vehicle_identity_binding` 的车牌/手机号反查 VIN只写入可确定 VIN 的记录;冲突和未匹配记录留在 JSON 报告里人工处理。
```bash
release=$(basename "$(readlink -f /opt/lingniu-go-native/current)")
input=/opt/lingniu-go-native/imports/${release}/jt808_mapping
# 先 dry-run确认 scan.sources、resolved / unresolved / conflicts。
/opt/lingniu-go-native/current/identity-import \
-input "${input}" \
-ensure-schema \
-unresolved-out "/tmp/${release}-identity-unresolved.csv" \
-conflicts-out "/tmp/${release}-identity-conflicts.csv" \
-timeout 5m > /tmp/${release}-identity-dryrun.json
jq '.scan.sources' /tmp/${release}-identity-dryrun.json
jq '.scan.unsupported_items // []' /tmp/${release}-identity-dryrun.json
jq '.source_results' /tmp/${release}-identity-dryrun.json
# dry-run 无冲突后再 apply。
/opt/lingniu-go-native/current/identity-import \
-input "${input}" \
-apply \
-sync-data-sources \
-timeout 5m > /tmp/${release}-identity-apply.json
# 只同步来源表时可以不传 input不带 -apply 只输出候选/跳过数量,不写库。
/opt/lingniu-go-native/current/identity-import \
-sync-data-sources \
-timeout 1m > /tmp/${release}-source-sync-dryrun.json
/opt/lingniu-go-native/current/identity-import \
-sync-data-sources \
-apply \
-timeout 1m > /tmp/${release}-source-sync-apply.json
```
导入后检查:
- `scan.sources` 是否覆盖全部平台目录;重点看每个 `source_code``phone_records``plate_records``skipped`,如果某个平台记录数异常低,先处理源文件表头/列位置,不要直接 apply。
- `scan.unsupported_items` 必须为空;如果出现 `.xls``.xlsb`,先转成 `.xlsx` 再导入,避免某个平台文件被跳过后造成 VIN 映射缺口。`identity-import -apply` 会在连接 MySQL/建表前拒绝带 unsupported 文件的目录。
- `source_results` 是否逐平台呈现合理的 `resolved/unresolved/conflicts`;如果某个平台 `unresolved` 很高,先补 `vehicle_identity_binding` 的 VIN/车牌基础事实;如果 `conflicts` 不为 `0`,先人工确认同一平台下手机号/车牌是否重复指向不同车辆。
- 同一平台同一手机号/车牌重复出现但不冲突时,导入器会合并非空字段,优先保留可用于 VIN 解析的车牌;如果同一标识对应多个不同车牌,会进入 conflict不会静默覆盖。
- `identity-import -apply` 会跳过 unresolved/conflict 项,只写入可确定 VIN 且无冲突的标识;实际写入包在单个 MySQL 事务里,任一写入错误都会回滚本次导入。
- `vehicle_identifier` 总数和各 `source_code` 分布是否符合文件规模。
- `vehicle_data_source.source_code/platform_name/source_kind` 会从 `jt808_registration.phone/source_ip``vehicle_identifier` 推断;同一来源 IP 推断出多个平台时会跳过,且不会覆盖人工维护的平台名。
- 来源维护 API 支持 `sourceCodeMissing=true` 快速列出未绑定平台编码的来源,例如:`/api/stats/data-sources?protocol=JT808&sourceCodeMissing=true&includeTotal=true`
- 来源类型使用 `source_kind` 维护:`PLATFORM` 表示稳定平台源,`DIRECT` 表示车辆直连或动态 IP`UNKNOWN` 表示未分类。每日里程最终投影在同等质量下按 `PLATFORM -> UNKNOWN -> DIRECT` 选择来源,然后再比较 `trust_priority`、样本数和最新时间。
- 每日里程 API 会通过 `source_id` 关联 `vehicle_data_source` 返回选中来源证据,例如:`/api/stats/daily-metrics?protocol=JT808&dateFrom=2026-07-12&dateTo=2026-07-12&limit=1`。排查异常里程时同时核对 `source_id``source_ip``platform_name``source_code``source_kind``latest_total_mileage_km`,不要只看 `daily_mileage_km`
- 每日里程候选来源 API 用于审计多源选举,例如:`/api/stats/daily-metrics/sources?protocol=JT808&dateFrom=2026-07-12&dateTo=2026-07-12&selected=true&limit=1`。它返回 `source_key``phone``sample_count``first_total_mileage_km``latest_total_mileage_km``quality_status``quality_reason``is_selected`,用于确认每个来源独立算出的里程以及最终是否被选中。`first_event_time` 可能是跨日前推得到的历史基线时间,排查时应结合 `quality_reason` 理解。
- 每日里程诊断 API 用于排查“实时在线但统计缺失”,例如:`/api/stats/daily-metrics/diagnostics?protocol=YUTONG_MQTT&date=2026-07-12&diagnosis=NO_SOURCE_SAMPLE&includeTotal=true`。它会从 `vehicle_realtime_snapshot/location` 找当天活跃车辆,再关联最终表和候选来源表输出 `diagnosis``OK` 表示最终日里程已存在;`MISSING_DAILY` 表示候选来源已有样本但最终投影缺失;`NO_SOURCE_SAMPLE` 表示当天确实收到总里程字段但 stat-writer 没抽到候选样本;`NO_TOTAL_MILEAGE` 表示当天实时活跃但没有收到总里程字段。宇通 MQTT 可能稀疏上报,`vehicle_realtime_location.total_mileage_km` 会保留旧值,判断当天是否真实上报要看 `total_mileage_event_time`
- `NO_TOTAL_MILEAGE``reason` 会进一步说明源头问题:`realtime_total_mileage_missing` 表示当前态没有总里程;`realtime_total_mileage_non_positive` 表示源头总里程为 0 或负数;`realtime_total_mileage_time_missing` 表示当前态有总里程旧值但缺少该字段的上报时间证据;`realtime_total_mileage_not_reported_on_stat_date` 表示当前态保留了旧总里程,但指定业务日期没有重新上报总里程字段。
- `vehicle_realtime_snapshot.parsed_json` 是稀疏帧合并后的当前态,字段存在只证明历史上曾上报,不能证明当前帧或统计日上报。字段诊断返回 `mapped_protocol_field_without_fresh_evidence` 时应按 `raw_frame_query_path` 核对原始帧;只有 `vehicle_realtime_location.total_mileage_event_time` 落在统计日内,才把标准总里程视为新鲜证据。
- 每日里程诊断汇总 API 用于大屏和告警入口,例如:`/api/stats/daily-metrics/diagnostics/summary?date=2026-07-12`。它按协议返回 `active_count``ok_count``missing_daily_count``no_source_sample_count``no_total_mileage_count``actionable_issue_count`;健康状态下应优先关注 `actionable_issue_count=0`,异常时再下钻到诊断明细接口。
- 每日里程诊断原因汇总 API 用于告警聚合和运营看板,例如:`/api/stats/daily-metrics/diagnostics/reasons?date=2026-07-12`。它按 `protocol + diagnosis + reason` 返回车辆数;需要只看某类问题时可传 `diagnosis=NO_TOTAL_MILEAGE`,比拉取逐车明细更适合高频轮询。
- `stats-backfill` 用于从 TDengine RAW 证据层补算 MySQL 每日里程。ECS 上直接执行会默认加载 `/opt/lingniu-go-native/env/base.env``/opt/lingniu-go-native/env/stat-writer.env`;默认 `BACKFILL_DRY_RUN=true`,不会写库。补算采用 `BACKFILL_METHOD=last_diff`对每个协议、VIN、来源分别取当日最新累计总里程和同一来源在目标日前最近一次累计总里程`当日最新 - 最近历史总里程` 计算。补算窗口首日会聚合目标日前的历史并取 `LAST(event_time)`因此前一日缺失时会继续向更早日期寻找历史完全为空时才使用当天第一条作基线。JT808 历史补算结束后会按 `source_code` 归并平台来源键,保证历史与实时统计使用同一来源。正式写入前先 dry-run例如
```bash
BACKFILL_PROTOCOLS=YUTONG_MQTT \
BACKFILL_DATE_FROM=2026-07-12 \
BACKFILL_DATE_TO=2026-07-12 \
/opt/lingniu-go-native/current/stats-backfill
BACKFILL_DRY_RUN=false \
BACKFILL_PROTOCOLS=GB32960,JT808,YUTONG_MQTT \
BACKFILL_DATE_FROM=2026-07-12 \
BACKFILL_DATE_TO=2026-07-12 \
/opt/lingniu-go-native/current/stats-backfill
```
生产定时补算使用 systemd timer默认每天 01:30 补算昨天往前 3 天,覆盖上游晚到或断传后恢复的总里程字段:
```bash
cd go/vehicle-gateway
scripts/install-stats-backfill-timer.sh \
--host root@115.29.187.205 \
--days-back 1 \
--window-days 3 \
--on-calendar '*-*-* 01:30:00'
systemctl list-timers lingniu-go-stats-backfill.timer --no-pager
systemctl cat lingniu-go-stats-backfill.service lingniu-go-stats-backfill.timer --no-pager
```
如果只想验证定时任务配置不写库,可以加 `--dry-run` 安装;正式生产不加 `--dry-run`,服务内会设置 `BACKFILL_DRY_RUN=false`。timer 设置 `Persistent=false`,避免首次安装时立刻执行当天已错过的计划;即使 ECS 短暂停机,下一次 3 天窗口也会补到遗漏日期。
- 诊断 API 判断“指定日期活跃”时会同时看 `event_time``received_at``updated_at`,任一时间落在业务日期内即纳入;这样设备时间漂移或未来时间不会遮蔽服务端当天实际收到的数据。
- 每日里程质量规则会把 1km 内的小幅负漂移按 0km 处理,`quality_reason=negative_jitter_clamped`。优先用前一自然日同来源末值前一日缺失时继续向前查找最近历史日末值并按“2500km × 两个累计里程端点相隔自然日数”放大物理合理性上限,但仍把完整差值记在当前统计日,不平均分摊或伪造中间日期。该上限覆盖车辆以约 100km/h 连续运行 24 小时的极端场景,同时继续拒绝约 4000km/日的异常跳变。超过窗口上限的正向突增或超过 1km 的倒退是 `INVALID_DELTA`,不会进入最终 `vehicle_daily_mileage`;历史完全为空时使用当天第一条。如果设备事件时间比接收时间未来超过 10 分钟业务层会统一使用接收时间TDengine RAW 帧仍保留设备原始时间作为证据。
- 来源诊断 API 支持按来源 IP 汇总注册手机号和 `vehicle_identifier` 匹配情况,例如:`/api/stats/data-sources/diagnostics?protocol=JT808&sourceCodeMissing=true&includeTotal=true``reason=no_identifier_match` 表示该来源下手机号还没有进入 `vehicle_identifier``reason=ambiguous_source_code` 表示同一来源 IP 匹配到多个平台编码,需要人工确认;`reason=candidate_available` 表示可以同步或确认候选 `source_code`
- 来源类型建议 API 是只读辅助,例如:`/api/stats/data-sources/kind-suggestions?protocol=JT808&sourceKind=UNKNOWN&includeTotal=true`。它会输出 `suggested_source_kind``suggestion_confidence``suggestion_reason`,用于人工确认后再 PATCH `source_kind`;不要把建议结果无审核地批量写回。
- `source_code` 用于程序稳定识别来源,`platform_name` 用于展示和人工修正;如果两者看起来不一致,优先核对该来源 IP 下的 `jt808_registration.phone` 是否来自同一平台,再决定是否人工修正平台名。
- Gateway 默认启用 `IDENTITY_SOURCE_CODE_LOOKUP_ENABLED=true`。808 VIN 解析会先用 `source_endpoint -> vehicle_data_source.source_code``vehicle_identifier` 精确匹配,精确未命中时再回退到全局 `vehicle_identifier`、旧 `vehicle_identity_binding``jt808_registration`,因此多平台同一手机号/车牌映射不同 VIN 时,应优先维护正确的 `vehicle_data_source.source_code`
- `jt808_registration``vin = 'unknown'` 的手机号是否仍能在 `vehicle_identifier` 中匹配;如果能匹配,等待下一帧 808 位置或注册/鉴权帧触发自动更新。
- Gateway `vehicle_gateway_identity_total{protocol="JT808",status="resolved"}` 应持续增长。
Gateway 默认启用 `IDENTITY_SNAPSHOT_ONLY_ENABLED=true`,启动时并每隔 `IDENTITY_SNAPSHOT_REFRESH_INTERVAL_SECONDS=60` 秒把 `vehicle_data_source``vehicle_identifier`、旧 `vehicle_identity_binding``jt808_registration` 原子载入内存。快照包含 JT808 的 VIN、设备、车牌和最近可信鉴权码每帧身份解析及鉴权均不查询 MySQL。注册/鉴权/位置触达由独立 identity-writer 投影写入 `jt808_registration`,人工改库或新鉴权码最多等待一个刷新周期即可生效。快照刷新失败保留上一版,启动时 RDS 不可用也不阻止接入;未知 phone 会暂时 unresolvedRDS 恢复并完成刷新后自动解析。
## 重启顺序
优先重启最小故障层: