模块化单体 DDD 绞杀式迁移从单体到微服务的渐进式演进一句话结论模块化单体是“一个可部署单元 内部强边界”的架构DDD 负责识别边界绞杀式迁移负责在不重写系统的前提下逐步把模块抽成微服务。三者的关系是DDD 定义“切哪里”模块化单体提供“切得动”的代码结构绞杀式迁移给出“安全切出去”的路径。一、为什么需要模块化单体先看传统单体的困境1.1 传统单体的典型问题❌ 传统单体大泥球 ├── OrderService.cs ← 2万行什么都往里塞 ├── CustomerService.cs ← 直接 new SqlConnection ├── InventoryService.cs ← 调用 OrderService 的私有方法 └── Controllers/ ← 直接操作数据库典型症状边界侵蚀模块互相 reach-through、共享数据库耦合跨模块 JOIN、变成大泥球只是按文件夹分类没有真正的模块。1.2 模块化单体的定位模块化单体是“一个可部署单元内部拆成强边界模块”的架构。它提供微服务级别的组织清晰度但不承担分布式系统的成本网络失败、最终一致性、运维开销。维度传统单体模块化单体微服务部署单元1个1个N个模块边界文件夹强强制边界网络边界数据库共享共享可 schema 隔离独立重构成本低中边界约束高跨服务分布式复杂度无无高适用场景小到中型团队、领域边界仍在探索中、希望保持微服务就绪的接缝但暂时不想支付分布式税。二、DDD 如何定义模块边界2.1 战略设计限界上下文 → 模块DDD 战略设计的第一步是划分限界上下文Bounded Context——一个领域内特定模型适用的边界。在模块化单体中每个限界上下文对应一个模块。以电商系统为例限界上下文划分 ├── 订单上下文 (Order BC) → OrderModule ├── 支付上下文 (Payment BC) → PaymentModule ├── 库存上下文 (Inventory BC) → InventoryModule └── 客户上下文 (Customer BC) → CustomerModule同一个词在不同上下文中含义不同在订单上下文中“商品”是购买快照名称、价格、数量在库存上下文中“商品”是可量化和预留的库存单元。这正是限界上下文存在的意义——每个上下文有自己的模型不强行统一。2.2 战术设计模块内部的领域模型每个模块内部应用 DDD 战术模式模式说明示例实体有唯一标识随时间持久Order订单号不变状态可改值对象无标识属性定义不可变Address、Money、OrderItem聚合一致性边界一个聚合根Order是根OrderItem只能通过 Order 修改领域事件记录已发生的事用于解耦OrderPlaced、PaymentCompleted仓储聚合的持久化接口定义在领域层IOrderRepository核心原则实体应封装行为而非仅传数据。如果业务逻辑在 Service 层而非实体中就是贫血模型反模式。三、模块化单体的代码结构与强制边界3.1 分包结构facade internal 模式Spring Modulith 推荐的结构只有基础包中的类型对其他模块可见internal/下的全部隐藏。com.example.app.{module}/ ├── {Module}Service.kt # 公开门面跨模块 API只返回 DTO ├── {PublicDto}.kt # 公开 DTO ├── events/ # 领域事件公开 │ └── {DomainEvent}.kt └── internal/ # 全部私有 ├── model/ # 实体、值对象 ├── repository/ # 仓储实现 ├── application/ # 内部服务 └── controller/ # REST 控制器关键规则orders模块可以依赖inventory :: events只依赖事件子包但不能依赖inventory的完整 API。3.2 边界强制手段模块化单体的最大风险是边界侵蚀——模块互相 reach-through。必须用架构测试强制边界// ArchUnitNET 示例禁止跨模块直接访问 internal[Fact]publicvoidOrderModule_ShouldNotAccess_InventoryInternal(){varresultTypes.InAssembly(typeof(OrderService).Assembly).That().ResideInNamespace(App.Order).ShouldNot().HaveDependencyOn(App.Inventory.Internal).GetResult();result.IsSuccessful.Should().BeTrue();}3.3 模块间通信契约 事件模块之间不能直接调用对方的内部服务或访问数据库。通信方式模块间通信规则 ├── 同步查询 → 通过公开的 Facade 接口返回 DTO ├── 状态变更 → 通过领域事件进程内事件总线 └── 禁止跨模块 JOIN、直接引用对方实体、调用对方 internal 方法进程内事件总线如 MediatR / EventEmitter在模块化单体中充当“微型消息代理”让模块间通信像微服务一样但零网络开销。这为后续绞杀式迁移铺路——把进程内事件换成 Kafka 消息边界不动。四、绞杀式迁移从模块化单体到微服务的路径4.1 绞杀植物模式的启示“绞杀植物”的生态过程种子被鸟带到寄主树顶 → 空中发芽 → 气根下垂入土 → 网状根系包裹寄主 → 寄主枯死 → 绞杀植物独立成树。软件映射寄主树 旧单体/遗留系统绞杀植物种子 新功能或新模块气根入土 新模块逐渐接管流量寄主枯死 旧功能被完全替换独立成树 新模块独立部署为微服务4.2 在模块化单体上实施绞杀式迁移的完整步骤前置条件你已经有一个模块化单体模块边界清晰模块间通过契约和事件通信。步骤 1选定绞杀目标从模块化单体中选一个边界清晰、依赖少、有独立伸缩需求的模块。优先选择对伸缩有独立需求的模块如 AI 推理需要 GPU变更频率高、影响面大的模块技术栈不同的模块如 Python AI 服务步骤 2将进程内事件替换为跨进程消息模块化单体中模块通过进程内事件总线通信。迁移第一步把该模块的入站/出站事件通过消息中间件Kafka/RabbitMQ桥接。// 迁移前进程内事件eventBus.Publish(newOrderPlaced(orderId));// 迁移后进程内事件 Kafka 桥接eventBus.Publish(newOrderPlaced(orderId));// 本地继续kafkaProducer.SendAsync(newOrderPlaced(orderId));// 同时发到 Kafka这样新模块可以订阅 Kafka 事件而旧模块仍然处理本地事件。两者并行运行。步骤 3新模块独立部署接管部分流量新模块作为独立服务部署通过网关或特性开关逐步接管流量流量切换策略绞杀式 ├── 阶段 1新模块只接收 1% 流量影子模式 ├── 阶段 2新模块接收 10% 流量金丝雀 ├── 阶段 3新模块接收 50% 流量 ├── 阶段 4新模块接收 100%旧模块停写 └── 阶段 5移除旧模块代码步骤 4数据拆分最困难的一步模块化单体通常共享数据库。绞杀式迁移的最大障碍是数据耦合。策略Schema 分离先把该模块的表移到独立 schema禁止跨 schema JOIN读写分离新模块写自己的库旧模块读旧库通过事件同步双写过渡新模块写新库的同时通过事件让旧模块更新旧库反之亦然最终一致接受短暂不一致用补偿/对账机制兜底步骤 5移除旧模块当新模块完全接管旧模块的代码、数据库表、配置全部移除。绞杀完成。五、完整案例ITS M 系统的渐进式演进5.1 背景一个 ITSMIT 服务管理系统Go 单体应用包含80 服务和 80 控制器。面临的问题部署不灵活AI 推理需要 GPU但整个单体一起部署数据库耦合所有模块共享 PostgreSQL大租户场景性能瓶颈AI 紧耦合Python AI 服务与 Go 后端紧耦合无法独立扩展测试困难全量测试耗时长5.2 决策模块化单体 事件驱动中台不直接拆微服务而是先采用“模块化单体 事件驱动中台”的渐进式架构itsm-backend/ ├── domain/ │ ├── ticket/ # 工单领域模块 │ │ ├── model/ │ │ ├── service/ │ │ └── repository/ │ ├── incident/ # 事件领域 │ ├── problem/ # 问题领域 │ ├── change/ # 变更领域 │ └── common/ # 公共领域共享内核 ├── infrastructure/ │ ├── event/ # 事件总线实现Kafka │ ├── cache/ │ └── storage/ └── api/ # API 层关键决策模块化单体结构保持单部署单元按业务领域划分子包事件驱动解耦引入 Kafka定义工单/审批/SLA/通知事件AI 服务独立化Python AI 服务独立部署通过 HTTP/gRPC 通信多租户强化租户上下文贯穿全链路5.3 实施计划阶段内容时间Phase 1搭建 Kafka、定义事件 schema、实现事件总线2 周Phase 2按领域重组代码包、关键逻辑改事件驱动4 周Phase 3AI 服务 Docker 化、定义 Go-Python 协议2 周5.4 效果正面保留现有开发/调试体验无需大幅重构支持独立扩展 AI 服务GPU 按需事件驱动降低模块耦合便于后续拆分多租户能力为 SaaS 化奠定基础负面需要引入 Kafka 等消息基础设施事件一致性需要额外处理幂等、补偿团队扩大后需评估是否进一步拆分为微服务六、关键设计原则清单模块化单体原则原则说明验证方式显式接口模块通过公开 Facade 暴露能力返回 DTO架构测试禁止跨模块引用 internalSchema 隔离每模块独立 schema禁止跨 schema JOIN数据库审计事件通信模块间状态变更通过领域事件解耦检查是否有跨模块直接调用高内聚低耦合模块内强内聚模块间弱依赖依赖数量分析DDD 边界原则原则说明一个上下文一个模型不强行统一不同上下文的概念聚合是事务边界一个事务只修改一个聚合值对象优先默认用值对象建模仅在需跟踪身份时用实体实体封装行为避免贫血模型业务规则放在实体内绞杀式迁移原则原则说明不重写新模块逐步接管旧系统继续运行事件桥接进程内事件 → 跨进程消息边界不动流量渐进影子 → 金丝雀 → 全量数据最后拆代码边界先拆数据边界后拆七、三者的关系总览┌─────────────────────────────────────────────────────────┐ │ 演进路线 │ │ │ │ 传统单体 模块化单体 微服务 │ │ (大泥球) → (强边界模块) → (独立服务) │ │ │ │ ↑ ↑ ↑ │ │ │ │ │ │ │ DDD 战略设计 DDD 战术设计 绞杀式迁移 │ │ 划分子域 设计聚合/事件 逐步抽出 │ │ │ │ 关键模块化单体是过渡态不是终态 │ │ DDD 保证边界质量绞杀保证迁移安全 │ └─────────────────────────────────────────────────────────┘最终心法不要过早微服务——先用模块化单体验证边界DDD 是边界工具——限界上下文决定模块怎么切绞杀是迁移纪律——永远保持系统可运行逐步替换模块化单体是微服务的准备阶段——边界清晰的模块化单体拆微服务只是“把进程内调用换成 HTTP/gRPC 把进程内事件换成 Kafka”