实测过程发现并修复四层问题,随后全部按实测结果校订文档:
- build.gradle.kts:补 application.mainClass(Kotlin 顶层 main → ApplicationKt)。
此前 ./gradlew run 报 "No main class specified",installDist 启动脚本主类为空(Dockerfile 不可运行)。
- application-dev.yml:stub 模式真正免基础设施——
* datasources.default.enabled=false(micronaut-jdbc JdbcDataSourceEnabled 条件排除 DataSource,
原注释"stub 不建连"意图未实现,DataSource eager 强制连 MySQL 导致启动失败);
* 关闭 micronaut 自带 Redis/Kafka 健康指示器(无 broker 时装配即把 /health 打成 500);
* 端点配置前缀修正:micronaut.endpoints.* 是死配置(/env 404、/beans 401 实测),
正确为顶层 endpoints.*(env 默认禁用 / beans 默认 enabled+sensitive 一并注明);
* 关闭 eureka discovery,避免无注册中心时 /health 变 DOWN。
验证:MICRONAUT_ENVIRONMENTS=dev ./gradlew run 无任何 env/外部依赖启动 → POST /cminmsgs/send
200 返回 ID → /health UP;text/plain 415(U16 缺口实证)。37 测试全绿未受影响。
- README/docs/architecture.md/docs/design.md:按实测与复核意见校订——
* README:dev 冒烟命令恢复为"无需 DB/Redis/Kafka",记录修复缘由;
* architecture:U17 服务名未落地(注册名仍取 micronaut.application.name=msgexchange-nextgen,
msgx.service-name 无消费方)、三层隔离为目标态(仅 register-eureka=false 生效)、
管理端点 5.1 实际口径(/env 默认禁用、/beans 默认 enabled+sensitive)、就绪度口径更新;
* design:§9 补 U17 行;smoke 路径 UNSUPPORTED→CODEC_ERROR 修正;毒丸 head-deadline 判据不可达、
快照 CAS 崩溃重放二次自增版本等"代码行为≠文档声明"处加实现注;I1 状态行同步。
- gradle/libs.versions.toml + gradle.properties:版本矩阵注释按依赖解析实测修正
(platform 5.1.3 生效但 core 系解析 5.1.13,classpath 混用;原"5.1.10 无平台 BOM"说法与观测不符)。
未提交内容:无。代码级缺陷(毒丸判据、CAS 重放、DNLD 路由顺序、identity 键释放等)仍属
ACM2-10 排期,本次仅按文档职责如实标注,未改动实现。
11 KiB
11 KiB
msgexchange-v2 架构文档
现行架构权威为 Plane
airport_chengdu_msgexchange_api工作区的 ACM2-3(综合架构 v4); 脚手架跟踪 ACM2-4,评审与实施计划(U01–U30)ACM2-10。本文是仓库内的架构速览, 与代码同步维护;两者冲突时以 ACM2-3 为准并回改本文。 配套设计细节见 design.md。
1. 系统定位
新一代机场消息交换服务(AODB 报文接入 → 处理 → 对外投递),替换 legacy
msgexchange-api(Java 8 / Spring Boot 1.5 / Maven)。过渡策略为双跑三步:
影子对拍(同入口双收,比对输出)→ 切流(nextgen 权威)→ 旧仓库冻结
- legacy 维护不受本仓库影响;本仓库不声明 legacy 旧表 schema(见 §6 数据边界)。
- wire 契约冻结:消息结构唯一事实源为
SIS_AODB_RMS-V0.1.md+doc/unisysaodbsis.xsd; HTTP 端点路径与响应语义沿用现役(如POST /cminmsgs/send返回记录 ID)。
2. 技术栈
| 层 | 选型 | 说明 |
|---|---|---|
| 语言/运行时 | Kotlin 2.3 + JDK 25 | JDK 21 不可行(Micronaut 5.1 系要求 JVM 25+,ACMA-9 实测) |
| 框架 | Micronaut 5.1.3 | 编译期 DI:KSP(kotlin-ksp + micronaut-inject-kotlin)生成 *$Definition |
| 持久化 | MySQL + Flyway | 仓储现为接口(Micronaut Data JDBC 实装属 U05,阶段 1 后续) |
| 权威存储 | Redis(阶段 A) | flightInfo hash;仅主泵线程写(I5);Lua 脚本原子覆盖 |
| 投递 | Kafka(acks=all + 幂等) | outbox 模式,经 MSG_EVENT 表中转 |
| 投影(阶段 B) | Elasticsearch + Redis 投影 + FLIGHT_STATE | 仅阶段 B 启用(msgx.phase) |
| 注册中心 | Eureka(Micronaut 原生键) | 服务名契约 msgexchangeapi(影子 msgexchangeapi-shadow)——U17 未落地:当前注册名仍取 micronaut.application.name(=msgexchange-nextgen),msgx.service-name 无运行时消费方(见 §8 与 design.md §9) |
| 可观测 | logstash TCP(Async 包装)+ MDC traceId + 自定义健康指示器 | 见 design.md §8 |
3. 总体拓扑
┌──────────────────────────────────────────────────┐
│ msgexchange-nextgen │
│ (单实例 · 单写者) │
AODB/上游 ──HTTP──▶│ ingress │
│ InboxService ──事务1──▶ CMINMSGS(原文) │
│ └▶ PROC_STATE(PENDING) │
│ │
│ processing(msgx-pump 线程,严格 FIFO 队头) │
│ Pump ──tick──▶ MessageProcessor │
│ │ │ decode(XmlCodec) │
│ │ │ identity 绑定(I3) │
│ │ │ Handler.decide(纯函数) │
│ │─Schd DNLD──▶ SnapshotFlow(流程4) │
│ │─PUMP_JOB───▶ JobExecutor(同队列,决策1) │
│ │ │
│ ├────Redis Lua──▶ Redis flightInfo(A权威) │
│ └──事务2──▶ MSG_EVENT(outbox)+ 回填+SUCCEEDED│
│ │
│ delivery(msgx-dispatcher 线程,每 target FIFO) │
│ Dispatcher ──逐条──▶ Kafka(msg) │
│ └─flushSchd 聚合─▶ Kafka(schd) │
│ (阶段 B 追加:ES flight_hts → Redis 投影删除) │
└──────────────────────────────────────────────────┘
│ │
▼ ▼
下游 Kafka topic Eureka / logstash
要点:
- 两条专用 daemon 单线程(
msgx-pump/msgx-dispatcher)由PipelineLifecycle在ServerStartupEvent后拉起,不占用 Netty event loop;停机requestStop+ interrupt + join(U07)。仅当msgx.pipeline.autostart=true时装配——生产默认关, 当前属有意脚手架门禁(生产可运行需先完成 U05 数据层实装,见 §7)。 - 单写者约束(I5):阶段 A 全部 Redis 写集中在主泵线程;实例数必须为 1 (运行期租约/选主保护属 U26,尚未实装,当前靠部署拓扑约束)。
- 统一 FIFO(决策 1):定时作业(cron → PUMP_JOB 入队)与消息同队列, 作业产物不绕过队头顺序;job 与队头的先后目前为入队时间近似, 统一序号列属 U15(未实装,见 design.md §9 缺口清单)。
4. 模块职责
| 包 | 职责 | 对应 ACMA-8 | 主要类 |
|---|---|---|---|
ingress/ |
收报事务1:原文落库 + 伴生 PENDING 行;不解析报文 | 流程 1,I3 | InboxController InboxService |
processing/ |
主泵:FIFO 领取、解码、identity 绑定、纯函数决策、事务2 | 流程 2/4,I1/I2/I5 | Pump MessageProcessor SnapshotFlow Identity Handler(Registry) |
delivery/ |
投递:每 target 严格 FIFO、schd 聚合 | 流程 3 | Dispatcher SchdAggregation |
jobs/ |
泵作业:清场/归档/投影重建(经 PUMP_JOB 同队列) | 流程 4/5/7,I4 | JobExecutor HistorySweepJob ArchiveJob ProjectionRebuildJob |
codec/ |
XML 解码 + 失败分类(MALFORMED vs CODEC_ERROR) | 决策 4 前置 | XmlCodec DecodeResult |
domain/ |
状态机枚举、事件/决策模型、Phase 开关 | I1–I5 | ProcState MsgEvent Decision MsgKind |
infra/ |
仓储接口、重试策略、Redis Lua、stub、健康、日志 | 数据模型节 | 见 design.md |
config/ |
PipelineProps 参数表(ACMA-8 参数初值) |
— | PipelineProps |
5. 关键架构决策
| # | 决策 | 落点 |
|---|---|---|
| D1 | 作业与消息同队列(cron 只经 PUMP_JOB 入队,产物不绕过队头) | Pump.tick / JobExecutor |
| D2 | 阶段 B:ES 投递成功后同线程同步 enqueue 删除事件(不轮询 ack) | Dispatcher.tick(定案 2) |
| D3 | schd 唯一出口是 flushSchd 批量聚合(逐条循环显式排除 KAFKA_SCHD) | Dispatcher.tick(U06/N03) |
| D4 | 未实装 ≠ 非法:无 handler / staging 未实装 → FAILED(UNSUPPORTED) 可重放,绝不写终态 | MessageProcessor SnapshotFlow(U10/N21) |
| D5 | 失败迁移在持有具体 head/batch 的边界完成;loop 只作最后防线,不吞 InterruptedException/Error | MessageProcessor Dispatcher(U08) |
| D6 | 接口驱动 + 假仓储单测;时间一律经可注入 Clock |
infra/persistence FailureScheduler |
| D7 | stub 装配门禁:msgx.stubs=true 才装配内存实装;与 autostart 组合支撑 dev 冒烟 |
infra/stub(U07/U01) |
| D8 | 编译期 DI(KSP)+ 启动期冒烟测试锁定 BeanDefinition 生成 | build.gradle.kts(U01) |
6. 数据边界
- 本仓库 Flyway 只建六张辅助表:
PROC_STATE/MSG_EVENT/REF_DATA/REQ_TRACK/PUMP_JOB/FLIGHT_STATE(V2.0.0__aux_tables.sql)。 CMINMSGS/CMINMSGS_HST/COUTMSGS等 legacy 旧表归 legacy 仓库维护(冻结期), 本仓库不重复声明;全新空库需先建 legacy schema,否则收报首句 SQL 报表不存在 (README「数据库初始化」节)。- 回滚兼容关键:SUCCEEDED 时回填
CMINMSGS.SUBSYSTEM_*+DATE_PROCESSED/STATUS, 旧系统可按自身语义无缝接管(Runbook 第 7 步)。
7. 两阶段权威与当前就绪度
| 阶段 | 权威 | 投递目标 | 状态 |
|---|---|---|---|
A(msgx.phase=A) |
Redis flightInfo | KAFKA:msg、KAFKA:schd | 管道骨架+重试闭环已实装;Redis Lua/实仓储属 U05/U09 |
B(msgx.phase=B) |
FLIGHT_STATE + 投影 | + ES:flight_hts、REDIS:flightInfo | 未实施(阶段 2 后) |
就绪度(2026-09-07 复核口径):可编译、37 测试全绿、dev stub 进程级冒烟实测可端到端
(./gradlew run 无外部依赖启动 → 收报 200 → /health UP,修复记录见 README「进程级 dev 冒烟」);
生产默认配置不可对外服务——autostart=false 且生产(stubs=false)下仓储无实装、DI 装配
即失败。生产就绪前置:U05(数据层+事务)、U07 fail-fast 定案、U09(快照恢复协议)、
U13(投递毒丸补全)、U15(统一序号)。逐项状态见 ACM2-10「定稿实施计划」。
8. 部署与安全姿态
- 实例数 = 1(主泵单写者前提);双实例误配当前无运行期防护(U26:租约/DB 锁 + 拒启,未实装)。
- 影子隔离(目标态;U17/U26 未落地,勿按现状引用):服务名(
msgexchangeapi-shadow)+ 独立 schema + Redis key 前缀 + 独立 topic 三层隔离;当前代码仅msgx.register-eureka=false生效—— Kafka topic 写死字面量"msg"/"schd"(Dispatcher)、FlightRedisClient.eval无 key 前缀参数、 服务名未接msgx.service-name(§2)。 - 网络信任模型:
/cminmsgs/send无鉴权(沿用现役内网信任姿态);eureka default-zone 回退127.0.0.1:8761;口令/端点全部环境变量外置(零入库)。安全节细化属 U28。 - 管理端点:Micronaut 5.1 下
/env默认禁用、/beans默认 enabled+sensitive;dev/影子经 顶层endpoints.*(非micronaut.endpoints.*——实测前缀错误时不生效)放开/env、/beans与 health 明细。工程未引入 micronaut-security,sensitive 的实际拦截行为待 U28 定案。 - 同名单风险:影子与生产同名同路径会互相收报——切流前必须核对服务名三隔离。
9. 可观测性
- 日志:logstash TCP JSON 通道(Async + neverBlock 降级,logstash 不可达不阻塞业务线程);
结构化生命周期日志(收报/SUCCEEDED/SKIPPED/FAILED/DEAD/毒丸/flush 批次);MDC
traceId(当前 = cminmsgsId/eventId,处理片段;贯穿收报→投递属 U12 遗留)。 - 健康:
/health聚合redis-flight-store/kafka-delivery自定义指示器——真实 ping 判定(false/异常→DOWN,缺 bean→DOWN),非仅 bean 存在。 - 指标缺口:micrometer 队列深度/投递延迟 gauge 未引入(版本对齐待 U05 批次); DEAD/DLQ 告警出口与一致性哨兵实装(U25)未落地——告警当前以 ERROR 日志为落点。