跳转到正文
zeno's blog
返回

整洁架构(四):微服务时代的落地-从理想到妥协

专题: 整洁架构

Table of contents

Open Table of contents

TL;DR

国内大厂(腾讯、字节等)在微服务实践中大量借鉴了 Clean Architecture 的思想,但几乎没有团队完整照搬同心圆模型。实际落地是在敏捷迭代压力下,选择性应用依赖反转原则——核心域严格分层,CRUD 边缘服务直接怼。


大厂微服务架构的现实

腾讯/字节的典型做法

国内大厂微服务架构的演进路径大致相同:

第一阶段:单体拆服务(2015-2018)

按业务线拆,每个服务内部结构随意。MVC、三层架构混用,handler 里写 SQL 的情况普遍。问题不在于没有架构,而是每个团队的架构不一样,跨团队协作成本极高。

第二阶段:统一框架(2018-2021)

腾讯推 trpc-go,字节用 Kitex/Hertz。框架统一了通信层(RPC、服务发现、熔断),但框架管不了业务代码的组织方式。很多团队的 service 层变成了 “转发层”——handler 调 service,service 调 dao,每层只做参数透传,分层变成了形式主义。

第三阶段:DDD/Clean Architecture 引入(2021-至今)

核心业务域开始引入领域驱动设计和整洁架构。但落地时做了大量妥协。

实际落地的分层(不是教科书式的)

service/
├── api/              # 协议层(proto 定义、HTTP handler)
├── biz/              # 业务逻辑(≈ usecase + domain 合并)
│   ├── model/        # 领域模型
│   └── service/      # 业务编排
├── data/             # 数据访问(≈ adapter/repository)
└── conf/             # 配置

关键妥协:

  1. usecase 和 domain 通常合并为 biz。教科书的四层在微服务粒度下太重——一个微服务本身就是一个 bounded context,再在内部分四层,认知负担大于收益
  2. interface 定义经常放在 biz 层而非独立的 port 包。Go 社区 “small interface” 的习惯让独立 port 包显得多余
  3. DTO 和 proto message 合并。gRPC 的 protobuf message 既是传输格式也是 DTO,再加一层转换被认为得不偿失

什么时候严格分层,什么时候放松

大厂的经验法则:

服务类型架构策略原因
核心域(支付、交易、风控)严格 Clean Architecture,domain 层独立业务规则复杂,变更频繁,需要隔离
支撑域(用户中心、消息通知)简化版,biz + data 两层逻辑相对固定,变更少
通用域(文件上传、短链)几乎不分层,handler 直通 DB纯 CRUD,分层是浪费

核心判断标准不是服务大小,而是业务规则的复杂度和变更频率。

敏捷开发与整洁架构的张力

矛盾在哪

这不是不可调和的矛盾,但需要节奏上的平衡。

实际有效的结合方式

1. 架构决策延迟但不缺席

不在项目第一天画完整的同心圆。先用简单两层(handler + repository)快速跑起来。当出现以下信号时再引入 use case 层:

2. 分层是重构手段,不是初始设计

敏捷的节奏下,先让代码工作,然后在重构 sprint(或 tech debt 时间)中引入分层。这比一开始就四层架构但每层都是空壳要务实得多。

3. 用 ADR(Architecture Decision Record)记录边界决策

每次做出”这个服务要不要分 domain 层”的决策时,写一个 ADR 说明 why。后续成员不用猜为什么有的服务分了三层有的只有两层。

敏捷 + Clean Architecture 的反模式

微服务间的 Clean Architecture

单个服务内部的分层只是一半。微服务之间的依赖关系同样适用整洁架构的思想:

┌─────────────────────┐
│  BFF / API Gateway  │  ← 最外层:面向端的适配
│  ┌─────────────────┐│
│  │ 业务编排服务     ││  ← 用例层:跨服务流程编排
│  │ ┌─────────────┐ ││
│  │ │ 核心域服务   │ ││  ← 领域层:支付、交易等
│  │ └─────────────┘ ││
│  └─────────────────┘│
└─────────────────────┘

原则相同:核心域服务不依赖编排服务,编排服务不依赖 BFF。依赖方向从外向内。

违反这个原则的典型错误:支付服务为了推送通知直接调用消息服务 → 支付服务依赖了外层。正确做法:支付服务发领域事件,消息服务订阅事件。

Pitfalls

  1. 把框架当架构:trpc-go/Kitex 解决的是通信和治理,不是业务代码组织。用了微服务框架不等于有了整洁架构
  2. 微服务边界划错比内部不分层危害更大:一个服务内部写得乱但边界清晰,后续可以重构;两个服务边界耦合(共享数据库、循环调用),重构代价是拆服务
  3. 跨服务的 “domain 层复用”:多个服务 import 同一个 domain 包 → 实质上是分布式单体。每个服务应该有自己的领域模型,即使字段重复

分享这篇文章:

上一篇
mio(六):代码示例-TCP Echo Server
下一篇
mio(五):关键设计决策