Files
lingniu-vehicle-ingest/docs/ops/100k-capacity-baseline.md

228 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` | 稳态接近 0burst 后下降 |
| History writer | `vehicle_history_batch_pending_rows` | 稳态接近 0burst 后下降 |
| History writer | `vehicle_history_batch_flush_duration_ms{status}` | flush 延迟不持续上升 |
| History writer | `vehicle_history_config{setting="workers"}` | 不低于 `3` |
| History writer | `vehicle_history_worker_active` | 每个配置 worker 均为 `1` |
| NATS bridge | `vehicle_bridge_nats_consumer_ack_pending` | 稳态为 0 |
| NATS bridge | `vehicle_bridge_nats_consumer_pending` | burst 后下降 |
| NATS bridge | `vehicle_bridge_batch_pending_messages` | 稳态接近 0burst 后下降 |
| 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` | 稳态接近 0burst 后下降 |
| NATS fast writer | `vehicle_fast_writer_batch_pending_envelopes` | 稳态接近 0burst 后下降 |
| NATS fast writer | `vehicle_fast_writer_stage_duration_ms_histogram_bucket` | TDengine/Redis/ack 各阶段 p99 不持续上升 |
| Kafka consumers | `vehicle_*_kafka_lag` | 稳态为 0burst 后下降 |
| 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 并行写 Kafkaconcurrency=6 | 压测中段 | 约 24ms | 约 207ms | 约 311ms | 约 409ms | 约 247ms |
| topic 并行写 Kafkaconcurrency=6 | 压测结束 | 约 29ms | 约 184ms | 约 288ms | 约 368ms | 约 316ms |
并行版本在同一 NATS 批次内按 Kafka topic 分组并发写RAW 与派生 fields 全部成功后才 ACK 原 NATS 消息;任一 topic 失败仍保留对应源消息待重放。低负载 Bridge p99 从约 110ms 降到约 65ms1000 FPS 下 history p99 下降约 14%Bridge RAW p99 下降约 12%。压测中段 NATS pending 短时峰值 71Gateway async queue 短时 285结束时均归零Kafka lag 和各 writer retry 均为 0。主机为 4 核,瞬时 load average 约 9.68,但采样时 CPU 仍约 52% idle、无 D 状态进程、可用内存约 6.4 GiB。
增加队列等待指标后,使用五个全新 JT808 号段进行了 worker A/B每轮仍是 59,500 帧、发送/读取错误为 0、清理注册 1000 条。以下均为压测中段最近 512 样本窗口,秒级集中发送会比均匀流量更严格:
| Gateway RAW worker | Fast writer worker / fetch | Gateway queue wait p99 | Gateway 应答 p99 | Redis p99 | 结论 |
| ---: | --- | ---: | ---: | ---: | --- |
| 32 | `8 / 20ms` | 约 95ms | 约 92ms | 约 234ms | 入口突发排队明显 |
| 64 | `8 / 20ms` | 约 84ms | 约 79ms | 约 254ms | Gateway 有小幅收益,下游未改善 |
| 128 | `16 / 5ms` | 约 42ms | 约 17ms | 约 151ms | 当前最佳组合 |
| 128 | `32 / 5ms` | 约 50ms | 约 32ms | 约 221ms | pull worker 过多产生调度竞争,回退 |
最终保留 `NATS_ASYNC_RAW_WORKERS=128``FAST_WRITER_WORKERS=16``FAST_WRITER_FETCH_WAIT_MS=5`。Redis 批写自身 p99 低于 `5ms`,约 151ms 的剩余尾延迟主要位于 JetStream 持久化发布和 consumer deliveryNATS/Kafka pending、Kafka lag 与 writer retry 在每轮结束后均归零。
## 2026-07-14 WAL Outbox 验证
Gateway 已切换为分段 WAL + 异步 JetStream PubAck。WAL 使用 16MiB/5s 分段、1ms/256 条 group commit、`fsync=true`、10000 最大 inflight本地 Apple M4 并发基准约 1926 records/s`519144 ns/op`),没有以关闭 `fsync` 换取吞吐。
ECS 生产使用 JT808 `0x0200`、1000 个连接、每连接 1 FPS、持续 60 秒验证:连接成功 1000、失败 0发送 59500 帧、写错误 0、读错误 0WAL `submitted=acked=66776`(含同期真实三协议流量),结束后 WAL backlog/inflight、NATS pending、Kafka lag 全部为 0。压测结束 Gateway JT808 响应 recent p99 约 80ms。
随后在约 1000 FPS 下对 Gateway 执行真实 `SIGKILL`。systemd 约 5 秒后自动拉起;启动后立即从崩溃前 WAL 重放并确认约 563 条记录,最终 WAL backlog/inflight、NATS pending 和 history/stat/realtime/identity Kafka lag 均回到 0日志没有 CRC 损坏、publish error 或 panic。故障窗口内压测客户端的写/读错误是 TCP 连接被强制中断的预期结果,不代表已持久接受的服务端数据丢失。