你上次打开团队的架构图是什么时候?
大多数团队都有架构图。Confluence 上躺着一张两年前画的系统拓扑图,Visio 文件在某个共享盘里,PowerPoint 里有几页"技术方案评审"的框线图。问题是:没人信它们。新人入职时看一眼,发现和实际系统对不上,从此再也不看。老人心里有一张"真正的架构图",但那张图只存在于他的脑子里。
这不是个别现象。Simon Brown 在 2018 年的调查中发现,超过半数的开发者认为自己团队的架构文档"基本没用"或"严重过时"(Simon Brown, "The C4 Model for Visualising Software Architecture", c4model.com)。
架构图的困境不在于没人愿意画,而在于三个结构性问题:
- 没有统一语义。同一张图上,一个方框可能代表一个进程、一个服务、一个类,也可能代表一个团队。看图的人只能猜。
- 没有层次。一张图试图同时展示系统边界、部署单元、内部模块和代码结构,结果什么都看不清。
- 没有工程化手段维护。图画在 Visio 或白板上,代码改了图不会跟着改,三个月后图就变成历史文物。
本文的目标是给出一套解决方案:用 C4 模型建立分层的架构视图体系,用 diagram-as-code 工具让图可以版本管理,用文档即代码的实践让架构文档和代码一起演进。
本文假设你已经读过 第一篇:什么是架构,了解架构决策和质量属性的基本概念。如果你对架构评估感兴趣,可以参考 第四篇:架构评估。
一、传统架构图为什么失败
在讨论解决方案之前,先把问题拆清楚。传统架构图的失败不是偶然的,它有三个根本原因。
无标准语义的方框线条图
打开任何一个团队的架构图,你会看到一堆方框和箭头。但这些方框代表什么?一个方框是一个可部署的服务,还是一个代码模块?箭头表示 HTTP 调用,还是数据流向,还是"依赖关系"?
没有标准答案。IEEE 1471(现在是 ISO/IEC/IEEE 42010:2022)定义了架构描述的基本框架,要求每个视图(view)都有明确的视点(viewpoint),每个视点都有明确的关注点(concern)和受众(stakeholder)。但大多数团队画的图,既没有声明视点,也没有说明元素类型,更没有定义箭头的语义。
结果就是:画图的人知道每个框代表什么,看图的人不知道。一张图的信息量取决于画图的人是否在旁边解释。
工具导致的维护断裂
Visio、PowerPoint、Draw.io 这些工具的问题不在于画图能力不够,而在于它们和代码生活在两个世界里。代码在 Git 仓库里,经过 CI/CD 管道,有 code review、有自动化测试。架构图在文件服务器或 Wiki 上,没有版本管理,没有 review 流程,没有自动化检查。
代码和文档分离的结果是:代码变了,图不会变。拆分了一个微服务,代码仓库多了一个 repo,但架构图上那个大方框还是原来的样子。三个月后,新来的工程师对着过期的架构图理解系统,做出错误的设计决策。
UML 的理想与现实
UML(Unified Modeling Language)本来是想解决"没有标准"的问题。它定义了 14 种图(UML 2.5.1, OMG, 2017),涵盖了从用例到部署的方方面面。但在实际工程中,UML 的采用率一直很低。
原因不复杂:UML 太重了。画一张完整的 UML 部署图,你需要理解节点(node)、制品(artifact)、通信路径(communication path)、部署规约(deployment specification)等概念。对于大多数团队来说,画图的目的是沟通,不是建模。UML 把画图变成了一个需要专门学习的技能,代价超过了收益。
我的判断是:UML 在需要精确建模的领域(如航空航天、医疗器械)仍然有价值,但对于绝大多数 Web/互联网团队来说,它的复杂度是一个实际障碍。C4 模型的创始人 Simon Brown 说过一句话:"UML 的问题不在于它不好,而在于它试图成为所有人的所有东西。"(Simon Brown, "Software Architecture for Developers", Leanpub, 2022)
二、C4 模型:分层的架构可视化
C4 模型由 Simon Brown 于 2006 年左右开始构思,2011 年正式命名并推广(c4model.com)。C4 是 Context、Container、Component、Code 的缩写,代表四层从粗到细的视图。
C4 的核心理念很简单:像地图一样分层。你不会在一张地图上同时画出国家边界和街道门牌号。架构图也一样,不同层次的信息应该放在不同层次的图上。
Level 1:系统上下文图(System Context Diagram)
系统上下文图回答一个问题:这个系统是什么,谁在用它,它和哪些外部系统交互?
这是最粗粒度的视图。它只有三类元素:
- 人(Person):使用系统的用户角色
- 待描述的系统(Software System):你要讲的那个系统,画在中间
- 外部系统(External System):和你的系统交互的其他系统
一张好的上下文图,非技术人员也能看懂。它的目标受众是产品经理、业务方、新加入团队的工程师。
下面是一个电商系统的 Level 1 示例:
这张图传递了几个关键信息:电商系统有两类用户(普通用户和运营管理员),依赖三个外部系统(支付、物流、短信)。一个新加入团队的工程师,看完这张图就知道系统的边界在哪里。
Level 2:容器图(Container Diagram)
容器(Container)在 C4 模型中有特定含义:一个可独立运行的进程或可部署单元。Web 应用、移动 App、数据库实例、消息队列、文件存储------这些都是容器。注意,这里的"容器"和 Docker 容器不是一个概念。
容器图回答的问题是:系统内部由哪些可部署单元组成,它们之间如何通信?
目标受众是开发人员和运维人员。这张图上,你能看到技术选型(用了什么语言、什么框架、什么数据库)和通信方式(HTTP、gRPC、消息队列)。
继续用电商系统的例子:
从上下文图到容器图,信息密度明显增加。你能看到:系统内部分成了哪几个服务,每个服务用了什么技术栈,服务之间通过什么协议通信,数据存储在哪里。但注意,这张图仍然不涉及任何代码级别的细节。
Level 3:组件图(Component Diagram)
组件图把一个容器打开,展示内部的主要构建块。这里的"组件"指的是一组相关的功能,通常对应代码中的一个模块、一个包(package)或一组紧密协作的类。
组件图回答的问题是:某个容器内部是怎么组织的?
目标受众是负责这个容器的开发团队。对于不负责这个容器的人来说,容器图的粒度通常就够了。
以"订单服务"为例,组件图展示了服务内部的主要模块及其关系:
这张图传达了几个关键信息:请求从 Controller 进入,经过 Service 处理业务逻辑,Service 依赖 Validator 做规则校验、Repository 做数据持久化、PaymentClient 调用外部支付、EventPublisher 发布事件。每个组件的职责边界清晰。
组件图的一个重要作用是暴露不合理的依赖。例如,如果 Controller 直接调用了 Repository 绕过了 Service 层,在组件图上一眼就能看出来。这种"越层调用"在代码 review 中可能被忽略,但在图上非常显眼。
画 Level 3 图时有一个常见的犯错点:粒度不对。如果一个容器内部只有 3 个组件,画 Level 3 意义不大,Level 2 已经够了。如果有 30 个组件,说明要么容器本身太大应该拆分,要么你画到了类级别------那是 Level 4 的事。一个合理的 Level 3 图通常包含 5-15 个组件。
Level 4:代码图(Code Diagram)
最细粒度的一层,对应 UML 中的类图、实体关系图或包图。C4 模型对这一层的建议是:大多数情况下不要手动画,让 IDE 或工具自动生成。
理由很实际:代码级别的图变化太频繁了。一次重构可能改变十几个类的关系,手动维护的成本远超收益。如果你用的是 IntelliJ IDEA 或 Visual Studio,它们可以直接从代码生成类图。这些图的价值在于即时查看和临时沟通,而不是作为长期文档。
Level 4 图有用的少数场景包括:
- 设计评审:在实现一个复杂功能之前,用类图说明设计方案。评审结束后图不需要长期维护。
- 复杂领域模型:DDD(领域驱动设计)中的聚合根、实体、值对象之间的关系,用 ER 图或类图来表达比代码更直观。但即使这种情况,图也应该从代码自动生成。
- 遗留系统分析:接手一个不熟悉的老系统时,用 IDE 自动生成类图来理解代码结构。这是一次性的分析工作。
Simon Brown 在 C4 模型文档中明确说:"大多数团队只需要 Level 1 和 Level 2。Level 3 在大型系统中有用,Level 4 几乎不需要手动维护。"(c4model.com)
C4 的元素类型与关系规范
C4 模型的一个重要贡献是定义了清晰的元素类型。不像传统方框图里"一个方框什么都能代表",C4 中每种元素有明确的含义:
| 元素类型 | 含义 | 出现在哪一层 |
|---|---|---|
| Person | 使用系统的人类角色 | Level 1 |
| Software System | 一个完整的软件系统 | Level 1 |
| Container | 可独立运行/部署的进程或单元 | Level 2 |
| Component | 容器内部的功能模块 | Level 3 |
| Code Element | 类、接口、函数 | Level 4 |
关系(Relationship)也有规范:每条关系要标注方向、描述(做什么)和技术细节(用什么协议/格式)。例如"订单服务 -> 支付网关:发起支付请求(HTTPS/JSON)",这里有方向(订单服务发起),有描述(发起支付请求),有技术(HTTPS/JSON)。
这种严格的元素类型定义,让不同的人画出的图具有可比性。你画的"容器"和我画的"容器"含义相同,不需要猜。
四层视图的层次关系
把四层视图的关系总结一下:
| 层级 | 名称 | 回答的问题 | 元素类型 | 目标受众 | 维护频率 |
|---|---|---|---|---|---|
| Level 1 | 系统上下文 | 系统是什么、谁在用 | Person, System | 所有人 | 低(季度/年) |
| Level 2 | 容器 | 系统由哪些部署单元组成 | Container, Database, Queue | 开发 + 运维 | 中(月度) |
| Level 3 | 组件 | 某个容器内部怎么组织 | Component | 容器负责团队 | 中高(双周) |
| Level 4 | 代码 | 类和接口的关系 | Class, Interface | 单个开发者 | 高(自动生成) |
从上到下,粒度越细,变化越频繁,受众越窄。这就是"像地图一样分层"的核心:你不需要在同一张图上塞进所有信息。
三、C4 与其他架构描述方法的比较
C4 不是唯一的架构描述方法。在选择之前,值得把几种主流方法放在一起比较。
Kruchten 4+1 视图模型
Philippe Kruchten 在 1995 年提出了 4+1 视图模型("Architectural Blueprints---The 4+1 View Model of Software Architecture", IEEE Software, 1995),定义了逻辑视图(Logical View)、进程视图(Process View)、开发视图(Development View)、物理视图(Physical View)和场景视图(Scenarios)。
4+1 模型对架构描述理论的贡献很大------它首次系统性地说明了"架构不是一张图"这个观点。但在实际工程中,4+1 的落地一直比较困难。原因有几个:
- 五个视图的边界不清晰。逻辑视图和开发视图的区别在哪?进程视图和物理视图的关系是什么?不同的人有不同的理解。
- 没有提供具体的图示规范。4+1 说了要画什么,但没说怎么画。落地时还是要依赖 UML 或其他符号系统。
- 对小团队来说太重。维护五个视图需要大量精力,大多数团队做不到。
关于 4+1 模型更详细的讨论,可以参考 第一篇:什么是架构。
arc42
arc42 是 Gernot Starke 和 Peter Hruschka 于 2005 年发起的架构文档模板(arc42.org)。它定义了 12 个章节,覆盖了从引言、约束、上下文到部署、运行时、设计决策的方方面面。
arc42 的优势在于全面。你不需要自己想"架构文档应该包含什么",arc42 告诉你一共 12 个部分,每个部分该写什么内容。它的劣势也在于全面------12 个章节全部写完,工作量不小。
实际操作中,arc42 和 C4 不冲突。很多团队的做法是:用 arc42 的模板来组织文档结构,用 C4 模型来画图。arc42 的第 5 章"Building Block View"天然对应 C4 的 Level 2 和 Level 3,第 7 章"Deployment View"对应部署相关的补充视图。
方法对比
| 维度 | UML | 4+1 视图模型 | arc42 | C4 模型 |
|---|---|---|---|---|
| 定位 | 通用建模语言 | 视图理论框架 | 文档模板 | 分层可视化方法 |
| 学习曲线 | 高(14 种图) | 中(概念清晰但落地难) | 中(模板驱动) | 低(4 层 + 少量元素类型) |
| 图示规范 | 完整、严格 | 不提供 | 推荐但不强制 | 简洁、明确 |
| 工具支持 | Enterprise Architect 等 | 无专用工具 | 模板 + 任意工具 | Structurizr、Mermaid 等 |
| 维护成本 | 高 | 高(五个视图) | 中高(12 章节) | 低到中 |
| 实际采用率 | 低(Web/互联网行业) | 学术引用多,工程落地少 | 欧洲较多 | 近年增长快 |
| 适合团队规模 | 大型/管制行业 | 大型 | 中大型 | 各种规模 |
这张表的核心判断是:C4 的优势不在于它比其他方法更"正确",而在于它的简洁性大幅降低了实际采用的门槛。一个方法如果团队用不起来,那它的理论完备性毫无意义。
四、Diagram-as-Code:让架构图可以被版本管理
传统画图工具的核心问题是:图和代码分离,维护路径不同。Diagram-as-code 的思路是把图的定义写成文本格式,和代码一起放在 Git 仓库里,享受代码的所有工程化能力------版本控制、diff、code review、CI/CD。
Structurizr DSL
Structurizr 是 Simon Brown 开发的 C4 模型工具,包含一个领域特定语言(Structurizr DSL)来定义架构模型。Structurizr DSL 不只是画图,它先定义模型(model),再从模型生成视图(view)。
以下是电商系统的 Structurizr DSL 定义(删减版,仅保留 Level 1 和 Level 2 关键部分):
text
workspace "电商系统" "电商系统的架构描述" {
model {
customer = person "普通用户" "浏览商品、下单、查看订单"
admin = person "运营管理员" "管理商品、处理订单、查看报表"
ecommerce = softwareSystem "电商系统" "提供商品浏览、下单、支付、物流跟踪" {
web = container "Web 前端" "提供用户界面" "React, Nginx"
api = container "API 网关" "统一入口,路由、限流、认证" "Go, Kong"
orderService = container "订单服务" "处理订单生命周期" "Java, Spring Boot"
productService = container "商品服务" "商品信息的增删改查" "Java, Spring Boot"
userService = container "用户服务" "注册、登录、用户信息管理" "Go"
orderDb = container "订单数据库" "存储订单数据" "MySQL" "database"
productDb = container "商品数据库" "存储商品数据" "MySQL" "database"
userDb = container "用户数据库" "存储用户信息" "PostgreSQL" "database"
mq = container "消息队列" "异步事件通知" "RabbitMQ" "queue"
cache = container "缓存" "热点数据缓存" "Redis" "cache"
}
payment = softwareSystem "支付网关" "支付宝/微信支付" "existing"
logistics = softwareSystem "物流系统" "第三方物流,运单查询" "existing"
customer -> web "浏览、下单" "HTTPS"
admin -> web "管理商品和订单" "HTTPS"
web -> api "调用" "HTTPS/JSON"
api -> orderService "路由请求" "gRPC"
api -> productService "路由请求" "gRPC"
api -> userService "路由请求" "gRPC"
orderService -> orderDb "读写" "SQL"
productService -> productDb "读写" "SQL"
userService -> userDb "读写" "SQL"
orderService -> mq "发布订单事件" "AMQP"
productService -> cache "读取缓存" "Redis protocol"
orderService -> payment "发起支付" "HTTPS/API"
}
views {
systemContext ecommerce "SystemContext" {
include *
autoLayout
}
container ecommerce "Containers" {
include *
autoLayout
}
theme default
}
}
Structurizr DSL 的几个设计特点值得注意:
- 模型和视图分离。先定义元素和关系,再决定哪些元素出现在哪张图上。一个模型可以生成多张不同的视图。
- 关系只定义一次 。
orderService -> payment "发起支付"这条关系定义一次,在上下文图和容器图上都能自动显示。 - 支持多种渲染方式。同一份 DSL 可以通过 Structurizr Lite(本地)、Structurizr Cloud(在线)或导出为 PlantUML/Mermaid 来渲染。
Structurizr 的局限性在于:免费版的 Structurizr Lite 功能足够日常使用,但 Structurizr Cloud 的高级功能需要付费。
Mermaid C4 图
Mermaid 从 v9.2 开始支持 C4 图语法(mermaid.js.org/syntax/c4.html)。Mermaid 的优势在于它已经被 GitHub、GitLab、Notion、Obsidian 等平台原生支持,不需要额外安装工具。
前面的 Level 1 和 Level 2 示例已经展示了 Mermaid C4 图的写法。这里补充一些实际使用中的注意事项:
- Mermaid C4 图目前只支持 C4Context、C4Container、C4Component、C4Dynamic 四种图类型,不支持 C4Deployment(部署视图)。
- Mermaid 的 C4 渲染和 Structurizr 的风格有差异,元素的默认颜色和布局不完全一致。
- 对于复杂的 C4 模型,Mermaid 的自动布局能力有限。图超过 15-20 个元素时,布局容易混乱。
PlantUML C4 扩展
PlantUML 有一个社区维护的 C4 扩展库(github.com/plantuml-stdlib/C4-PlantUML),提供了一套预定义的宏来画 C4 图。语法示例:
text
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
Person(customer, "普通用户", "浏览商品、下单")
System_Boundary(ecommerce, "电商系统") {
Container(api, "API 网关", "Go, Kong", "统一入口")
Container(order, "订单服务", "Java", "处理订单")
ContainerDb(db, "订单数据库", "MySQL", "存储订单")
}
System_Ext(payment, "支付网关", "处理支付")
Rel(customer, api, "调用", "HTTPS")
Rel(api, order, "路由", "gRPC")
Rel(order, db, "读写", "SQL")
Rel(order, payment, "支付", "HTTPS")
@enduml
PlantUML C4 扩展的优势在于 PlantUML 的生态成熟------支持嵌入到 AsciiDoc、Markdown(通过插件)、Wiki 等各种文档系统中。劣势在于 PlantUML 需要 Java 运行环境,CI 集成时需要额外配置。
三种工具的对比
| 维度 | Structurizr DSL | Mermaid C4 | PlantUML C4 |
|---|---|---|---|
| 模型与视图分离 | 支持 | 不支持(直接画图) | 不支持 |
| C4 图类型支持 | 完整(含部署视图) | 4 种基本类型 | 完整 |
| 平台集成 | 需要 Structurizr Lite/Cloud | GitHub/GitLab/Notion 原生 | 需要 Java + 插件 |
| 自动布局 | 较好 | 中等,复杂图易乱 | 较好 |
| 学习成本 | 中(DSL 语法) | 低 | 中 |
| 导出格式 | PNG/SVG/PlantUML/Mermaid | SVG/PNG | PNG/SVG |
| 适合场景 | 正式架构文档 | 轻量快速、嵌入 README | 已有 PlantUML 工具链的团队 |
我的建议是:如果团队刚开始尝试 diagram-as-code,从 Mermaid 开始。门槛最低,GitHub 仓库里直接渲染,不需要额外工具。当模型复杂到需要模型-视图分离时,再迁移到 Structurizr DSL。
CI 集成
Diagram-as-code 真正的价值在 CI 集成。把图的源文件放在代码仓库里之后,可以做到:
- Pull Request 里 review 架构变更 。修改了
.dsl或.mmd文件,reviewer 可以在 diff 里看到架构变更,和代码变更一起审查。 - CI 自动渲染。在 CI 管道里把 DSL/Mermaid 文件渲染成图片,部署到内部文档站。
- 变更通知。架构图文件变更时,自动通知相关团队。
一个简单的 CI 集成示例(GitHub Actions):
yaml
name: Render Architecture Diagrams
on:
push:
paths:
- 'docs/architecture/**/*.dsl'
- 'docs/architecture/**/*.mmd'
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Render Mermaid diagrams
uses: mermaid-js/mermaid-cli-action@v1
with:
input: docs/architecture/
output: build/diagrams/
- name: Deploy to docs site
uses: peaceiris/actions-gh-pages@v3
with:
publish_dir: build/diagrams/
这段配置的含义是:当 docs/architecture/ 目录下的 DSL 或 Mermaid 文件发生变更时,自动渲染并部署到文档站。
五、文档即代码:让架构文档活下来
画图只是架构文档的一部分。一个完整的架构文档还需要解释"为什么这样设计"、"做过哪些取舍"、"系统的约束是什么"。Diagram-as-code 解决了图的维护问题,文档即代码(Documentation as Code)解决了整个架构文档的维护问题。
ADR + C4 + README:架构文档三件套
实际工程中,架构文档不需要面面俱到。以下三样东西组合起来,已经能覆盖大多数团队的需求:
1. 架构决策记录(Architecture Decision Record, ADR)
ADR 记录的是"为什么"------为什么选择了这个数据库,为什么用消息队列而不是同步调用,为什么拆分了这个服务。每个 ADR 是一个简短的 Markdown 文件,包含标题、状态、上下文、决策和后果。
ADR 的概念由 Michael Nygard 在 2011 年的博文"Documenting Architecture Decisions"中提出。一个典型的 ADR 结构:
markdown
# ADR-003: 订单服务使用 MySQL 而非 PostgreSQL
## 状态
已接受(2026-03-15)
## 上下文
订单服务需要选择关系数据库。团队有 3 名工程师熟悉 MySQL,1 名熟悉 PostgreSQL。
订单数据的查询模式以主键查询和时间范围查询为主,不涉及复杂的 JSON 查询或地理空间查询。
## 决策
使用 MySQL 8.0 作为订单服务的主数据库。
## 理由
1. 团队 MySQL 经验更丰富,降低运维风险。
2. 订单数据的查询模式不需要 PostgreSQL 的高级特性。
3. 公司已有 MySQL DBA 团队和监控体系。
## 后果
- 如果未来需要复杂 JSON 查询,可能需要引入额外的存储(如 Elasticsearch)。
- 放弃了 PostgreSQL 在并发写入场景下 MVCC 的性能优势。
ADR 的关键实践要点:
- 每个决策一个文件,放在代码仓库的
docs/adr/目录下。 - ADR 一旦接受就不修改,只能通过新的 ADR 来替代旧的。这是为了保留决策历史------你可以追溯三个月前为什么选了方案 A,后来为什么又换成了方案 B。
- 编号递增,方便引用。在其他文档或代码注释中可以直接写"见 ADR-003"。
- 状态有四种:提议中(Proposed)、已接受(Accepted)、已弃用(Deprecated)、已替代(Superseded by ADR-XXX)。
ADR 的一个常见误解是"只记录大决策"。实际上,ADR 的粒度不是按决策的大小来定的,而是按可逆性来定的。选择用 gRPC 还是 HTTP/JSON 做服务间通信,这是一个成本很高、难以回退的决策,应该写 ADR。选择用哪个日志库,切换成本低,可以不写。
工具方面,adr-tools(github.com/npryce/adr-tools)是一个命令行工具,可以用 adr new "标题" 快速创建 ADR 文件,自动编号和生成模板。
2. C4 架构图
用前面介绍的 diagram-as-code 方式维护。Level 1 和 Level 2 是必须的,Level 3 按需补充。图文件和 ADR 放在同一个 docs/architecture/ 目录下,方便交叉引用。
3. README / 架构概述
每个代码仓库的 README 或独立的 ARCHITECTURE.md 文件,用 1-2 页的篇幅说明:系统做什么、主要的技术决策、各模块的职责、如何运行和部署。这个文件是新人入职的第一个入口。
Matklad(rust-analyzer 的作者)在 2021 年的博文"ARCHITECTURE.md"中倡导了一个做法:在项目根目录放一个 ARCHITECTURE.md 文件,用 500 字左右描述代码库的高层结构。不需要面面俱到,只需要回答"代码的入口在哪里、主要模块是什么、数据怎么流动"这三个问题。这个建议在开源社区获得了广泛认同。
这三样东西的关系是:README/ARCHITECTURE.md 告诉你"系统是什么",C4 图告诉你"系统长什么样",ADR 告诉你"为什么这样设计"。三者互相引用、互相补充。
文档目录结构
把三件套组织在一起,一个典型的目录结构是:
text
docs/
architecture/
c4/
workspace.dsl # Structurizr DSL 模型定义
system-context.mmd # Level 1 Mermaid 图
containers.mmd # Level 2 Mermaid 图
adr/
001-use-microservices.md
002-api-gateway-choice.md
003-order-db-mysql.md
004-async-with-rabbitmq.md
ARCHITECTURE.md # 架构概述
自动化架构验证
文档和代码分离的问题,光靠"大家记得更新文档"是解决不了的。更可靠的方式是自动化验证------用工具检查实际代码是否符合架构文档描述的结构。
ArchUnit 是 Java 生态中最成熟的架构验证工具(archunit.org)。它允许你用代码来表达架构规则,在单元测试阶段自动检查。
一个示例:假设 C4 容器图中,订单服务不应该直接调用用户数据库,只能通过用户服务的 API 来访问用户数据。这个约束可以用 ArchUnit 来验证:
java
@ArchTest
static final ArchRule order_service_should_not_access_user_db =
noClasses()
.that().resideInAPackage("..order..")
.should().accessClassesThat()
.resideInAPackage("..user.repository..")
.because("订单服务应通过用户服务 API 访问用户数据,而非直接访问用户数据库(见 C4 容器图)");
类似的工具在其他语言中也有:
- .NET:NetArchTest
- Go:go-arch-lint
- Python:import-linter
- TypeScript:dependency-cruiser
我认为架构验证工具是 diagram-as-code 的必要补充。仅仅把图放在 Git 里还不够,如果代码违反了图上描述的架构约束,CI 应该报错。否则图和代码迟早还是会脱节。
需要说明的是,架构验证工具目前的能力有限------它只能检查代码层面的依赖关系(谁 import 了谁),无法验证运行时的调用关系(服务 A 是否真的通过 HTTP 调用了服务 B)。跨服务的架构约束验证,需要结合 API 契约测试(contract testing)和服务网格(service mesh)的流量规则来实现。
文档更新的触发机制
文档最终能不能保持更新,取决于更新动作能否嵌入到已有的开发流程中。几种被验证有效的触发机制:
PR 模板提醒。在 Pull Request 模板中加一行 checklist:
markdown
- [ ] 本次变更是否影响架构图?如影响,已同步更新 `docs/architecture/` 下的相关文件。
这不是强制检查,但它的作用是提醒。开发者提交 PR 时看到这行 checklist,会思考一下自己的变更是否涉及架构层面。
CODEOWNERS 保护 。在 GitHub/GitLab 中,可以通过 CODEOWNERS 文件把 docs/architecture/ 目录设为需要特定 reviewer 审批。架构图的变更不会被随意修改,但也不会被忽略。
text
# .github/CODEOWNERS
docs/architecture/ @team-leads @architects
定期 review 会议。每个季度花 30 分钟,团队一起打开 Level 1 和 Level 2 图,和当前系统做一次对照。这个会议不需要长,目的就是把过期的部分找出来。
六、实战案例:一个微服务团队的 C4 落地过程
以下案例基于真实的工程经验,做了脱敏处理。
背景
一个 12 人的后端团队,维护一套电商相关的微服务系统,包含 8 个服务、3 种数据库、2 个消息队列。团队之前的架构文档情况:
- Confluence 上有一张两年前画的"系统架构图",包含大约 30 个方框,方框之间用箭头连接,没有图例,没有说明箭头含义。
- 每个服务的 README 只有"如何启动",没有架构信息。
- 没有 ADR,技术决策散落在 Jira ticket 的评论里。
- 新人入职平均需要 2 周才能理解系统全貌。
第一步:画 Level 1,对齐系统边界
团队花了一个下午开会,画出 Level 1 系统上下文图。过程中发现几个问题:
- 团队中不同人对"系统边界"的理解不一样。有人认为支付模块是自己系统的一部分,有人认为它是外部系统。
- 有一个内部的"数据同步服务",大家都知道它存在,但从来没有出现在任何架构图上。
光是画 Level 1 这个动作本身,就逼迫团队对齐了系统边界的认知。最终确认:系统边界内有 8 个自研服务,外部依赖 4 个系统(支付、物流、短信、数据平台)。
第二步:画 Level 2,选择技术方案
Level 2 容器图花了两天。团队选择用 Mermaid 语法,原因是公司用 GitLab,Mermaid 可以直接在 GitLab 的 Markdown 里渲染,不需要额外工具。
画 Level 2 时发现的关键问题:
- 两个服务之间存在循环依赖。订单服务调用库存服务扣减库存,库存服务又调用订单服务查询订单状态。这个循环在之前的方框线条图上看不出来,因为箭头没有方向语义。画 C4 容器图时,每条关系都要标明方向和协议,循环依赖一目了然。
- 有一个 Redis 实例同时被 5 个服务共享,但之前的架构图上只画了一个"Redis"方框,没有标明哪些服务在用。
第三步:引入 ADR
团队约定:从这一天开始,所有影响架构的决策都写 ADR。不需要长篇大论,500 字以内说清楚上下文、决策和后果。
前三个月写了 15 个 ADR,覆盖了:数据库选型、消息队列选型、服务拆分决策、缓存策略变更等。
第四步:CI 集成
在 GitLab CI 中加了一个 job:每次 docs/architecture/ 目录变更时,自动把 Mermaid 文件渲染成 PNG,部署到内部文档站。
另外加了一条规则:如果 PR 修改了服务间的 API 定义(Proto 文件或 OpenAPI spec),CI 会提示"请检查架构图是否需要同步更新"。不是强制的,但起到了提醒作用。
具体实现是在 .gitlab-ci.yml 中加了一个 stage:
yaml
check-architecture-docs:
stage: lint
rules:
- changes:
- "proto/**/*"
- "api/openapi/**/*"
script:
- echo "Proto 或 API 定义发生了变更,请确认 docs/architecture/ 下的架构图是否需要同步更新。"
- echo "如果不需要更新,请在 MR 描述中说明原因。"
allow_failure: true
allow_failure: true 意味着这个检查不会阻断 CI,但会在 MR 页面上显示一个警告。团队讨论过是否设为强制检查,最终决定不强制------因为很多 Proto 文件的变更(比如加一个字段)确实不影响容器级别的架构图。强制检查会让开发者对检查产生免疫,反而降低了提醒效果。
效果
实施 6 个月后的变化:
- 新人入职理解系统全貌的时间从 2 周缩短到 3 天。Level 1 和 Level 2 图是入职材料的第一页。
- 架构图的更新频率从"几乎不更新"变成了"平均每月 2-3 次"。因为图在代码仓库里,改图和改代码的流程一样。
- ADR 积累到 30+ 个,成为团队讨论技术决策时的标准工具。"你去写个 ADR"比"你去写个技术方案文档"的执行阻力小得多------一个 ADR 500 字就够了,一个"技术方案文档"动辄 5000 字。
- 发现并修复了 2 个架构级别的问题(循环依赖、共享 Redis 的隔离性问题),这些问题在之前的方框图上完全看不出来。
- 技术评审会议的效率明显提升。之前评审时大家对着白板画一遍系统结构,现在直接打开容器图,讨论变更的部分。
踩过的坑
- Level 3 图的维护成本比预期高。团队尝试为每个服务画 Level 3 组件图,但代码重构后组件图频繁过期。最终决定只为核心服务(订单服务和支付服务)维护 Level 3 图,其他服务只保留 Level 2。
- Mermaid 的 C4 语法不稳定。Mermaid 的 C4 支持在早期版本中有一些 bug,图的渲染效果在 GitLab 和本地 preview 工具中不完全一致。团队固定了 Mermaid 版本来缓解这个问题。
- ADR 的"什么时候该写"不好判断。一开始要求所有决策都写 ADR,结果连"用 log4j 还是 logback"这种小事也写了 ADR。后来约定:只有影响两个以上服务、或者不可逆转的决策才写 ADR。
- C4 图上的命名和代码中的命名不一致 。容器图上写的是"订单服务",代码仓库叫
order-svc,Kubernetes deployment 叫order-service-v2。三个名字指的是同一个东西,但新人不知道。后来在容器图上统一标注了代码仓库名和部署名。
关键反思
回头看,C4 落地最大的阻力不是工具和方法,而是习惯。团队花了大约两个月才建立起"改代码的时候顺便看一下图"的习惯。在这之前,需要 Tech Lead 在 code review 时不断提醒。
另一个反思:不要在一开始就追求完美。第一版 Level 1 图就是在白板上画的,用手机拍照存在了 GitLab 的 issue 里。等团队达成共识后,才用 Mermaid 重新画了一版。如果一上来就要求"用 Structurizr DSL 写一个完整的 C4 模型",大概率会因为学习成本太高而放弃。
七、架构文档方法综合对比
下面这张表把常见的架构文档方法放在一起做一个全面的对比。评判维度包括学习成本、维护成本、沟通效果和工程化程度。
| 方法 | 学习成本 | 维护成本 | 非技术人员可读性 | 版本管理 | CI 集成 | 适合阶段 | 典型问题 |
|---|---|---|---|---|---|---|---|
| 白板 / 便签 | 无 | 无法维护 | 高 | 不支持 | 不支持 | 头脑风暴 | 画完就丢 |
| Visio / Draw.io | 低 | 高(手动更新) | 中 | 困难(二进制文件) | 不支持 | 小团队初期 | 与代码脱节 |
| PowerPoint | 低 | 高 | 高 | 困难 | 不支持 | 汇报演示 | 不是工程文档 |
| UML(完整) | 高 | 很高 | 低 | 取决于工具 | 取决于工具 | 管制行业 | 过度建模 |
| Mermaid C4 | 低 | 低 | 中 | Git 原生 | 简单 | 各阶段 | 复杂图布局差 |
| Structurizr DSL | 中 | 低到中 | 中 | Git 原生 | 中等 | 中大型系统 | 需要额外工具 |
| PlantUML C4 | 中 | 低到中 | 中 | Git 原生 | 中等(需 Java) | 已有 PlantUML 的团队 | 依赖 Java |
| arc42 + C4 | 中 | 中 | 中 | Git 原生 | 支持 | 大型系统 | 全部写完工作量大 |
| ADR | 低 | 很低 | 中高 | Git 原生 | 简单 | 所有团队 | 判断写 ADR 的时机 |
这张表的核心结论:没有万能方案。白板适合头脑风暴,Mermaid 适合日常维护,Structurizr 适合正式建模,ADR 适合记录决策。关键是根据团队规模和系统复杂度选择组合,而不是追求一个完美方案。
一个实际的选型建议:
- 5 人以下小团队,单体或少量服务:Mermaid C4(Level 1 + Level 2) + ADR,够了。
- 10-30 人团队,微服务架构:Mermaid 或 Structurizr DSL + ADR + ARCHITECTURE.md + CI 集成。
- 50 人以上大团队,多个子系统:Structurizr DSL + arc42 模板 + ADR + ArchUnit 验证 + 专人维护。
八、C4 模型的局限与补充视图
C4 模型的四层视图覆盖了"系统结构"这个维度,但架构描述不只是结构。C4 模型自身也承认这一点,提出了两种补充视图。
动态图(Dynamic Diagram)
C4 的四层视图都是静态的------它们展示系统在某个时刻的结构,不展示运行时行为。当你需要说明"一次下单请求经过了哪些服务、调用了哪些接口"时,静态视图不够用。
C4 的动态图(Dynamic Diagram)本质上是带有 C4 元素的序列图或协作图。它从容器图或组件图中选取相关元素,按照时间顺序展示一个特定场景的调用流程。
部署图(Deployment Diagram)
C4 的部署图展示容器如何映射到基础设施(服务器、容器编排平台、云服务)。容器图告诉你有哪些部署单元,部署图告诉你这些单元跑在哪里。
部署图在运维人员和 SRE 的日常工作中特别有用。它回答的问题包括:每个服务部署了几个实例?数据库是单机还是主从?流量入口在哪里?
C4 没有覆盖的领域
C4 模型不处理以下内容:
- 数据模型:实体之间的关系、数据库表结构。需要 ER 图或领域模型图来补充。
- 网络拓扑:VLAN、子网、防火墙规则。需要网络拓扑图。
- 运行时行为:除了动态图展示的请求流程,状态机、事件流、并发模型等需要其他图来表达。
- 非功能性约束:性能指标、可用性目标、安全策略。需要文字描述或专门的架构视图。
这不是 C4 的缺陷,而是它的设计边界。C4 的目标是"用最简单的方式描述软件结构",不是替代所有架构描述方法。实际操作中,C4 的四层视图 + 动态图 + 部署图覆盖了大约 80% 的架构沟通需求,剩下的 20% 用其他工具补充。
九、常见误区与实践建议
误区一:图越详细越好
这是最常见的错误。有人画了一张包含 50 个方框的"架构图",试图在一张图上展示所有信息。结果谁都看不懂。
C4 模型的核心原则就是分层。如果一张图上的元素超过 20 个,考虑把它拆分到下一层。Level 1 通常只有 5-10 个元素,Level 2 通常 10-20 个,Level 3 也不应该超过 20 个。
误区二:所有服务都需要 Level 3
Level 3 组件图的维护成本不低。对于简单的 CRUD 服务,Level 2 容器图已经提供了足够的信息。只有核心服务、复杂服务、或者新人难以理解的服务,才需要 Level 3。
判断标准很简单:如果一个服务的内部结构,一个新工程师看代码目录就能理解,那不需要 Level 3。如果服务内部有复杂的事件驱动逻辑、多个状态机、或者非直觉的模块依赖,才值得画 Level 3。
误区三:架构图等于架构文档
图只是架构文档的一部分。图能告诉你"系统长什么样",但不能告诉你"为什么这样设计"。没有 ADR 的架构图,就像没有注释的代码------你能看到它做了什么,但不知道为什么。
一个完整的架构文档至少要回答三个问题:是什么(C4 图)、为什么(ADR)、怎么跑(部署文档和运维手册)。只画图不写 ADR,三个月后你自己都记不清当初为什么做了那个决策。
误区四:一次性把所有图都画完
比起一次性画完所有图然后逐渐过期,更好的策略是:先画 Level 1 和 Level 2,然后随着需求逐步补充。有新人入职、有技术评审、有事故复盘------这些都是补充和更新架构图的好时机。
误区五:把 C4 的"容器"和 Docker 容器混淆
这是初学者最常犯的错误。C4 模型中的 Container 是"可独立运行的进程或部署单元",和 Docker 容器(container)是完全不同的概念。一个 C4 Container 可以是一个 Java JAR、一个 Node.js 进程、一个数据库实例,也可以是一个部署在 Docker 容器中的微服务------但它和 Docker 没有绑定关系。Simon Brown 自己也承认这个命名造成了混淆,但因为 C4 模型在 Docker 流行之前就已经定名,改名的成本太高。
实践建议清单
- 从 Level 1 开始。花一个小时和团队一起画出系统上下文图。这个过程本身就有价值------它会逼你对齐系统边界。
- 图放在代码仓库里。不管用 Mermaid、Structurizr 还是 PlantUML,源文件和代码在一起。
- 架构变更和代码变更一起 review。修改了服务间的接口?请同时更新容器图。
- 每个重要决策写 ADR。500 字以内,写清楚上下文、决策和后果。
- 不追求完美。一张 80% 准确的架构图,比一张 100% 准确但永远画不完的架构图有用得多。
- 定期检查。每个季度花一个小时看一下 Level 1 和 Level 2 图,确认和实际系统是否一致。
十、结论
架构文档的核心矛盾是:人人都知道它重要,但大多数团队做不好。做不好的原因不是态度问题,而是方法和工具的问题------没有分层、没有语义、没有工程化手段。
C4 模型提供了一个实用的分层框架:Level 1 对齐系统边界,Level 2 展示部署结构,Level 3 深入容器内部,Level 4 交给 IDE 自动生成。diagram-as-code 工具让图可以被版本管理、diff、review。ADR 记录了"为什么"。三者组合起来,架构文档就能活下来。
但工具不是万能的。架构文档最终能不能维护下去,取决于团队是否把"更新文档"视为开发流程的一部分,而不是额外负担。最好的做法是把架构图的更新嵌入到已有的工程流程中------代码 review 时顺便看一下图是否需要更新,Sprint 回顾时检查一下 ADR 是否有遗漏。
架构文档不需要完美,但它需要活着。
上一篇:复杂度管理
下一篇:单体架构
参考资料
标准与规范
- ISO/IEC/IEEE 42010:2022, "Systems and software engineering --- Architecture description"
- UML 2.5.1, Object Management Group (OMG), 2017
书籍与论文
- Simon Brown, "Software Architecture for Developers", Leanpub, 2022
- Simon Brown, "The C4 Model for Visualising Software Architecture", c4model.com
- Philippe Kruchten, "Architectural Blueprints---The 4+1 View Model of Software Architecture", IEEE Software, 1995
- Michael Nygard, "Documenting Architecture Decisions", 2011, cognitect.com/blog
工具与项目
- Structurizr DSL: structurizr.com/dsl
- Structurizr Lite: structurizr.com/help/lite
- Mermaid C4 Diagram: mermaid.js.org/syntax/c4.html
- PlantUML C4 Extension: github.com/plantuml-stdlib/C4-PlantUML
- ArchUnit: archunit.org
- arc42 Template: arc42.org
- dependency-cruiser (TypeScript): github.com/sverweij/dependency-cruiser
- import-linter (Python): github.com/seddonym/import-linter
博客与演讲
- Simon Brown, "C4 Model" talk, various conferences (2018-2024)
- Gernot Starke, Peter Hruschka, "arc42 in Practice", arc42.org/documentation