架构图最大的问题不是画不出来,而是画完就过时:UML 图太复杂没人看,随手画的框图又说不清细节,新人和外部团队永远在"问人"而不是"看图"。C4 模型(C4 Model)用四个层层递进的抽象层级解决了"画多细"的问题,而"图即代码(Diagrams as Code)“解决了"会不会过时"的问题。本文讲清 C4 的四层、用什么工具落地、以及如何让它跟代码一起演进。
1. 为什么架构图总是失效
1.1 两个极端
架构文档常见的失败模式只有两种:
| 失败模式 | 表现 | 后果 |
|---|---|---|
| 画得太细 | 一张 UML 类图塞进 200 个类 | 没人看得懂,画完即废 |
| 画得太粗 | 一个方框写"系统” | 说不清边界、依赖、职责 |
| 手工维护 | Visio/PPT 手绘 | 代码改了图不改,迅速失真 |
根因是缺少"分层"意识:给 CEO 看的图和给新入职工程师看的图,粒度需求天差地别,硬塞进一张图必然两头不讨好。
1.2 文档化的正确目标
架构文档的目标不是"完整",而是让不同读者在合适的抽象层级上快速建立正确心智模型。这需要回答三个问题:
- 这个系统是什么、为谁服务(对外);
- 它由哪些可独立部署的单元组成、如何通信(对开发);
- 某个容器内部如何拆成组件(对具体模块负责人)。
C4 模型正是为这三个问题设计的,同时呼应了 架构设计基础原则 中"关注点分离"的思路。
2. C4 模型:四层抽象
2.1 四层概览
C4 由 Simon Brown 提出,四个层次像地图的缩放级别:
| 层级 | 名称 | 面向读者 | 元素 | 典型数量 |
|---|---|---|---|---|
| L1 | Context(上下文) | 所有人、非技术 | 人、你的系统、外部系统 | 5~10 |
| L2 | Container(容器) | 技术/运维 | 应用、数据库、队列、前端 | 5~15 |
| L3 | Component(组件) | 架构师/开发 | 容器内的组件 | 5~20 |
| L4 | Code(代码) | 该模块开发者 | 类/接口 | 按需,通常省略 |
关键约定:C4 的"容器(Container)“指可独立运行/部署的单元(进程、服务、数据库、SPA),不是 Docker 容器——这是最常见的误解。
2.2 L1 上下文图:边界与外部依赖
上下文图回答"我们的系统在世界的哪个位置”。它只画三类东西:人、被描述的系统、与之交互的外部系统。
┌──────────┐ ┌───────────────┐
│ 顾客 │────────▶│ 电商平台 │
└──────────┘ 下单 │ (被描述系统) │
└───────┬───────┘
┌──────────┐ │
│ 运营人员 │─────────────────┘
└──────────┘ 管理后台
│ 支付
▼
┌───────────┐
│ 支付网关 │ ← 外部系统
└───────────┘
图上每个箭头都要标注交互内容(“下单"“支付”),不写技术细节(不写 HTTP/REST)。
2.3 L2 容器图:可部署单元与通信
容器图把被描述的系统"拆开”,展示其中的可独立部署单元及其通信协议:
┌────────────┐ HTTPS/JSON ┌──────────────┐
│ Web SPA │─────────────▶│ API 网关 │
│ (浏览器) │ │ (Node/Express)│
└────────────┘ └──────┬───────┘
│ gRPC
┌───────────▼──────────┐
│ 订单服务 │
│ (Java/Spring) │
└───┬──────────┬───────┘
JDBC │ │ 发布事件
┌────────────▼──┐ ┌───▼─────────┐
│ 订单数据库 │ │ Kafka │
│ (PostgreSQL) │ │ (事件总线) │
└────────────────┘ └─────────────┘
这是信息密度最高的一层,多数团队的文档化精力应该集中在这里:它直接对应"部署拓扑 + 技术选型 + 集成方式"。
2.4 L3 组件图与 L4 代码图
组件图放大某个容器,展示内部组件与职责(如订单服务里的 OrderController、OrderService、OrderRepository)。L4 代码图(类图)通常不必手画——现代 IDE 能按需生成,且变化太快,手工维护得不偿失。
经验法则:手工维护到 L3 为止,L4 交给工具按需生成。
3. 图即代码:让文档跟着代码走
3.1 为什么用文本描述架构
手绘图过时的根因是它不在版本控制里、也不随代码评审。把架构描述成文本,就能享受代码的一切待遇:Git 版本化、PR 评审、CI 校验、可 diff。这就是"图即代码(Diagrams as Code)"。
3.2 Structurizr DSL
Structurizr 是 C4 的官方工具,用一套 DSL 同时定义模型与视图,多视图从同一个模型生成,保证一致性:
workspace "电商平台" {
model {
customer = person "顾客" "下单购买商品"
ops = person "运营人员" "管理商品与订单"
ecommerce = softwareSystem "电商平台" {
spa = container "Web SPA" "React 单页应用" "浏览器"
gateway = container "API 网关" "路由与鉴权" "Node.js"
orderSvc = container "订单服务" "订单与库存" "Java/Spring"
orderDb = container "订单数据库" "" "PostgreSQL"
bus = container "事件总线" "" "Kafka"
}
payment = softwareSystem "支付网关" "外部支付服务" "External"
customer -> spa "使用"
ops -> spa "管理"
spa -> gateway "调用 API" "HTTPS/JSON"
gateway -> orderSvc "转发" "gRPC"
orderSvc -> orderDb "读写" "JDBC"
orderSvc -> bus "发布领域事件" "Kafka 协议"
orderSvc -> payment "发起支付" "HTTPS"
}
views {
systemContext ecommerce "context" { include * ; autoLayout lr }
container ecommerce "containers" { include * ; autoLayout tb }
}
}
从这一份模型可以同时导出上下文图、容器图、组件图,甚至生成可点击的架构文档站点。模型改了,所有视图同步更新——这是它对手绘图最大的优势。
3.3 PlantUML / Mermaid 作为轻量替代
如果不想引入 Structurizr,用 PlantUML 的 C4-PlantUML 宏或 Mermaid 也能画:
graph TB
customer[顾客] --> spa[Web SPA]
spa -->|HTTPS| gateway[API 网关]
gateway -->|gRPC| order[订单服务]
order --> db[(订单数据库)]
order --> bus[[Kafka]]
| 工具 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| Structurizr | 模型驱动、多视图一致 | 学习成本、需渲染服务 | 中大型系统 |
| C4-PlantUML | 生态成熟、文本可 diff | 模型与视图仍需分别维护 | 一般团队 |
| Mermaid | GitHub/GitLab 原生渲染 | 布局控制弱、元素多会乱 | 轻量/内嵌文档 |
| draw.io | 所见即所得、上手快 | 文本 diff 差 | 临时沟通 |
4. 扩展视图:C4 之外的补充
四层静态结构图不足以描述系统。Simon Brown 后来补充了几种视图:
4.1 动态视图
动态视图描述一次交互的时序(下单流程跨哪些容器、按什么顺序):
顾客 → SPA → 网关 → 订单服务 → 支付网关 → 订单服务 → Kafka
(每个箭头标注序号 1..n 与内容)
它对应 UML 时序图,但只使用 C4 的元素,避免引入新概念。
4.2 部署视图
部署视图把容器映射到基础设施节点(可用区、K8s 集群、区域),回答"东西跑在哪里":
| 容器 | 生产节点 | 副本数 | 备注 |
|---|---|---|---|
| API 网关 | K8s prod-ns | 3 | HPA 2~10 |
| 订单服务 | K8s prod-ns | 4 | 跨 3 可用区 |
| 订单数据库 | RDS 主备 | 1 主 1 备 | 多可用区 |
4.3 与 ADR 配合
C4 回答"系统长什么样",架构决策记录(ADR) 回答"为什么长这样"。两者是互补的:容器图上的每条连线背后都可能对应一条 ADR。
容器图:订单服务 ──发布领域事件──▶ Kafka
ADR-007:订单与库存解耦采用事件驱动而非同步 RPC
推荐做法:在 C4 元素上挂链接,指向对应的 ADR,让读者能一路从"是什么"追到"为什么"。
5. 落地:让文档不过时的流程
5.1 纳入 CI
把架构模型文件放进代码仓库,并在 CI 中校验:
# Structurizr CLI:校验模型 + 导出站点
structurizr-cli export -w docs/architecture/workspace.dsl \
-f static-site -o build/arch-site
# 校验模型一致性(引用了不存在的元素会报错)
structurizr-cli validate -w docs/architecture/workspace.dsl
模型语法错误、引用了不存在的元素,CI 直接失败——这就把"文档"变成了"代码"。
5.2 谁在什么时候更新
| 触发时机 | 更新内容 | 责任人 |
|---|---|---|
| 新增/下线服务 | 容器图 | 提交 PR 的团队 |
| 引入新外部依赖 | 上下文图 | 架构师 |
| 模块重构 | 组件图 | 模块负责人 |
| 部署拓扑变化 | 部署视图 | SRE/运维 |
| 重大选型决策 | 关联新 ADR | 决策发起人 |
5.3 文档化的"够用"标准
不要追求大而全。一套可用的最小架构文档集:
- 1 张上下文图(所有人能看懂边界);
- 1 张容器图(开发能看懂拓扑);
- N 张组件图(按需,只画复杂模块);
- 关联的 ADR 库(解释关键决策);
- 1 个 README 索引(说明每份文档的读者与维护人)。
6. 常见误区
| 误区 | 表现 | 修正 |
|---|---|---|
| 把"容器"当 Docker | 图里画了一堆 Pod | 容器 = 可独立部署单元 |
| 上下文图写技术细节 | 箭头标"REST/JSON" | 只写业务交互 |
| 每层都手画 | L4 类图天天过时 | L4 交给 IDE 生成 |
| 图不标交互内容 | 只有连线没有说明 | 每条边标注做什么 |
| 文档脱离代码库 | 放在 Confluence 里腐烂 | 放仓库、进 CI、随 PR 评审 |
| 只有图没有决策 | 新人问"为什么这样" | 图挂 ADR 链接 |
| 追求一张图讲完 | 元素爆炸没人看 | 分层,按读者拆分 |
还有一条常被忽略:别把 C4 当教条。小系统可能只需要上下文图 + 容器图两张;大系统可以在 L2 与 L3 之间加"子系统"层级。C4 提供的是分层思维,而非必须四层齐全的规范,其演进理念与 演进式架构 一致——文档结构应随系统演进调整。
7. 小结
| 要点 | 结论 |
|---|---|
| 核心思想 | 用四层抽象匹配不同读者的粒度需求 |
| L1/L2 | 上下文图讲边界,容器图讲拓扑,信息密度最高 |
| L3/L4 | 组件图按需画,代码图交给工具生成 |
| 防过时 | 图即代码 + 进版本控制 + CI 校验 |
| 补视图 | 动态视图讲时序,部署视图讲落地 |
| 配合 | C4 讲"是什么",ADR 讲"为什么",互相挂链 |
一句话记住:C4 的价值不在"四层",而在"分层"与"单一模型多视图"。当架构描述变成随代码演进的文本资产、当每张图都能追溯到对应的决策记录,架构文档才真正从"画完就废"变成团队的活知识库。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。