# 车辆接入生产运行手册

## 范围

本文覆盖 ECS `115.29.187.205` 上的 Go 原生车辆接入链路：

- GB32960 TCP 接入
- JT/T 808 TCP 接入
- 宇通 MQTT 接入
- NATS 到 Kafka 桥接
- Kafka 历史、统计、实时消费者
- Redis 实时缓存、MySQL 实时表、TDengine 历史表

运行手册的目标是按自上而下的顺序定位问题：入口、队列、桥接、消费、存储、查询 API。

当前生产服务、topic、表和 Redis key 的清单见 [生产数据面清单](../architecture/production-data-plane-inventory.md)。

10W 车辆容量目标、当前缺口和压测口径见 [100K 车辆接入容量基线](100k-capacity-baseline.md)。

## 服务地图

| 层级 | 服务 | systemd 单元 | 本机端点 |
| --- | --- | --- | --- |
| 接入 | 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 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 配置基线

生产环境的 env 文件必须和 Go topic 命名空间保持一致：

| 服务 | 必要 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.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` 只在兼容开关打开时使用 |

Gateway 在 NATS 模式下设置 `FIELDS_DERIVE_FROM_RAW_ENABLED=true`，每帧只发布 canonical raw；bridge 设置 `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 lag；worker 不应超过 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` 的定时巡检口径保持一致。

## 五分钟排查顺序

先回答最核心的问题：数据有没有进来，有没有排队，有没有被消费，最后有没有被查询到。

1. 检查所有服务 `/readyz`。
2. 检查 gateway 帧计数和 TCP 活跃连接。
3. 检查 NATS bridge 的 pending 和 ack-pending。
4. 检查 bridge 写 Kafka 与 NATS ack 是否同时增长。
5. 检查 history、stat、realtime、identity 四类 Kafka consumer lag。
6. 检查各 writer 的写入、提交、更新计数。
7. 最后再查存储和业务查询 API。

```bash
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|last_frame|publish_total|last_publish)|vehicle_async_sink'

curl -fsS http://127.0.0.1:20214/metrics \
  | 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: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=''` 关闭。

## 健康基线

当前生产环境的健康特征：

- 所有 `/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 不持续扩大；统计链路还应看到 `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 触发：

```bash
systemctl status lingniu-go-capacity-check.timer
journalctl -u lingniu-go-capacity-check.service --since '10 minutes ago' --no-pager
```

如果 `lingniu-go-capacity-check.service` 失败，先看 journal JSON 里的 `findings`，再按对应层级处理。

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` 用于阶段性连接和帧写入压测。压测生产入口前必须先确认上游真实数据窗口，避免和业务流量混淆。

```bash
cd /opt/lingniu-go-native/current/go/vehicle-gateway

go run ./cmd/load-sim \
  -protocol jt808 \
  -addr 127.0.0.1:808 \
  -connections 100 \
  -connect-rate 100 \
  -send-interval 10s \
  -duration 2m \
  -template 0200 \
  -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 并行写 Kafka；RAW 和 fields 均成功后才 ACK NATS 源消息。当前生产压测基线使用 `NATS_ASYNC_RAW_WORKERS=128`、`FAST_WRITER_WORKERS=16`、`FAST_WRITER_FETCH_WAIT_MS=5`。

## 告警阈值建议

| 信号 | 建议阈值 | 含义 |
| --- | --- | --- |
| `/readyz` 非 ok | 立即处理 | 服务或依赖不可用。 |
| Gateway 活跃连接 | 预期有流量时某协议降为 `0` | 上游网络、监听端口或进程可能异常。 |
| `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"}` | 单协议样本达到 `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 超过 `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_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_location_writes_total{status="error"}` / `vehicle_history_last_location_write_unix_seconds{status="error"}` | 累计错误超过 `-history-location-error-max`，且最后一次错误距今不超过 `-history-location-error-recent-seconds`（默认 `300s`） | TDengine 位置派生表最近仍有写入失败。优先查异常设备时间、坐标投影和 TDengine 时间范围；错误窗口恢复后健康状态自动恢复，累计 counter 仍用于审计。缺少最后错误时间时保持降级，避免观测缺失被误判为恢复。 |
| `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 超过 `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` | 超过 `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 超过 `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 topic，stat 应有三类 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 或存储链路断开。 |

## 事故处理路径

### 没有新数据

1. 先确认监听端口。

```bash
ss -lntp | grep -E ':(808|32960|20200|20211|20212|20213|20214|20215|20216) '
```

2. 检查 gateway readiness 和日志。

```bash
curl -fsS http://127.0.0.1:20211/readyz
journalctl -u lingniu-go-gateway.service --since '10 minutes ago' --no-pager
```

3. 看 gateway 帧计数是否增长。
4. 如果 gateway 有增长但 bridge 没增长，查 NATS publish 和 bridge 日志。
5. 如果 bridge 写入增长但 writer 不增长，查 Kafka lag 和 consumer 日志。

### Kafka lag 持续增长

1. 先从 metrics 定位是哪个服务、哪个 topic、哪个 partition。
2. 检查对应服务 `/readyz`。
3. 检查存储依赖：history 看 TDengine，stat 看 MySQL，realtime 看 Redis/MySQL/TDengine。
4. 先看日志，再决定是否重启。

```bash
journalctl -u lingniu-go-history-writer.service --since '10 minutes ago' --no-pager
journalctl -u lingniu-go-stat-writer.service --since '10 minutes ago' --no-pager
journalctl -u lingniu-go-realtime-api.service --since '10 minutes ago' --no-pager
```

Kafka 是可回放日志。只要 Kafka 还保留消息，恢复 consumer 后 lag 应该能自动追平。不要在 lag 未归零前手工补数。

### NATS pending 持续增长

1. 检查 bridge `/readyz` 和日志。
2. 对比 `vehicle_bridge_kafka_writes_total` 和 `vehicle_bridge_nats_acks_total`。
3. 如果 Kafka 写入失败，检查 ECS 到 Kafka broker 的网络。
4. 如果 `ack_pending > 0`，优先检查 Kafka 是否监听 `9092`、Kafka 盘是否打满，再看 bridge 日志。
5. 如果 `ack_pending = 0` 但 `consumer_pending` 下降，说明 bridge 正在追历史积压；不要反复重启，持续观察下降速度即可。
6. 如果 `consumer_pending` 不下降，才考虑扩容 bridge 或降低 batch/fetch 等配置。

```bash
journalctl -u lingniu-go-nats-kafka-bridge.service --since '10 minutes ago' --no-pager
systemctl restart lingniu-go-nats-kafka-bridge.service
```

生产 stream 必须设置字节上限，避免 NATS JetStream 在 Kafka 或下游故障时吃满根盘：

```bash
grep NATS_STREAM_MAX_BYTES /opt/lingniu-go-native/env/nats-fast-writer.env
grep NATS_STREAM_MAX_BYTES /opt/lingniu-go-native/env/nats-kafka-bridge.env
grep NATS_STREAM_ENSURE_TIMEOUT_SECONDS /opt/lingniu-go-native/env/nats-fast-writer.env
grep NATS_STREAM_ENSURE_TIMEOUT_SECONDS /opt/lingniu-go-native/env/nats-kafka-bridge.env
du -sh /opt/lingniu-nats/data
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`。生产默认设置 `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 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 会暂时 unresolved，RDS 恢复并完成刷新后自动解析。

## 重启顺序

优先重启最小故障层：

1. 只有一个 consumer lag 异常时，先重启对应 writer。
2. 实时投影或 API 异常时，重启 realtime API。
3. bridge pending 或 ack-pending 卡住时，重启 NATS Kafka bridge。
4. 只有入口监听、解析循环或上游连接处理异常时，才重启 gateway。

```bash
systemctl restart lingniu-go-history-writer.service
systemctl restart lingniu-go-stat-writer.service
systemctl restart lingniu-go-realtime-api.service
systemctl restart lingniu-go-nats-kafka-bridge.service
systemctl restart lingniu-go-gateway.service
```

排查前不要直接全量重启。全量重启会抹掉时间线证据，也会掩盖问题到底发生在入口、队列、存储还是查询层。

## 存储原则

运行指标保留在 `/metrics`，不要为了服务健康计数新增 MySQL 或 TDengine 业务表。

业务存储保持最小化：

- raw frames 保存完整 parsed JSON，用于回放和审计。
- realtime snapshot/location 只保存 API 需要的当前业务字段。
- history 表保存可查询的时间序列核心字段。
- 派生统计应能从 Kafka 或 raw history 重新计算。

只有当产品查询明确需要、并且指标定义稳定时，才新增持久化聚合。
