# 100K 车辆接入容量基线

更新时间：2026-07-13

## 目标

支撑 100,000 台车辆接入，入口服务在高连接数和高帧率下保持可观测、可背压、可恢复。

## 初始容量假设

| 项 | 目标 |
| --- | --- |
| 连接车辆数 | 100,000 |
| 平均帧间隔 | 10-30 秒 |
| 平均入口 FPS | 3,000-10,000 |
| 短时 burst FPS | 20,000 |
| Gateway p99 parse + enqueue | < 50ms |
| Redis 当前态延迟 | < 3s |
| TDengine 历史写入 lag | 可观测且 burst 后下降 |
| MySQL 当前态 | 异步，不能阻塞 Redis |

## 当前生产观测

当前 ECS `115.29.187.205` 在低负载下健康：

- JT808 连接约 242。
- GB32960 连接约 2。
- Kafka lag 为 0。
- NATS ack pending 为 0。
- Redis 写入约 0ms。
- MySQL 投影约 1-2ms。
- ECS load 约 `0.09 / 0.15 / 0.21`。
- 可用内存约 6.6GB。
- 根分区使用约 37%。

## 当前已知缺口

- `net.core.somaxconn=128`，无法作为 100K 连接生产基线。
- Gateway 默认 `TCP_MAX_CONNECTIONS` 已调整为 `120000`，生产仍可通过环境变量覆盖。
- 已新增可重复的 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 参数建议

100K 连接目标需要至少以下系统参数作为起点：

```bash
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.ip_local_port_range="10000 65000"
sysctl -w net.ipv4.tcp_tw_reuse=1
```

systemd gateway 已配置 `LimitNOFILE=1048576`，需要持续保持。

## 验收指标

| 层 | 指标 | 目标 |
| --- | --- | --- |
| Gateway | `vehicle_gateway_active_connections` | 能稳定到阶段目标连接数 |
| Gateway | `vehicle_gateway_connection_closes_total{reason}` | `read_error/extract_error/max_connections` 不异常增长 |
| Gateway | `vehicle_gateway_connection_rejections_total` | 容量目标内不增长 |
| Gateway async sink | `vehicle_async_sink_queue_depth{sink}` | 队列深度不持续增长 |
| Gateway async sink | `vehicle_async_sink_enqueue_total{sink,kind,status}` | `timeout/closed` 不增长 |
| Gateway async sink | `vehicle_async_sink_publish_total{sink,kind,status}` | `error` 不增长 |
| Gateway async sink | `vehicle_async_sink_publish_duration_ms_histogram_bucket` | p99 不持续上升 |
| Gateway | `vehicle_gateway_publish_total{status="ok"}` | 持续增长 |
| Gateway | `vehicle_gateway_frame_duration_ms_histogram_bucket` | p99 小于容量目标 |
| History writer | `vehicle_history_batch_flush_total{status}` | `error` 不增长 |
| History writer | `vehicle_history_batch_rows_total{status}` | batch 行数持续增长 |
| History writer | `vehicle_history_batch_pending_messages` | 稳态接近 0，burst 后下降 |
| History writer | `vehicle_history_batch_pending_rows` | 稳态接近 0，burst 后下降 |
| 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` | 稳态接近 0，burst 后下降 |
| NATS bridge | `vehicle_bridge_batch_duration_ms_histogram_bucket` | p99 不持续上升 |
| NATS fast writer | `vehicle_fast_writer_nats_consumer_ack_pending` | 稳态为 0 |
| NATS fast writer | `vehicle_fast_writer_nats_consumer_pending` | burst 后下降 |
| NATS fast writer | `vehicle_fast_writer_batch_pending_messages` | 稳态接近 0，burst 后下降 |
| NATS fast writer | `vehicle_fast_writer_batch_pending_envelopes` | 稳态接近 0，burst 后下降 |
| NATS fast writer | `vehicle_fast_writer_stage_duration_ms_histogram_bucket` | TDengine/Redis/ack 各阶段 p99 不持续上升 |
| Kafka consumers | `vehicle_*_kafka_lag` | 稳态为 0，burst 后下降 |
| 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

10W 车辆接入时，中间件只承担解耦和短期缓冲职责，不能成为长期历史库。

| Component | Guardrail |
| --- | --- |
| 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` |
| 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 下降斜率。

## 分阶段压测

压测入口：

```bash
# 100 连接 smoke test
/opt/lingniu-go-native/current/load-sim \
  -protocol jt808 \
  -addr 127.0.0.1:808 \
  -connections 100 \
  -connect-rate 100 \
  -send-interval 10s \
  -duration 2m \
  -template 0200 \
  -send=false
```

`load-sim` 在 `-send=true` 时会按连接和帧序号生成可被现有解析器解析的唯一协议帧：

- 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 数据，只能在明确隔离标识和压测窗口后执行。

1. 100 连接 smoke test。
2. 10,000 连接保持测试。
3. 10,000 连接 + 1 FPS/连接短测。
4. 50,000 连接保持测试。
5. 100,000 连接保持测试。
6. 按真实协议分布进行混合帧率测试。

每一阶段必须记录：

- active connections
- gateway frame/publish counters
- NATS pending 和 ack pending
- Kafka lag
- Redis/MySQL/TDengine 写入耗时和 pending depth
- CPU/load/memory/file descriptors
- 错误日志

## 2026-07-03 阶段压测记录

压测方式：在 ECS 本机使用 `/opt/lingniu-go-native/current/load-sim` 对 `127.0.0.1:808` 发起 JT808 hold-only 连接，`-send=false`，不发送业务帧，不污染 raw 数据。

压测前基线，时间 `2026-07-03 18:03:10 CST`：

| 项 | 值 |
| --- | --- |
| 服务状态 | 6 个 Go systemd 服务均 active |
| JT808 连接 | 约 240 |
| GB32960 连接 | 约 2 |
| load average | `0.43 / 0.22 / 0.23` |
| available memory | 约 `6661 MB` |
| NATS ack pending | `0` |
| NATS pending | 约 `39` |
| history/stat/realtime Kafka lag | `0` |

阶段结果：

| 阶段 | 时间 | 参数 | 峰值连接/FD | 结果 | 资源和队列 |
| --- | --- | --- | --- | --- | --- |
| 100 连接 | `18:03:21-18:04:21 CST` | `connections=100 connect-rate=100 duration=60s send=false` | JT808 active `340`，`ss` 约 `341` | opened `100`，failed `0`，frames `0`，write_errors `0` | load 约 `0.33/0.21/0.22`，available memory 约 `6659 MB` |
| 1,000 连接 | `18:04:38-18:05:38 CST` | `connections=1000 connect-rate=500 duration=60s send=false` | JT808 active `1240`，`ss` 约 `1241`，gateway FD 约 `1256` | opened `1000`，failed `0`，frames `0`，write_errors `0` | load 约 `0.80/0.33/0.26`，available memory 约 `6645 MB` |
| 10,000 连接 | `18:05:59-18:07:59 CST` | `connections=10000 connect-rate=1000 duration=120s send=false` | JT808 active `10240`，`ss` 约 `10241`，gateway FD 约 `10256` | opened `10000`，failed `0`，frames `0`，write_errors `0` | 20 秒时 load 约 `0.45/0.33/0.27`，available memory 约 `6409 MB`，NATS ack pending `0` |
| 50,000 连接 | `18:11:22-18:14:22 CST` | `connections=50000 connect-rate=2000 duration=180s send=false` | JT808 active `50238`，`ss` 约 `50239`，gateway FD 约 `50254` | opened `50000`，failed `0`，frames `0`，write_errors `0` | 45 秒时 load 约 `0.07/0.23/0.24`，available memory 约 `5590 MB`，NATS ack pending 短时 `29`，后续归零 |
| 100,000 总连接 | `18:18:13-18:21:13 CST` | JT808 `50000` + GB32960 `50000`，各 `connect-rate=2000 duration=180s send=false` | JT808 active `50236`，GB32960 active `50002`，总 active 约 `100238`，gateway FD 约 `100252` | JT808 opened `50000` / failed `0`；GB32960 opened `50000` / failed `0`；frames `0`，write_errors `0` | 75 秒时 load 约 `0.16/0.30/0.29`，available memory 约 `4511 MB`，NATS ack pending 短时 `10`，后续归零 |

压测后：

- `2026-07-03 18:08:47 CST` JT808 active 回落到 `240`，`ss` 约 `241`。
- `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/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 并行写 Kafka，concurrency=6 | 压测中段 | 约 24ms | 约 207ms | 约 311ms | 约 409ms | 约 247ms |
| topic 并行写 Kafka，concurrency=6 | 压测结束 | 约 29ms | 约 184ms | 约 288ms | 约 368ms | 约 316ms |

并行版本在同一 NATS 批次内按 Kafka topic 分组并发写，RAW 与派生 fields 全部成功后才 ACK 原 NATS 消息；任一 topic 失败仍保留对应源消息待重放。低负载 Bridge p99 从约 110ms 降到约 65ms；1000 FPS 下 history p99 下降约 14%，Bridge RAW p99 下降约 12%。压测中段 NATS pending 短时峰值 71，Gateway 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 delivery；NATS/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、读错误 0，WAL `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 连接被强制中断的预期结果，不代表已持久接受的服务端数据丢失。
