Files
lingniu-vehicle-ingest/docs/ops/vehicle-ingest-runbook.md
2026-07-16 06:48:47 +08:00

65 KiB
Raw Blame History

车辆接入生产运行手册

范围

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

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

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

当前生产服务、topic、表和 Redis key 的清单见 生产数据面清单

10W 车辆容量目标、当前缺口和压测口径见 100K 车辆接入容量基线

服务地图

层级 服务 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.v1vehicle.raw.go.jt808.v1vehicle.raw.go.yutong-mqtt.v1
lingniu-go-stat-writer.service vehicle.fields.go.gb32960.v1vehicle.fields.go.jt808.v1vehicle.fields.go.yutong-mqtt.v1
lingniu-go-realtime-writer.service vehicle.raw.go.gb32960.v1vehicle.raw.go.jt808.v1vehicle.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 fieldsvehicle.event.go.unified.v1 只在兼容开关打开时使用

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这是在追补历史 backlogvehicle_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=3REALTIME_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 differentfields 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 TCP0.0.0.0:32960
  • JT/T 808 TCP0.0.0.0:808
  • 实时 writer127.0.0.1:20216
  • 身份 writer127.0.0.1:20217
  • 实时/API0.0.0.0:20200

协议鉴权配置

Gateway 的鉴权策略分为 disabledobserveenforce。生产新增或变更账号时先使用 observe,确认 vehicle_gateway_authentication_total 中只有 accepted 后再切换 enforce

GB32960 多平台账号使用仅 root 可读的 JSON 文件,不把密码提交到 Git 或写入 systemd unit

{
  "platform-a": "current-password",
  "platform-b": ["migration-old-password", "current-password"]
}

gateway.env 只配置文件地址和模式:

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 混用:

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=0vehicle_durable_outbox_inflight=0,且 submittedacked 最终一致。submit_errorack_errorremove_errorclose_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 结果。仓库提供统一脚本:

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)

脚本会构建 gatewayhistory-writerstat-writeridentity-writerrealtime-apinats-fast-writernats-kafka-bridgefields-projectorcapacity-checkload-simstats-backfillidentity-import,并逐个校验 file 输出包含 ELF 64-bitx86-64。正式发布按 bridge、fields projector、下游 writer、Gateway 的依赖顺序重启,确保 canonical raw 到 fields 的派生能力先于入口切换生效;发布窗口暂停 lingniu-go-capacity-check.timer,发布后恢复 timer、检查 9 个 systemd 服务、9 个 /readyzcapacity-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。
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_totalactionable_issue_totalitems。二进制默认 -daily-mileage-diagnostics-max-actionable=-1 表示只展示不阻断;生产定时巡检和发布门禁应显式设置一个阈值,例如当前已知 MQTT 稀疏总里程期间可先用 50,待问题清理后收紧到 0。诊断接口不可访问会降级,因为这表示 MySQL/API 观测链路不可用;临时环境可用 -daily-mileage-diagnostics-url='' 关闭。

健康基线

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

  • 所有 /readyz 都返回 status=ok
  • vehicle_bridge_nats_consumer_ack_pending0
  • 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_connectionsvehicle_gateway_connection_closes_total 为准。

定时容量巡检由 systemd timer 触发:

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_secondsgateway 看 vehicle_gateway_last_frame_unix_secondsvehicle_gateway_last_publish_unix_secondsbridge 看 vehicle_bridge_last_kafka_write_unix_secondsfast-writer 看 vehicle_fast_writer_last_message_unix_secondsvehicle_fast_writer_last_stage_unix_secondshistory/stat/realtime 分别看对应 last_messagelast_write/updatelast_commit。如果 counter 很高但 last 时间长时间不变,说明对应协议或 topic 已经停止活动。capacity-check JSON 会额外派生 *_last_*_age_seconds,现场排查优先看 age 是否持续扩大。

压测入口

Go 版本提供 cmd/load-sim 用于阶段性连接和帧写入压测。压测生产入口前必须先确认上游真实数据窗口,避免和业务流量混淆。

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 时必须读取服务端应答,并使用隔离手机号区间和自动清理:

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=0write_errors=0response_bytes>0read_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=128FAST_WRITER_WORKERS=16FAST_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_totalvehicle_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_identifiervehicle_identity_bindingjt808_registration 索引和 RDS 状态。
vehicle_gateway_identity_cache_entries 超过 capacity-check -gateway-identity-cache-entries-max,默认 300000 Gateway 身份解析缓存超过容量预期。优先确认 IDENTITY_LOOKUP_CACHE_MAX_ENTRIESIDENTITY_LOOKUP_CACHE_TTL_SECONDSIDENTITY_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
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_fieldsTDengine 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_jsoninvalid_envelopeprotocol_mismatchskipped_missing_fieldsmarshal_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_SUBJECTSNATS_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_bindingjt808_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/*.envKAFKA_TOPICSKAFKA_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_WORKERSclosed 只应出现在服务退出窗口,退出时会先 drain 已排队数据再关闭 MySQL 连接。
vehicle_realtime_plate_cache_entries 超过 capacity-check -realtime-plate-cache-entries-max,默认 200000 Realtime API 的 VIN 到车牌缓存超过容量预期。优先确认 PLATE_CACHE_MAX_ENTRIESPLATE_CACHE_TTL_SECONDSvehicle_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,默认 1000000evictions 持续增长 stat-writer 的去重、source touch、投影节流、历史基线缓存达到运行时上限。优先确认 STATS_CACHE_MAX_ENTRIESSTATS_CACHE_RETENTION_HOURSSTATS_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-writesfound=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_vinskipped_missing_mileageskipped_non_positive_mileageskipped_missing_sourceskipped_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. 先确认监听端口。
ss -lntp | grep -E ':(808|32960|20200|20211|20212|20213|20214|20215|20216) '
  1. 检查 gateway readiness 和日志。
curl -fsS http://127.0.0.1:20211/readyz
journalctl -u lingniu-go-gateway.service --since '10 minutes ago' --no-pager
  1. 看 gateway 帧计数是否增长。
  2. 如果 gateway 有增长但 bridge 没增长,查 NATS publish 和 bridge 日志。
  3. 如果 bridge 写入增长但 writer 不增长,查 Kafka lag 和 consumer 日志。

Kafka lag 持续增长

  1. 先从 metrics 定位是哪个服务、哪个 topic、哪个 partition。
  2. 检查对应服务 /readyz
  3. 检查存储依赖history 看 TDenginestat 看 MySQLrealtime 看 Redis/MySQL/TDengine。
  4. 先看日志,再决定是否重启。
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_totalvehicle_bridge_nats_acks_total
  3. 如果 Kafka 写入失败,检查 ECS 到 Kafka broker 的网络。
  4. 如果 ack_pending > 0,优先检查 Kafka 是否监听 9092、Kafka 盘是否打满,再看 bridge 日志。
  5. 如果 ack_pending = 0consumer_pending 下降,说明 bridge 正在追历史积压;不要反复重启,持续观察下降速度即可。
  6. 如果 consumer_pending 不下降,才考虑扩容 bridge 或降低 batch/fetch 等配置。
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 或下游故障时吃满根盘:

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,即 20GiBNATS_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=writerREALTIME_REDIS_PROJECTOR_ENABLED=false,继续从 Kafka raw 写 MySQL snapshot/locationrealtime-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=3STATS_PROJECT_INTERVAL_SECONDS=15vehicle_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。
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 报告里人工处理。

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_codephone_recordsplate_recordsskipped,如果某个平台记录数异常低,先处理源文件表头/列位置,不要直接 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_ipvehicle_identifier 推断;同一来源 IP 推断出多个平台时会跳过,且不会覆盖人工维护的平台名。
  • 来源维护 API 支持 sourceCodeMissing=true 快速列出未绑定平台编码的来源,例如:/api/stats/data-sources?protocol=JT808&sourceCodeMissing=true&includeTotal=true
  • 来源类型使用 source_kind 维护:PLATFORM 表示稳定平台源,DIRECT 表示车辆直连或动态 IPUNKNOWN 表示未分类。每日里程最终投影在同等质量下按 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_idsource_ipplatform_namesource_codesource_kindlatest_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_keyphonesample_countfirst_total_mileage_kmlatest_total_mileage_kmquality_statusquality_reasonis_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 找当天活跃车辆,再关联最终表和候选来源表输出 diagnosisOK 表示最终日里程已存在;MISSING_DAILY 表示候选来源已有样本但最终投影缺失;NO_SOURCE_SAMPLE 表示当天确实收到总里程字段但 stat-writer 没抽到候选样本;NO_TOTAL_MILEAGE 表示当天实时活跃但没有收到总里程字段。宇通 MQTT 可能稀疏上报,vehicle_realtime_location.total_mileage_km 会保留旧值,判断当天是否真实上报要看 total_mileage_event_time
  • NO_TOTAL_MILEAGEreason 会进一步说明源头问题: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_countok_countmissing_daily_countno_source_sample_countno_total_mileage_countactionable_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例如
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 天,覆盖上游晚到或断传后恢复的总里程字段:

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_timereceived_atupdated_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=truereason=no_identifier_match 表示该来源下手机号还没有进入 vehicle_identifierreason=ambiguous_source_code 表示同一来源 IP 匹配到多个平台编码,需要人工确认;reason=candidate_available 表示可以同步或确认候选 source_code
  • 来源类型建议 API 是只读辅助,例如:/api/stats/data-sources/kind-suggestions?protocol=JT808&sourceKind=UNKNOWN&includeTotal=true。它会输出 suggested_source_kindsuggestion_confidencesuggestion_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_codevehicle_identifier 精确匹配,精确未命中时再回退到全局 vehicle_identifier、旧 vehicle_identity_bindingjt808_registration,因此多平台同一手机号/车牌映射不同 VIN 时,应优先维护正确的 vehicle_data_source.source_code
  • jt808_registrationvin = '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_sourcevehicle_identifier、旧 vehicle_identity_bindingjt808_registration 原子载入内存。快照包含 JT808 的 VIN、设备、车牌和最近可信鉴权码每帧身份解析及鉴权均不查询 MySQL。注册/鉴权/位置触达由独立 identity-writer 投影写入 jt808_registration,人工改库或新鉴权码最多等待一个刷新周期即可生效。快照刷新失败保留上一版,启动时 RDS 不可用也不阻止接入;未知 phone 会暂时 unresolvedRDS 恢复并完成刷新后自动解析。

重启顺序

优先重启最小故障层:

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

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