diff --git a/README.md b/README.md index 0b85bc0f..aa96d531 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ | 框架 | Spring Boot 3.5.3 | | 网络 | Netty 4.2.9.Final | | 并发 | Disruptor 4 + 虚拟线程 + `StructuredTaskScope` | -| 消息队列 | **Kafka**(vin 分区,保证单车有序) | +| 事件流 | **Kafka**(唯一生产消息通道,vin 分区,保证单车有序) | | 线上消息格式 | **Protobuf**,调试走 JSON | | MQTT 客户端 | Eclipse Paho 1.2.5 | | 会话索引 | Redis | @@ -96,7 +96,8 @@ Redis 最新状态查询不在默认 Maven reactor 中;需要时使用 `-Popti ## 核心原则 1. **接入与业务落库分离**:协议接入应用只负责收、解析、校验、规整和投递 Kafka;`vehicle-history-app` 写入 TDengine 历史与 RAW JSON,`vehicle-analytics-app` 将 JT808 `daily_mileage_km` 写入 MySQL `vehicle_stat_metric` -2. **协议即插拔**:每个 `protocol-*` / `inbound-*` 都可独立开关(`lingniu.ingest..enabled`) -3. **顺序保证**:同一 VIN 严格有序(Disruptor hash + Kafka 分区 key) -4. **幂等消费**:Envelope 带 `eventId`,下游去重 -5. **原始可回放**:协议接入会把 `rawArchiveKey/rawArchiveUri` 追加到事件 metadata;Kafka Envelope、TDengine `raw_frames` 和导出都使用 `archive://...` 逻辑 URI 追溯原始 bytes,实际文件由 `sink-archive` 管理 +2. **Kafka 唯一消息通道**:生产链路不提供 RabbitMQ、RocketMQ 或其他 MQ 后端;接入、历史、统计统一通过 Kafka Envelope 解耦 +3. **协议即插拔**:每个 `protocol-*` / `inbound-*` 都可独立开关(`lingniu.ingest..enabled`) +4. **顺序保证**:同一 VIN 严格有序(Disruptor hash + Kafka 分区 key) +5. **幂等消费**:Envelope 带 `eventId`,下游去重 +6. **原始可回放**:协议接入会把 `rawArchiveKey/rawArchiveUri` 追加到事件 metadata;Kafka Envelope、TDengine `raw_frames` 和导出都使用 `archive://...` 逻辑 URI 追溯原始 bytes,实际文件由 `sink-archive` 管理 diff --git a/docs/operations/vehicle-ingest-tdengine-verification.md b/docs/operations/vehicle-ingest-tdengine-verification.md index e2ccbfb1..5ae43ca1 100644 --- a/docs/operations/vehicle-ingest-tdengine-verification.md +++ b/docs/operations/vehicle-ingest-tdengine-verification.md @@ -56,6 +56,8 @@ java -jar modules/apps/jt808-ingest-app/target/jt808-ingest-app.jar ```bash curl -sS http://127.0.0.1:20400/actuator/health +curl -sS http://127.0.0.1:20400/actuator/health/liveness +curl -sS http://127.0.0.1:20400/actuator/health/readiness lsof -nP -iTCP:808 -sTCP:LISTEN ``` @@ -74,6 +76,8 @@ java -jar modules/apps/gb32960-ingest-app/target/gb32960-ingest-app.jar ```bash curl -sS http://127.0.0.1:20100/actuator/health +curl -sS http://127.0.0.1:20100/actuator/health/liveness +curl -sS http://127.0.0.1:20100/actuator/health/readiness lsof -nP -iTCP:32960 -sTCP:LISTEN ``` @@ -90,6 +94,8 @@ java -jar modules/apps/vehicle-history-app/target/vehicle-history-app.jar ```bash curl -sS http://127.0.0.1:20200/actuator/health +curl -sS http://127.0.0.1:20200/actuator/health/liveness +curl -sS http://127.0.0.1:20200/actuator/health/readiness curl -sS http://127.0.0.1:20200/v3/api-docs \ | grep -E '/api/event-history/locations|/api/event-history/raw-frames' ``` @@ -127,8 +133,14 @@ JT808 注册身份绑定: ```bash curl -sS http://127.0.0.1:20400/actuator/health +curl -sS http://127.0.0.1:20400/actuator/health/liveness +curl -sS http://127.0.0.1:20400/actuator/health/readiness curl -sS http://127.0.0.1:20100/actuator/health +curl -sS http://127.0.0.1:20100/actuator/health/liveness +curl -sS http://127.0.0.1:20100/actuator/health/readiness curl -sS http://127.0.0.1:20200/actuator/health +curl -sS http://127.0.0.1:20200/actuator/health/liveness +curl -sS http://127.0.0.1:20200/actuator/health/readiness ``` Swagger: diff --git a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java index 0009ac89..593eddac 100644 --- a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java +++ b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/MavenModuleProfileTest.java @@ -170,6 +170,7 @@ class MavenModuleProfileTest { .contains("Netty " + projectProperty(pom, "netty.version")) .contains("Eclipse Paho " + projectProperty(pom, "paho-mqtt.version")) .contains("session-core/ 设备会话 + 鉴权 + Token + Redis SessionStore") + .contains("| 事件流 | **Kafka**(唯一生产消息通道,vin 分区,保证单车有序) |") .contains("protocol-jt1078/ JT/T 1078(optional-command-gateway profile") .contains("protocol-jsatl12/ 苏标主动安全报警附件(optional-attachments profile)") .contains("vehicle-state-service/ Kafka 全字段事件消费 + Redis 热状态查询(optional-latest-state profile)") @@ -177,6 +178,7 @@ class MavenModuleProfileTest { .contains("vehicle-history-app/ TDengine 历史查询 + RAW JSON") .contains("vehicle-analytics-app/ JT808 每日里程指标消费") .contains("协议接入应用只负责收、解析、校验、规整和投递 Kafka") + .contains("生产链路不提供 RabbitMQ、RocketMQ 或其他 MQ 后端") .contains("`vehicle-analytics-app` 将 JT808 `daily_mileage_km` 写入 MySQL `vehicle_stat_metric`") .doesNotContain("Spring Boot 3.4") .doesNotContain("Netty 4.1") @@ -189,7 +191,8 @@ class MavenModuleProfileTest { .doesNotContain("Parquet + DuckDB 文件型明细库") .doesNotContain("车辆状态/日统计消费") .doesNotContain("本服务不写业务库") - .doesNotContain("里程统计全部由 Kafka 下游消费者实现"); + .doesNotContain("里程统计全部由 Kafka 下游消费者实现") + .doesNotContain("| 消息队列 |"); } @Test diff --git a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/PortainerComposeResourceLimitsTest.java b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/PortainerComposeResourceLimitsTest.java index f3ab4209..54867f6a 100644 --- a/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/PortainerComposeResourceLimitsTest.java +++ b/modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/PortainerComposeResourceLimitsTest.java @@ -111,6 +111,18 @@ class PortainerComposeResourceLimitsTest { } } + @Test + void tdengineVerificationRunbookChecksLivenessAndReadinessForManagedApps() throws IOException { + String runbook = Files.readString(repositoryRoot() + .resolve("docs/operations/vehicle-ingest-tdengine-verification.md")); + + for (String httpPort : List.of("20100", "20400", "20200")) { + assertThat(runbook) + .contains("curl -sS http://127.0.0.1:" + httpPort + "/actuator/health/liveness") + .contains("curl -sS http://127.0.0.1:" + httpPort + "/actuator/health/readiness"); + } + } + @Test void dockerEntrypointExecsJavaForSignalHandling() throws IOException { String dockerfile = Files.readString(repositoryRoot().resolve("Dockerfile"));