土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】架构视图与文档:C4 模型从入门到实战

文章导航

分类入口
architecture
标签入口
#c4-model#architecture-documentation#structurizr#mermaid#diagram-as-code#adr#arc42

目录

你上次打开团队的架构图是什么时候?

大多数团队都有架构图。Confluence 上躺着一张两年前画的系统拓扑图,Visio 文件在某个共享盘里,PowerPoint 里有几页”技术方案评审”的框线图。问题是:没人信它们。新人入职时看一眼,发现和实际系统对不上,从此再也不看。老人心里有一张”真正的架构图”,但那张图只存在于他的脑子里。

这不是个别现象。Simon Brown 在 2018 年的调查中发现,超过半数的开发者认为自己团队的架构文档”基本没用”或”严重过时”(Simon Brown, “The C4 Model for Visualising Software Architecture”, c4model.com)。

架构图的困境不在于没人愿意画,而在于三个结构性问题:

  1. 没有统一语义。同一张图上,一个方框可能代表一个进程、一个服务、一个类,也可能代表一个团队。看图的人只能猜。
  2. 没有层次。一张图试图同时展示系统边界、部署单元、内部模块和代码结构,结果什么都看不清。
  3. 没有工程化手段维护。图画在 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)

系统上下文图回答一个问题:这个系统是什么,谁在用它,它和哪些外部系统交互?

这是最粗粒度的视图。它只有三类元素:

一张好的上下文图,非技术人员也能看懂。它的目标受众是产品经理、业务方、新加入团队的工程师。

下面是一个电商系统的 Level 1 示例:

C4Context
    title 电商系统 - 系统上下文图

    Person(customer, "普通用户", "浏览商品、下单、查看订单")
    Person(admin, "运营管理员", "管理商品、处理订单、查看报表")

    System(ecommerce, "电商系统", "提供商品浏览、下单、支付、物流跟踪等功能")

    System_Ext(payment, "支付网关", "支付宝/微信支付,处理在线支付")
    System_Ext(logistics, "物流系统", "第三方物流公司,提供运单查询")
    System_Ext(sms, "短信服务", "发送验证码和通知短信")

    Rel(customer, ecommerce, "浏览、下单、查看订单", "HTTPS")
    Rel(admin, ecommerce, "管理商品和订单", "HTTPS")
    Rel(ecommerce, payment, "发起支付请求", "HTTPS/API")
    Rel(ecommerce, logistics, "查询物流信息", "HTTPS/API")
    Rel(ecommerce, sms, "发送通知", "HTTPS/API")

这张图传递了几个关键信息:电商系统有两类用户(普通用户和运营管理员),依赖三个外部系统(支付、物流、短信)。一个新加入团队的工程师,看完这张图就知道系统的边界在哪里。

Level 2:容器图(Container Diagram)

容器(Container)在 C4 模型中有特定含义:一个可独立运行的进程或可部署单元。Web 应用、移动 App、数据库实例、消息队列、文件存储——这些都是容器。注意,这里的”容器”和 Docker 容器不是一个概念。

容器图回答的问题是:系统内部由哪些可部署单元组成,它们之间如何通信?

目标受众是开发人员和运维人员。这张图上,你能看到技术选型(用了什么语言、什么框架、什么数据库)和通信方式(HTTP、gRPC、消息队列)。

继续用电商系统的例子:

C4Container
    title 电商系统 - 容器图

    Person(customer, "普通用户", "浏览商品、下单")

    System_Boundary(ecommerce, "电商系统") {
        Container(web, "Web 前端", "React, Nginx", "提供用户界面")
        Container(api, "API 网关", "Go, Kong", "统一入口,路由、限流、认证")
        Container(order, "订单服务", "Java, Spring Boot", "处理订单生命周期")
        Container(product, "商品服务", "Java, Spring Boot", "商品信息的增删改查")
        Container(user, "用户服务", "Go", "注册、登录、用户信息管理")
        ContainerDb(orderdb, "订单数据库", "MySQL", "存储订单数据")
        ContainerDb(productdb, "商品数据库", "MySQL", "存储商品数据")
        ContainerDb(userdb, "用户数据库", "PostgreSQL", "存储用户信息")
        ContainerQueue(mq, "消息队列", "RabbitMQ", "异步事件通知")
        Container(cache, "缓存", "Redis", "热点数据缓存")
    }

    System_Ext(payment, "支付网关", "支付宝/微信支付")

    Rel(customer, web, "访问", "HTTPS")
    Rel(web, api, "调用", "HTTPS/JSON")
    Rel(api, order, "路由请求", "gRPC")
    Rel(api, product, "路由请求", "gRPC")
    Rel(api, user, "路由请求", "gRPC")
    Rel(order, orderdb, "读写", "TCP/SQL")
    Rel(product, productdb, "读写", "TCP/SQL")
    Rel(user, userdb, "读写", "TCP/SQL")
    Rel(order, mq, "发布订单事件", "AMQP")
    Rel(product, cache, "读取缓存", "TCP")
    Rel(order, payment, "发起支付", "HTTPS/API")

从上下文图到容器图,信息密度明显增加。你能看到:系统内部分成了哪几个服务,每个服务用了什么技术栈,服务之间通过什么协议通信,数据存储在哪里。但注意,这张图仍然不涉及任何代码级别的细节。

Level 3:组件图(Component Diagram)

组件图把一个容器打开,展示内部的主要构建块。这里的”组件”指的是一组相关的功能,通常对应代码中的一个模块、一个包(package)或一组紧密协作的类。

组件图回答的问题是:某个容器内部是怎么组织的?

目标受众是负责这个容器的开发团队。对于不负责这个容器的人来说,容器图的粒度通常就够了。

以”订单服务”为例,组件图展示了服务内部的主要模块及其关系:

C4Component
    title 订单服务 - 组件图

    Container_Boundary(order, "订单服务") {
        Component(controller, "OrderController", "Spring MVC", "接收 gRPC/HTTP 请求,参数校验")
        Component(service, "OrderService", "Spring Bean", "订单核心业务逻辑:创建、取消、退款")
        Component(validator, "OrderValidator", "Spring Bean", "订单规则校验:库存、限购、风控")
        Component(repo, "OrderRepository", "MyBatis", "订单数据读写")
        Component(payClient, "PaymentClient", "HTTP Client", "调用支付网关 API")
        Component(eventPub, "OrderEventPublisher", "RabbitMQ Client", "发布订单领域事件")
    }

    ContainerDb(orderdb, "订单数据库", "MySQL")
    ContainerQueue(mq, "消息队列", "RabbitMQ")
    System_Ext(payment, "支付网关", "支付宝/微信支付")

    Rel(controller, service, "调用")
    Rel(service, validator, "校验订单")
    Rel(service, repo, "读写订单")
    Rel(service, payClient, "发起支付")
    Rel(service, eventPub, "发布事件")
    Rel(repo, orderdb, "SQL")
    Rel(payClient, payment, "HTTPS")
    Rel(eventPub, mq, "AMQP")

这张图传达了几个关键信息:请求从 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 图有用的少数场景包括:

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 的落地一直比较困难。原因有几个:

  1. 五个视图的边界不清晰。逻辑视图和开发视图的区别在哪?进程视图和物理视图的关系是什么?不同的人有不同的理解。
  2. 没有提供具体的图示规范。4+1 说了要画什么,但没说怎么画。落地时还是要依赖 UML 或其他符号系统。
  3. 对小团队来说太重。维护五个视图需要大量精力,大多数团队做不到。

关于 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 关键部分):

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 的几个设计特点值得注意:

  1. 模型和视图分离。先定义元素和关系,再决定哪些元素出现在哪张图上。一个模型可以生成多张不同的视图。
  2. 关系只定义一次orderService -> payment "发起支付" 这条关系定义一次,在上下文图和容器图上都能自动显示。
  3. 支持多种渲染方式。同一份 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 图的写法。这里补充一些实际使用中的注意事项:

PlantUML C4 扩展

PlantUML 有一个社区维护的 C4 扩展库(github.com/plantuml-stdlib/C4-PlantUML),提供了一套预定义的宏来画 C4 图。语法示例:

@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 集成。把图的源文件放在代码仓库里之后,可以做到:

  1. Pull Request 里 review 架构变更。修改了 .dsl.mmd 文件,reviewer 可以在 diff 里看到架构变更,和代码变更一起审查。
  2. CI 自动渲染。在 CI 管道里把 DSL/Mermaid 文件渲染成图片,部署到内部文档站。
  3. 变更通知。架构图文件变更时,自动通知相关团队。

一个简单的 CI 集成示例(GitHub Actions):

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 结构:

# 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 的关键实践要点:

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 告诉你”为什么这样设计”。三者互相引用、互相补充。

文档目录结构

把三件套组织在一起,一个典型的目录结构是:

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 来验证:

@ArchTest
static final ArchRule order_service_should_not_access_user_db =
    noClasses()
        .that().resideInAPackage("..order..")
        .should().accessClassesThat()
        .resideInAPackage("..user.repository..")
        .because("订单服务应通过用户服务 API 访问用户数据,而非直接访问用户数据库(见 C4 容器图)");

类似的工具在其他语言中也有:

我认为架构验证工具是 diagram-as-code 的必要补充。仅仅把图放在 Git 里还不够,如果代码违反了图上描述的架构约束,CI 应该报错。否则图和代码迟早还是会脱节。

需要说明的是,架构验证工具目前的能力有限——它只能检查代码层面的依赖关系(谁 import 了谁),无法验证运行时的调用关系(服务 A 是否真的通过 HTTP 调用了服务 B)。跨服务的架构约束验证,需要结合 API 契约测试(contract testing)和服务网格(service mesh)的流量规则来实现。

文档更新的触发机制

文档最终能不能保持更新,取决于更新动作能否嵌入到已有的开发流程中。几种被验证有效的触发机制:

PR 模板提醒。在 Pull Request 模板中加一行 checklist:

- [ ] 本次变更是否影响架构图?如影响,已同步更新 `docs/architecture/` 下的相关文件。

这不是强制检查,但它的作用是提醒。开发者提交 PR 时看到这行 checklist,会思考一下自己的变更是否涉及架构层面。

CODEOWNERS 保护。在 GitHub/GitLab 中,可以通过 CODEOWNERS 文件把 docs/architecture/ 目录设为需要特定 reviewer 审批。架构图的变更不会被随意修改,但也不会被忽略。

# .github/CODEOWNERS
docs/architecture/ @team-leads @architects

定期 review 会议。每个季度花 30 分钟,团队一起打开 Level 1 和 Level 2 图,和当前系统做一次对照。这个会议不需要长,目的就是把过期的部分找出来。


六、实战案例:一个微服务团队的 C4 落地过程

以下案例基于真实的工程经验,做了脱敏处理。

背景

一个 12 人的后端团队,维护一套电商相关的微服务系统,包含 8 个服务、3 种数据库、2 个消息队列。团队之前的架构文档情况:

第一步:画 Level 1,对齐系统边界

团队花了一个下午开会,画出 Level 1 系统上下文图。过程中发现几个问题:

  1. 团队中不同人对”系统边界”的理解不一样。有人认为支付模块是自己系统的一部分,有人认为它是外部系统。
  2. 有一个内部的”数据同步服务”,大家都知道它存在,但从来没有出现在任何架构图上。

光是画 Level 1 这个动作本身,就逼迫团队对齐了系统边界的认知。最终确认:系统边界内有 8 个自研服务,外部依赖 4 个系统(支付、物流、短信、数据平台)。

第二步:画 Level 2,选择技术方案

Level 2 容器图花了两天。团队选择用 Mermaid 语法,原因是公司用 GitLab,Mermaid 可以直接在 GitLab 的 Markdown 里渲染,不需要额外工具。

画 Level 2 时发现的关键问题:

第三步:引入 ADR

团队约定:从这一天开始,所有影响架构的决策都写 ADR。不需要长篇大论,500 字以内说清楚上下文、决策和后果。

前三个月写了 15 个 ADR,覆盖了:数据库选型、消息队列选型、服务拆分决策、缓存策略变更等。

第四步:CI 集成

在 GitLab CI 中加了一个 job:每次 docs/architecture/ 目录变更时,自动把 Mermaid 文件渲染成 PNG,部署到内部文档站。

另外加了一条规则:如果 PR 修改了服务间的 API 定义(Proto 文件或 OpenAPI spec),CI 会提示”请检查架构图是否需要同步更新”。不是强制的,但起到了提醒作用。

具体实现是在 .gitlab-ci.yml 中加了一个 stage:

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 个月后的变化:

踩过的坑

  1. Level 3 图的维护成本比预期高。团队尝试为每个服务画 Level 3 组件图,但代码重构后组件图频繁过期。最终决定只为核心服务(订单服务和支付服务)维护 Level 3 图,其他服务只保留 Level 2。
  2. Mermaid 的 C4 语法不稳定。Mermaid 的 C4 支持在早期版本中有一些 bug,图的渲染效果在 GitLab 和本地 preview 工具中不完全一致。团队固定了 Mermaid 版本来缓解这个问题。
  3. ADR 的”什么时候该写”不好判断。一开始要求所有决策都写 ADR,结果连”用 log4j 还是 logback”这种小事也写了 ADR。后来约定:只有影响两个以上服务、或者不可逆转的决策才写 ADR。
  4. 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 适合记录决策。关键是根据团队规模和系统复杂度选择组合,而不是追求一个完美方案。

一个实际的选型建议:


八、C4 模型的局限与补充视图

C4 模型的四层视图覆盖了”系统结构”这个维度,但架构描述不只是结构。C4 模型自身也承认这一点,提出了两种补充视图。

动态图(Dynamic Diagram)

C4 的四层视图都是静态的——它们展示系统在某个时刻的结构,不展示运行时行为。当你需要说明”一次下单请求经过了哪些服务、调用了哪些接口”时,静态视图不够用。

C4 的动态图(Dynamic Diagram)本质上是带有 C4 元素的序列图或协作图。它从容器图或组件图中选取相关元素,按照时间顺序展示一个特定场景的调用流程。

C4Dynamic
    title 下单流程 - 动态视图

    ContainerDb(orderdb, "订单数据库", "MySQL")
    Container(api, "API 网关", "Go")
    Container(order, "订单服务", "Java")
    Container(product, "商品服务", "Java")
    ContainerQueue(mq, "消息队列", "RabbitMQ")

    Rel(api, order, "1. 创建订单请求", "gRPC")
    Rel(order, product, "2. 校验库存", "gRPC")
    Rel(order, orderdb, "3. 写入订单", "SQL")
    Rel(order, mq, "4. 发布订单创建事件", "AMQP")
    Rel(order, api, "5. 返回订单号", "gRPC")

部署图(Deployment Diagram)

C4 的部署图展示容器如何映射到基础设施(服务器、容器编排平台、云服务)。容器图告诉你有哪些部署单元,部署图告诉你这些单元跑在哪里。

部署图在运维人员和 SRE 的日常工作中特别有用。它回答的问题包括:每个服务部署了几个实例?数据库是单机还是主从?流量入口在哪里?

C4 没有覆盖的领域

C4 模型不处理以下内容:

这不是 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 流行之前就已经定名,改名的成本太高。

实践建议清单

  1. 从 Level 1 开始。花一个小时和团队一起画出系统上下文图。这个过程本身就有价值——它会逼你对齐系统边界。
  2. 图放在代码仓库里。不管用 Mermaid、Structurizr 还是 PlantUML,源文件和代码在一起。
  3. 架构变更和代码变更一起 review。修改了服务间的接口?请同时更新容器图。
  4. 每个重要决策写 ADR。500 字以内,写清楚上下文、决策和后果。
  5. 不追求完美。一张 80% 准确的架构图,比一张 100% 准确但永远画不完的架构图有用得多。
  6. 定期检查。每个季度花一个小时看一下 Level 1 和 Level 2 图,确认和实际系统是否一致。

十、结论

架构文档的核心矛盾是:人人都知道它重要,但大多数团队做不好。做不好的原因不是态度问题,而是方法和工具的问题——没有分层、没有语义、没有工程化手段。

C4 模型提供了一个实用的分层框架:Level 1 对齐系统边界,Level 2 展示部署结构,Level 3 深入容器内部,Level 4 交给 IDE 自动生成。diagram-as-code 工具让图可以被版本管理、diff、review。ADR 记录了”为什么”。三者组合起来,架构文档就能活下来。

但工具不是万能的。架构文档最终能不能维护下去,取决于团队是否把”更新文档”视为开发流程的一部分,而不是额外负担。最好的做法是把架构图的更新嵌入到已有的工程流程中——代码 review 时顺便看一下图是否需要更新,Sprint 回顾时检查一下 ADR 是否有遗漏。

架构文档不需要完美,但它需要活着。


上一篇:复杂度管理

下一篇:单体架构


参考资料

标准与规范

书籍与论文

工具与项目

博客与演讲

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构师工具箱:建模、可视化与决策辅助

架构设计不是凭空想象,而是需要工具辅助的系统性工程。从最初的白板画图到如今的代码化架构描述(Architecture as Code),架构师手中的工具箱经历了深刻的变革。一个成熟的架构团队,至少需要在三个维度上配备趁手的工具:建模与描述——将头脑中的架构意图精确表达出来;可视化与沟通——让不同角色的干系人都能理解架构…

2026-04-13 · architecture

【系统架构设计百科】架构治理:适应度函数与演进式架构

架构决策写在文档里,三个月后没人记得;架构评审会上达成的共识,半年后被新来的同事无意打破。这是每一个经历过大型系统演进的架构师都深有体会的痛点。当系统规模超过 50 个微服务、团队人数突破 200 人时,仅靠文档和评审来守住架构约束,几乎不可能。Netflix 在 2018 年提出的「铺好的路(Paved Road)」…

2026-04-13 · architecture

【系统架构设计百科】架构决策与 ADR:如何做出可追溯的技术决策

口头约定的架构决策会在人员流动中丢失,会在争论中反复翻车。ADR(Architecture Decision Records)用一种轻量的文档格式,把每一个关键技术决策的背景、选项、理由和代价写下来,跟着代码一起版本管理。本文从 ADR 的三种主流格式讲到 Git 仓库中的实操管理,再拆解 Spotify 和 Uber 的工业实践。

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。


By .