返回博客

8 Layer 合并为 3 进程

coomia-dip 的初始架构将系统划分为 8 个 Layer,每个 Layer 独立部署、独立演进。这个设计在理论上完美遵循了"关注点分离"原则,但在一个 4 人团队的实际开发中,8 个独立进程意味着 8 份配置、8 个启动脚本、28 条潜在的 gRPC 连接——复杂度远超团队的承受能力。本文记录了我们如何在保持逻辑架构清晰的前提下,将 8 个 Layer 合并为 3 个部署进程:Control(B+G)、Compute(C+F)、Intelligence(D+E),以及合并过程中的技术挑战和经验教训。

Coomia发布于 2026年2月18日16 分钟阅读
分享本文Twitter / X

系列:S14 工程实录 · 第 3 篇 | 难度:中级 | 阅读时间:15 分钟

8 Layer 合并为 3 进程

#TL;DR

coomia-dip 的初始架构将系统划分为 8 个 Layer,每个 Layer 独立部署、独立演进。这个设计在理论上完美遵循了"关注点分离"原则,但在一个 4 人团队的实际开发中,8 个独立进程意味着 8 份配置、8 个启动脚本、28 条潜在的 gRPC 连接——复杂度远超团队的承受能力。本文记录了我们如何在保持逻辑架构清晰的前提下,将 8 个 Layer 合并为 3 个部署进程:Control(B+G)、Compute(C+F)、Intelligence(D+E),以及合并过程中的技术挑战和经验教训。

#1. 问题:8 Layer 的代价

#1.1 理想与现实的差距

分层架构在白板上画出来时非常优雅:

Code
A (Deployment) → B (Control) → C (Data)
                     ↓            ↓
                 G (Metadata)  F (Pipeline)
                     ↓
                 D (Reasoning) → E (Agent)
                     ↓
                 H (SDK)

每个 Layer 有清晰的职责边界,技术栈可以独立选择,团队可以并行开发。这是微服务架构的教科书案例。

但现实是:

问题 1:启动一个完整的开发环境需要 12 分钟

开发者每天早上到办公室的第一件事是 docker compose up,然后等 12 分钟。这 12 分钟包括:

  • 8 个服务的镜像拉取和启动
  • 3 个基础设施(Doris、MinIO、Redis)的初始化
  • 服务间的健康检查和连接建立

如果中间有任何一个服务启动失败(在 WSL2 环境下,这种情况大约每周发生 2-3 次),就需要排查问题、清理状态、重新启动,可能又要 20 分钟。

问题 2:调试跨 Layer 的问题极其困难

一个典型的 Action 执行流程需要经过 4 个 Layer:

Code
SDK (H) → Control Layer (B) → Data Layer (C) → Intelligence Layer (D)

当 Action 执行失败时,开发者需要在 4 个服务的日志中来回跳转,拼凑出完整的调用链。我们虽然集成了分布式追踪(Jaeger),但在开发环境中,额外启动一个 Jaeger 服务意味着内存消耗又增加 500MB。

问题 3:gRPC 连接管理的复杂度

8 个 Layer 之间最多可能有 C(8,2) = 28 条双向连接。实际使用中,我们有 14 条活跃的 gRPC 连接。每条连接都需要:

  • 配置目标地址和端口
  • 处理连接超时和重试
  • 管理连接池
  • 处理优雅关闭

14 条连接的配置和管理代码加起来超过 2000 行。

问题 4:测试环境的资源消耗

CI/CD 流水线中,跑一次完整的集成测试需要启动全部 8 个服务 + 3 个基础设施 = 11 个容器。在我们的 CI 服务器上(32GB 内存),这占用了约 20GB 内存,几乎没有余量。

#1.2 量化痛苦

我们用一周时间收集了以下数据:

指标数值
每日启动开发环境的时间12-30 分钟
每周因启动失败浪费的时间2-3 小时
跨 Layer 调试的平均时间45 分钟/次
gRPC 连接管理代码行数2,100 行
Docker Compose 配置行数380 行
CI 完整测试运行时间35 分钟
CI 内存消耗20GB

这些数字告诉我们:架构的优雅不能以开发效率为代价。

#2. 设计约束:什么不能变

在讨论合并方案之前,我们明确了不可妥协的约束:

#2.1 逻辑边界必须保留

合并的是部署单元(进程),不是代码组织。每个 Layer 的代码仍然在独立的 package/module 中,有清晰的 API 边界。这意味着未来如果团队扩大,可以随时拆回独立部署。

#2.2 gRPC 接口不变

所有 Layer 之间的通信仍然通过 gRPC。合并后,同一进程内的 Layer 之间的 gRPC 调用走进程内通道(in-process channel),避免网络开销,但接口定义不变。

#2.3 技术栈约束

  • B(Control)和 G(Metadata)都是 Java/Spring Boot,可以合并
  • C(Data)和 F(Pipeline)都是 Java/Quarkus,可以合并
  • D(Reasoning)和 E(Agent)都是 Python/FastAPI,可以合并
  • A(Deployment)保持独立(它是部署工具,不是运行时服务)
  • H(SDK)保持独立(它是客户端库,不是服务)

#2.4 可逆性

合并必须是可逆的。如果未来需要,任何被合并的 Layer 都能在一周内拆回独立部署。

#3. 合并方案

#3.1 最终的 3 进程架构

经过评估,我们确定了以下合并方案:

Code
进程 1: Control Service (Java/Spring Boot)
├── Control Layer: Control Layer(Ontology Runtime, Schema Registry, Action Engine)
└── Metadata & Governance Layer: Metadata & Governance(元数据管理, 数据血缘, 审计)

进程 2: Compute Service (Java/Quarkus)
├── Data Layer: Data Layer(存储引擎, 查询引擎, OQL)
└── Pipeline & Orchestration Layer: Pipeline & Orchestration(数据管道, ETL, 调度)

进程 3: Intelligence Service (Python/FastAPI)
├── Reasoning & Decision Layer: Reasoning & Decision(规则引擎, 推理引擎)
└── Agent Runtime Layer: Agent Runtime(Agent 框架, Temporal 工作流)

Deployment & Operations Layer(Deployment)和 SDK & Developer Experience Layer(SDK)不参与合并——它们本来就不是运行时服务。

#3.2 为什么是这样的组合

B + G → Control Service

B(Control)负责 Ontology 的核心运行时,G(Metadata)负责元数据治理。它们共享大量的领域模型(ObjectType、LinkType、PropertyType),合并后可以消除大量的跨进程数据传输。

在合并前,G 需要通过 gRPC 从 B 拉取 ObjectType 定义来构建数据血缘;合并后,G 可以直接调用 B 的内部 API,延迟从 5-10ms 降到 < 0.1ms。

C + F → Compute Service

C(Data)负责数据存储和查询,F(Pipeline)负责数据管道。它们共享存储层——管道的输出直接写入 Data Layer 的存储。

在合并前,F 执行完 ETL 后需要通过 gRPC 将数据推送到 C;合并后,F 可以直接调用 C 的存储 API,批量导入性能提升 3-5 倍。

D + E → Intelligence Service

D(Reasoning)负责规则和推理,E(Agent)负责 Agent 运行时。它们共享 Python 运行时和 AI 模型加载器。

在合并前,E 的 Agent 需要通过 gRPC 调用 D 的推理接口进行决策;合并后,Agent 可以直接调用推理引擎的 Python 函数,消除了序列化/反序列化开销。

#4. 技术实现

#4.1 进程内 gRPC 通道

合并的核心技术挑战是:如何在同一进程内的两个 Layer 之间保留 gRPC 通信,但避免网络开销。

Java 侧(Control Service / Compute Service)

我们使用 gRPC 的 InProcessServerInProcessChannel

Java
// 创建进程内服务器
Server inProcessServer = InProcessServerBuilder
    .forName("control-internal")
    .addService(new MetadataGrpcService(metadataService))
    .build()
    .start();

// 创建进程内通道
ManagedChannel channel = InProcessChannelBuilder
    .forName("control-internal")
    .directExecutor()
    .build();

// 使用通道创建 Stub(与远程调用完全一样)
MetadataServiceGrpc.MetadataServiceBlockingStub stub =
    MetadataServiceGrpc.newBlockingStub(channel);

这种方式的好处是:

  • Stub 的使用方式与远程调用完全一致
  • 如果需要拆回独立部署,只需要将 InProcessChannel 改为 ManagedChannel
  • 进程内调用的延迟从 5-10ms 降到 < 0.1ms

Python 侧(Intelligence Service)

Python 的 grpcio 库也支持进程内通道,但我们采用了更简单的方案——直接函数调用 + 接口适配器:

Python
class ReasoningClient:
    """统一接口,支持远程和本地两种模式"""

    def __init__(self, mode: str = "local"):
        if mode == "remote":
            self._client = GrpcReasoningClient(address="reasoning:50051")
        else:
            self._client = LocalReasoningClient(reasoning_engine)

    def evaluate_rules(self, context: RuleContext) -> RuleResult:
        return self._client.evaluate_rules(context)

#4.2 配置统一

合并前,8 个服务各有自己的配置文件。合并后,我们使用分层配置:

YAML
# control-service.yml
server:
  port: 8080
  grpc-port: 50051

# Control Layer (B) 配置
ontology:
  runtime:
    cache-size: 10000
    schema-version-limit: 100

# Metadata Layer (G) 配置
metadata:
  lineage:
    enabled: true
    storage: doris
  audit:
    retention-days: 90

每个 Layer 的配置前缀不同,互不干扰。

#4.3 健康检查合并

合并前,每个服务有自己的 /health 端点。合并后,健康检查需要聚合多个 Layer 的状态:

Java
@GetMapping("/health")
public HealthResponse health() {
    Map<String, PlaneHealth> Layers = new LinkedHashMap<>();
    Layers.put("control-Layer", controlPlaneHealth.check());
    Layers.put("metadata-Layer", metadataPlaneHealth.check());

    boolean allHealthy = Layers.values().stream()
        .allMatch(h -> h.getStatus() == Status.UP);

    return new HealthResponse(
        allHealthy ? Status.UP : Status.DEGRADED,
        Layers
    );
}

返回示例:

JSON
{
  "status": "UP",
  "Layers": {
    "control-Layer": {"status": "UP", "details": {"objects": 15230}},
    "metadata-Layer": {"status": "UP", "details": {"lineageEdges": 8940}}
  }
}

#4.4 日志隔离

合并后的一个重要问题是日志混杂。我们通过 MDC(Mapped Diagnostic Context)为每个 Layer 的日志添加前缀:

Code
[2024-08-15 10:23:45] [CONTROL-B] Creating ObjectType: Device
[2024-08-15 10:23:46] [METADATA-G] Recording lineage for ObjectType: Device
[2024-08-15 10:23:46] [CONTROL-B] ObjectType created: Device (v1)

这样在查看合并后的日志时,仍然可以清晰地区分每个 Layer 的日志。

#5. 合并过程

#5.1 时间线

整个合并过程历时 3 周:

Week 1:准备

  • 梳理所有跨 Layer 的 gRPC 接口
  • 确认哪些可以改为进程内调用
  • 编写进程内通道的适配器
  • 更新 Docker Compose 配置

Week 2:合并

  • Day 1-2:合并 B + G → Control Service
  • Day 3-4:合并 C + F → Compute Service
  • Day 5:合并 D + E → Intelligence Service

Week 3:验证

  • 运行全部 3500+ 测试
  • 性能基准测试对比
  • 修复合并引入的 Bug(共 7 个)
  • 更新文档

#5.2 合并中遇到的问题

问题 1:Spring Boot 和 Bean 冲突

B 和 G 都是 Spring Boot 应用,合并后出现了 Bean 名称冲突。例如,两个 Layer 都定义了 ObjectTypeRepository 的 Bean。

解决方案:使用 @Qualifier 注解区分,并统一命名规范为 {Layer}{BeanName}

Java
@Bean
@Qualifier("controlObjectTypeRepository")
public ObjectTypeRepository controlObjectTypeRepository() { ... }

@Bean
@Qualifier("metadataObjectTypeRepository")
public ObjectTypeRepository metadataObjectTypeRepository() { ... }

问题 2:Quarkus 的类加载问题

C 和 F 都是 Quarkus 应用,合并后发现 Quarkus 的 CDI(Context and Dependency Injection)在处理两个模块的相同接口时出现了歧义。

解决方案:使用 @Alternative@Priority 注解明确优先级。

问题 3:Python 的模块命名冲突

D 和 E 都有 models 包,合并后 import models 出现歧义。

解决方案:所有导入改为绝对路径导入:

Python
# 之前
from models import RuleContext

# 之后
from intelligence_plane.reasoning.models import RuleContext
from intelligence_plane.agent.models import AgentContext

问题 4:端口冲突

合并前每个服务监听不同端口(8080-8087)。合并后只需要 3 个 HTTP 端口 + 3 个 gRPC 端口。

解决方案:统一端口分配策略。

服务HTTPgRPC
Control Service808050051
Compute Service808150052
Intelligence Service808250053

问题 5:数据库连接池调优

合并前,每个服务维护自己的数据库连接池(每个 10 个连接 × 8 = 80 个连接)。合并后,3 个服务共享更少的连接就够了(每个 20 个连接 × 3 = 60 个连接)。

#6. 合并效果

#6.1 开发效率提升

指标合并前合并后提升
启动开发环境12 分钟4 分钟3x
CI 完整测试35 分钟18 分钟1.9x
Docker Compose 行数380 行120 行-68%
gRPC 连接数14 条3 条-79%
连接管理代码2,100 行600 行-71%
CI 内存消耗20GB10GB-50%

#6.2 性能提升

进程内 gRPC 调用消除了网络开销:

调用路径合并前延迟合并后延迟提升
B → G(元数据查询)8ms0.05ms160x
C → F(管道触发)6ms0.03ms200x
D → E(Agent 推理)12ms0.08ms150x
B → C(跨进程)8ms8ms无变化

注意:跨进程的调用(如 B → C)延迟不变,因为它们仍然走网络 gRPC。

#6.3 代码组织

合并后的代码目录结构:

Code
control-Layer/
├── control-control/           # Control Layer 代码
│   ├── src/main/java/com/onto/control/
│   └── src/test/java/
├── governance-metadata/          # Metadata & Governance Layer 代码
│   ├── src/main/java/com/onto/metadata/
│   └── src/test/java/
├── control-service/           # 合并后的启动入口
│   ├── src/main/java/com/onto/control/ControlServiceApplication.java
│   └── src/main/resources/application.yml
└── build.gradle               # 多模块构建

每个 Layer 的代码仍然在独立的子模块中,保持了代码层面的清晰隔离。

#7. 可逆性验证

为了验证"可逆性"约束,我们在合并完成后花了半天时间做了一次"拆分演练":

  1. governance-metadatacontrol-Layer 中拆出
  2. 创建独立的 metadata-service 启动入口
  3. 将进程内通道改为网络 gRPC 通道
  4. 运行 Metadata & Governance Layer 的全部测试

结果:约 3 小时完成拆分,所有测试通过。这验证了我们的合并方案确实是可逆的。

#8. 经验教训

#8.1 架构应该匹配团队规模

Conway 定律说"系统的架构反映组织的沟通结构"。我们的经验补充了一条:系统的部署单元数量不应超过团队人数的 2 倍

4 人团队维护 8 个部署单元 = 每人 2 个。考虑到每个部署单元都有配置、监控、故障排查的开销,这已经超出了合理范围。合并为 3 个部署单元后(每人 < 1 个),开发效率显著提升。

#8.2 逻辑架构和部署架构可以解耦

这是这次合并最重要的认识。逻辑上的 8 个 Layer 和部署上的 3 个进程完全可以共存。代码组织遵循逻辑架构(清晰的边界、独立的模块),部署策略遵循实际需求(开发期合并,扩展期拆分)。

#8.3 进程内 gRPC 是一个好模式

进程内 gRPC 通道让我们在合并部署单元的同时保留了接口契约。这意味着:

  • 接口测试不需要修改
  • 拆分时只需要改通道配置
  • 性能有数量级的提升

#8.4 合并要趁早

如果我们在第三个月就做这个合并,而不是等到第九个月,可以省下约 6 个月的"运维税"。延迟合并的原因是"担心破坏现有功能"——但事实证明,有了完善的测试套件,合并是安全的。

#9. 什么时候应该拆回去

我们预设了三个"拆分触发条件":

  1. 团队扩大到 8 人以上:每个进程至少 2 人负责,有足够的人力维护独立部署
  2. 单进程的资源消耗达到瓶颈:CPU 或内存无法通过垂直扩展满足
  3. 不同 Layer 需要不同的扩缩策略:如 Intelligence Layer 需要 GPU 而 Control Layer 不需要

目前(Day 420),这三个条件都未触发。

#10. 总结

8 Layer 合并为 3 进程,是 coomia-dip 项目中对"务实工程"的一次重要实践。它告诉我们:好的架构不是追求理论上的完美分离,而是在清晰性和实用性之间找到平衡。

逻辑上,我们仍然有 8 个 Layer。代码上,每个 Layer 仍然是独立的模块。但在部署上,我们选择了对 4 人团队最高效的方式——3 个进程。

如果未来团队扩大、业务增长,我们随时可以拆回去。而且因为我们保留了 gRPC 接口和模块边界,拆分的成本是可控的。

#Key Takeaways

  1. 部署单元数 <= 团队人数 x 2:超过这个比例,运维开销会吞噬开发效率
  2. 逻辑架构和部署架构可以解耦:代码组织遵循逻辑边界,部署策略遵循实际需求
  3. 进程内 gRPC 通道是合并部署但保留接口契约的好模式
  4. 合并要趁早:有完善的测试套件支撑,合并的风险远低于预期
  5. 预设拆分条件:明确什么时候需要拆回去,避免合并后的惰性

#Next Article

下一篇:S14-04 存储统一之路 — 从 PostgreSQL 到 ClickHouse 到 Doris,存储架构三次变迁的完整故事。

Tags: #coomia-dip #架构合并 #微服务 #部署架构 #gRPC #Conway定律 #工程效率