C4 Model 与 Structurizr DSL、视图和架构漂移治理手册
三张图里的“订单服务”为什么不是同一个东西
一次支付故障复盘里,系统上下文图把“订单平台”画成一个系统,容器图把 orders-api 画成一个进程,部署图却把 orders-api、数据库和消息主题塞进同一个“订单服务”框。三张图分别由三个人复制修改,名称相同,抽象层级和责任边界却不同。评审者无法判断变更到底是代码模块、可部署进程,还是整个软件系统。
C4 Model 先解决语言问题:用少量固定抽象逐级放大系统。Structurizr 再解决一致性问题:元素和关系只在模型中定义一次,多张图只是同一模型的不同视图。修改一个元素名称会影响所有引用它的视图,不再依赖人工逐图替换。
这并不等于“写了 DSL,架构就自动正确”。解析器能发现不存在的标识符,却不知道代码仓库里是否已经多出一个未建模服务,也不知道箭头写反了。可靠链路要同时包含模型校验、可视化评审、代码事实映射和变更触发规则。
下面用 Docker、Git 和浏览器建立一套本地可复现环境。示例固定 Structurizr 产品版本 2026.05.22;落到项目时先在官方二进制页面确认支持版本和镜像变体,再由依赖更新流程升级。示例中的系统、域名、环境和凭证均为虚构值。
C4 的四级缩放不是四张必画清单
C4 的核心静态结构由四个层级组成:
| 层级 | 回答的问题 | 主要元素 | 常见误用 |
|---|---|---|---|
| System Context | 这个系统服务谁,与哪些外部系统交互 | 人、软件系统 | 把内部微服务全塞进来 |
| Container | 系统内部由哪些可运行应用或数据存储组成 | 应用、数据存储 | 把 Docker 容器当成唯一含义 |
| Component | 某个 container 内部有哪些主要职责单元 | 组件 | 把每个类都画成组件 |
| Code | 关键组件怎样由代码元素实现 | 类、接口、函数、表等 | 长期手工维护全部类图 |
C4 中的 container 是应用或数据存储的逻辑运行单元,例如服务端应用、SPA、移动应用、数据库 schema 或对象存储 bucket;它不等同于 Docker container。Component 是 container 内部有清晰责任与接口的结构,不是“所有 package”。Code 级通常由 IDE 或分析工具按需生成,长期手工维护的成本很高。C4 官方图类型说明也指出,很多团队只需上下文图和容器图就能获得主要价值。
系统全景图、动态视图和部署视图是补充视角。动态视图描述一次场景中关系发生的顺序;部署视图把 container instance 放入具体环境的 deployment node;它们不能用来混写静态责任。一个 API 在容器图中出现一次,在生产部署图中可以有多个实例,这正是“逻辑模型”和“物理部署”分开的价值。
每张图都应有标题、范围、图例;元素写名称、类型和简短责任,container/component 写技术;关系使用单向箭头并标注意图,跨进程关系标注协议。颜色不是 C4 语义,蓝框和灰框也不是规范要求。完整检查可对照 C4 图评审清单。
先避开已经停止演进的入口
旧教程常要求启动 structurizr/lite,再安装独立的 Structurizr CLI。现在这两个入口都已进入 EOL:Lite 由 local 命令替代,旧 CLI 由统一二进制中的 push、pull、export、validate、inspect 等命令替代。旧服务还能用于迁移读取,但不再接收功能、缺陷和安全更新,不应成为新项目基线。官方迁移状态见 Structurizr EOL 页面。
对应关系很直接:
| 旧入口 | 当前入口 | 迁移动作 |
|---|---|---|
structurizr/lite | structurizr/structurizr ... local | 备份 workspace.dsl、workspace.json、文档、ADR、图标和主题后切换镜像 |
独立 structurizr-cli | structurizr/structurizr ... <command> 或统一 WAR | 把 CI 中的脚本改为新命令,再比较导出物 |
| on-premises | server | 核对许可、认证、存储和升级路径 |
| cloud | local、server 或静态导出 | 先导出 workspace,再迁移发布入口和权限 |
Lite 只适合单人访问同一个数据目录。多个用户并发访问同一实例,或多个 Lite 实例同时写同一目录,可能造成 workspace 内容损坏或布局丢失。迁移时先停掉所有旧实例,复制整个数据目录,再在副本上启动 local;不能让新旧进程同时“试跑”同一挂载目录。
创建第一个可运行 workspace
项目目录采用 DSL 为模型事实源,JSON 保存需要人工布局时产生的坐标信息:
your-project/
docs/
architecture/
structurizr/
workspace.dsl
workspace.json
structurizr.properties
README.md
rendered/
index.html
scripts/
architecture-check.sh将下面内容保存为 workspace.dsl:
workspace "Order Platform" "Places orders and coordinates payment" {
!identifiers hierarchical
model {
customer = person "Customer" "Places and tracks orders"
support = person "Support Agent" "Investigates failed orders"
payment = softwareSystem "Payment Provider" "Authorises payments"
ordering = softwareSystem "Order Platform" "Accepts and fulfils orders" {
web = container "Web Application" "Customer order UI" "TypeScript"
api = container "Orders API" "Owns order lifecycle" "Java and Spring Boot" {
orderController = component "Order Controller" "Exposes the order HTTP API"
orderService = component "Order Service" "Coordinates order state changes"
paymentGateway = component "Payment Gateway" "Calls the payment provider"
orderController -> orderService "Invokes"
orderService -> paymentGateway "Requests payment authorisation"
}
database = container "Orders Database" "Stores order state" "PostgreSQL"
web -> api "Calls" "JSON/HTTPS"
api -> database "Reads from and writes to" "JDBC/TLS"
ordering.api.paymentGateway -> payment "Authorises payment" "JSON/HTTPS"
}
customer -> ordering "Places and tracks orders"
support -> ordering "Investigates failed orders"
production = deploymentEnvironment "Production" {
edge = deploymentNode "Application Runtime" "Managed container platform" "Linux" {
webInstance = containerInstance ordering.web
apiInstance = containerInstance ordering.api
}
data = deploymentNode "Data Runtime" "Managed database service" "PostgreSQL" {
databaseInstance = containerInstance ordering.database
}
}
}
views {
systemContext ordering "OrderPlatformContext" "People and systems around the order platform" {
include *
autoLayout lr
}
container ordering "OrderPlatformContainers" "Applications and data stores" {
include *
autoLayout lr
}
component ordering.api "OrdersApiComponents" "Major responsibilities inside the API" {
include *
autoLayout lr
}
deployment ordering production "OrderPlatformProduction" "Production deployment" {
include *
autoLayout lr
}
styles {
element "Person" {
shape Person
background #08427b
color #ffffff
}
element "Software System" {
background #1168bd
color #ffffff
}
element "Container" {
background #438dd5
color #ffffff
}
element "Component" {
background #85bbf0
color #111111
}
}
}
}workspace 是模型和视图的顶层容器。model 定义节点与有向关系,views 从同一张图结构中选取子集。!identifiers hierarchical 让嵌套标识符带上父级语义,例如 ordering.api,降低大型 workspace 的命名冲突。每个 view 都显式给出稳定 key;让工具自动生成 key 可能在重构后丢失手工布局或外部引用。
include * 不是“把所有模型塞进每张图”。它按视图类型加入适用元素和关系。随着模型增长,生产图应改成表达式或明确的 include/exclude,控制认知负荷。autoLayout lr 中 lr 表示从左到右;还可以设置方向、层级间距和同层节点间距。参数变化会重排全图,因此布局调整与模型变更最好分开提交。
用 Structurizr Local 看见模型
先检查端口和 Docker:
docker version
docker ps --format "table {{.Names}}\t{{.Ports}}"Linux、macOS 或 Git Bash 在 docs/architecture/structurizr 目录执行:
export STRUCTURIZR_VERSION=2026.05.22
docker pull "structurizr/structurizr:${STRUCTURIZR_VERSION}"
docker run --rm -it \
-p 127.0.0.1:8080:8080 \
-v "$PWD:/usr/local/structurizr" \
"structurizr/structurizr:${STRUCTURIZR_VERSION}" localPowerShell 使用:
$env:STRUCTURIZR_VERSION = "2026.05.22"
docker run --rm -it `
-p 127.0.0.1:8080:8080 `
-v "${PWD}:/usr/local/structurizr" `
"structurizr/structurizr:$env:STRUCTURIZR_VERSION" local浏览器打开 http://localhost:8080。预期能看到 workspace 摘要、四个视图和图例。进入图编辑器移动一个元素,等待自动保存,然后检查目录中是否生成或更新 workspace.json。DSL 保存模型语义,JSON 还包含布局;使用手工布局时二者都应进入版本控制。
把本地行为写进 structurizr.properties:
structurizr.autoSaveInterval=5000
structurizr.autoRefreshInterval=2000
structurizr.editable=true
structurizr.workspace.maxsize=1MBautoSaveInterval 和 autoRefreshInterval 使用毫秒,0 表示关闭。editable=false 可以把本地实例变成只读评审入口。workspace 默认最大尺寸是 1MB;提高上限会允许更多模型、文档和嵌入资源,但也会增加解析、渲染、传输和浏览器内存压力。先拆分 workspace、外置大图标并减少无价值视图,再考虑提高限制。
Local 是开发机工具,不应直接发布到团队网络。它默认围绕本地作者工作流设计,不提供完整团队认证与并发治理。端口只绑定 127.0.0.1;需要共享访问时选择受控静态站点或 Structurizr Server,并补齐 TLS、认证、授权、审计、备份和升级。
命令行先证明 DSL 合法
新统一镜像把原 CLI 能力拆成命令。仍在 workspace 目录执行:
docker run --rm \
-v "$PWD:/usr/local/structurizr" \
"structurizr/structurizr:${STRUCTURIZR_VERSION}" \
validate -workspace workspace.dsl
docker run --rm \
-v "$PWD:/usr/local/structurizr" \
"structurizr/structurizr:${STRUCTURIZR_VERSION}" \
inspect -workspace workspace.dsl -severity error,warning
docker run --rm \
-v "$PWD:/usr/local/structurizr" \
"structurizr/structurizr:${STRUCTURIZR_VERSION}" \
list -workspace workspace.dsl健康路径中,validate 以零状态退出;inspect 没有错误或警告时以零状态退出;list 能列出人员、系统、container、component 和关系。inspect 的退出码反映违规数量,适合 CI 门禁,但检查规则升级后可能新增告警,升级工具时要先在单独 PR 中审查差异。
接着导出可分发产物:
mkdir -p ../../rendered/structurizr-static
docker run --rm \
-v "$PWD:/usr/local/structurizr" \
"structurizr/structurizr:${STRUCTURIZR_VERSION}-noble" \
export -workspace workspace.dsl -format static \
-output ../../rendered/structurizr-static预期输出目录出现静态站点文件。浏览器打开入口页,逐个检查视图、缩放、链接和图例。也可以导出 plantuml、plantuml/c4plantuml、mermaid 或 json。这些 exporter 对形状、图标、过滤视图、手工布局和交互能力的支持不完全一致;导出成功只证明转换完成,不证明视觉等价。需要 PNG/SVG 时应根据官方导出说明选择带浏览器依赖的构建,并在 CI 缓存与供应链策略中计入 Chromium 下载和运行成本。
故意留一个坏标识符
复制一份实验文件:
cp workspace.dsl workspace.invalid.dsl把 component view 的 ordering.api 改成不存在的 ordering.ordersApi,再运行:
docker run --rm \
-v "$PWD:/usr/local/structurizr" \
"structurizr/structurizr:${STRUCTURIZR_VERSION}" \
validate -workspace workspace.invalid.dsl预期命令非零退出,并报告找不到对应标识符或视图作用域无效。这条证据说明 view 不是复制出来的图片:它必须引用模型中真实存在的元素。修复标识符后再次验证,命令应恢复为零状态。
完成实验立即清理:
rm workspace.invalid.dslPowerShell 使用 Copy-Item 和 Remove-Item -LiteralPath .\workspace.invalid.dsl。删除前确认它是实验副本,不要误删正式 workspace。
再制造一种校验抓不到的漂移
从正式 DSL 中删除这一行:
ordering.api.paymentGateway -> payment "Authorises payment" "JSON/HTTPS"DSL 仍可能通过 validate,视图也能生成,但实际代码仍在调用支付提供方。语法校验只知道模型内部自洽,不知道运行系统事实。故障证据不是解析错误,而是下面三份证据互相矛盾:
rg -n "payment|Payment" src config docs/architecture
git diff -- docs/architecture/structurizr/workspace.dsl如果代码或配置仍存在支付客户端,而模型关系被删除,就要判断是代码待下线、模型误删,还是调用已迁到别的 container。修复后不仅恢复一条箭头,还应在 PR 中链接对应代码、配置或 ADR,使评审者能解释关系为何存在。
这也是架构漂移治理的核心:
工具负责让反馈更早、更一致,owner 负责解释事实差异。
接进真实项目,而不是放在架构师个人目录
团队把以下入口放进仓库:
workspace.dsl:模型、关系、视图和样式的权威源。workspace.json:仅在使用手工布局时提交;不要多人同时拖动同一视图。structurizr.properties:本地预览行为,不放密码和许可证。
README.md:运行命令、owner、视图入口和触发更新的代码区域。CI 任务:固定版本执行 validate、inspect 和导出。
最小 scripts/architecture-check.sh 可以写成:
#!/usr/bin/env bash
set -euo pipefail
image="structurizr/structurizr:${STRUCTURIZR_VERSION:?set STRUCTURIZR_VERSION}"
root="$(cd "$(dirname "$0")/.." && pwd)"
workspace="$root/docs/architecture/structurizr"
docker run --rm -v "$workspace:/usr/local/structurizr" "$image" \
validate -workspace workspace.dsl
docker run --rm -v "$workspace:/usr/local/structurizr" "$image" \
inspect -workspace workspace.dsl -severity error,warningCI 设置 STRUCTURIZR_VERSION=2026.05.22,版本更新通过依赖 PR 审查。脚本只读 workspace;导出任务使用单独临时目录,避免 CI 以不同文件所有者覆盖开发机的 workspace.json。
变更触发器要落到目录和责任:新增服务、数据库、队列、外部 SaaS、公开 API、跨信任区调用或部署环境时,PR 模板要求检查对应 C4 元素、关系和视图。普通方法重构通常不更新上下文图;container 边界、技术选择和调用协议变化则必须更新。这样既避免每次提交都碰图,也避免重大结构变化无人负责。
大 workspace 为什么会失控
Structurizr 的内部核心是有向图:元素是节点,关系是边,view 是带作用域、筛选和布局信息的投影。多个 view 复用同一节点,所以改名和关系更新能保持一致;代价是模型越大,任意 include * 越容易把无关节点带进图,自动布局越不稳定,浏览器渲染与评审认知成本也越高。
扩展到多团队时,先按稳定所有权拆 workspace,再用 !include 或 workspace extends 共享受控模型片段。远程 URL 会引入网络、证书、可用性和供应链风险;生产基线优先使用仓库内文件并锁定提交。DSL 的 !script 和 !plugin 可以执行 JVM 侧代码,它们是代码执行入口,必须像构建插件一样审查来源、版本和权限,不能把陌生脚本当成无害文档。
手工布局带来另一种状态:DSL 改模型,JSON 保存坐标。如果只提交 DSL,其他人可能丢失布局;如果把 JSON 当唯一源,又会让语义 diff 变得困难。团队应二选一:主要使用 autoLayout,只提交 DSL;或者接受手工布局,同时提交 DSL 与 JSON,并指定单一布局 owner。不要在同一视图上无规则切换。
容量预算看趋势而非统一阈值。监测 workspace 文件大小、元素和关系数量、单图节点数、解析时间、静态导出时间及浏览器交互延迟。单图持续增长时,先按受众和问题拆 view;workspace 持续增长时,再按系统或团队所有权拆模型。提高 structurizr.workspace.maxsize 只能解除保护,不会降低认知负荷。
团队发布、权限与成本
个人和 CI 使用的 local、validate、inspect、export 等统一命令可以免费使用;预构建的 Structurizr Server 需要许可证。Server 的许可按安装和一定周期内的唯一用户数计量,嵌入查看和 API 自动进程也可能计入。购买前从 Structurizr Server 价格页读取实际档位、试用和条款,不把金额或用户上限复制进长期规范。
Server 快速启动默认关闭认证,官方示例只适合隔离开发验证。任何团队共享实例上线前都要先完成:
TLS 和反向代理边界。固定账号、文件账号或 SAML 等认证方案。workspace 级角色和管理员最小权限。
文件系统、对象存储、缓存和搜索的备份恢复。会话存储、升级、回滚和审计。许可证、自动进程与嵌入访问的容量预算。
structurizr.license、API key、密码和 SAML 秘密通过部署平台的 secret 注入,不写进 DSL、properties、镜像或 GitHub Actions 日志。模型本身同样可能敏感:内部系统名、管理入口、信任边界、供应商连接和恢复路径会提高攻击者侦察效率。公开静态站点应使用单独的公开 workspace 或明确过滤后的视图,不能把内部全模型导出后仅隐藏导航链接。
图标和主题也有数据边界。远程图标会在浏览器渲染时发起请求,暴露访问来源并受 CORS、网络和资源变更影响;本地文件更可控,但必须检查版权、体积和路径可移植性。部署图不应包含真实节点名、IP、账号或 secret;使用逻辑区域、节点角色和 <secret-ref> 表达即可。
排障从证据分型开始
Local 打不开 workspace
如果浏览器显示解析失败,先执行 validate,再看容器日志:
docker ps --filter "ancestor=structurizr/structurizr:${STRUCTURIZR_VERSION}"
docker logs <container-name>解析错误通常指向 DSL 行号、未知标识符或非法作用域。挂载错误则表现为自动创建了一份默认 workspace,或容器内找不到预期文件。用下面命令确认挂载,而不是反复刷新:
docker run --rm \
-v "$PWD:/usr/local/structurizr" \
"structurizr/structurizr:${STRUCTURIZR_VERSION}-noble" \
sh -lc 'pwd; ls -la; test -f workspace.dsl'保存布局时报只读或权限错误
现象是图能看、拖动后却无法写 workspace.json,容器日志出现 permission denied。先检查宿主目录权限和 hardened 镜像的运行用户,不要直接把目录改成全员可写。开发机可以使用适合 CI/本地环境的官方镜像变体,或者把目录所有权调整给明确用户;共享目录仍保持最小写权限。
导出成功但图与 Local 不一致
PlantUML、Mermaid 和静态 exporter 支持的形状、图标、过滤视图、动画和布局不同。先在导出能力对比中确认差异,再决定降低样式复杂度、改用静态站点,还是把 Local/Server 作为主要阅读入口。不要在导出后的 PlantUML 或 Mermaid 中手工补结构,否则它会变成第二事实源。
升级后出现新告警或大面积重排
先恢复旧的固定版本确认基线,再单独提交工具升级。比较 validate、inspect 输出、静态导出物和关键视图截图;新规则确实发现问题时修模型,布局算法变化时评估是否接受重排。不要把版本升级、全模型改名和样式重构塞进同一 PR,那会让行为变化无法归因。
清理、回滚与迁移演练
Local 容器使用 --rm 时退出即删除,workspace 留在宿主目录。停止进程后确认没有其他实例正在写,再处理生成物:
git status --short docs/architecture
git clean -nd docs/architecture/rendered/structurizr-static只预览并删除明确的导出目录,不删除 workspace.dsl、手工布局 workspace.json、ADR、图标或主题。回滚错误模型变更时,让 DSL 与 JSON 回到同一提交;随后重新运行 validate、inspect 和导出,证明布局与语义一致。
从 Lite 或旧 CLI 迁移时执行一轮可逆演练:停止旧写入、完整备份目录、在副本上启动固定版本 Local、逐个打开视图、保存一次布局、运行新命令、比较静态导出物。确认后再切换团队入口,并保留旧只读副本到回滚窗口结束。回滚不是重新启动旧容器并继续双写,而是停止新入口、恢复备份和旧版本,再记录新窗口产生的模型变更如何重放。
让模型持续贴近系统
架构模型不会因为进入 Git 就自动新鲜。长期治理需要三个节奏:PR 级触发检查结构变化,发布级核对部署和外部依赖,周期性由系统 owner 对照服务目录、IaC、API 清单和运行观测。任何对账发现的差异都要归类为“模型过期”“实现偏离决策”或“临时例外”,并链接修复任务或 ADR。
最终验收看不变量:同一元素在不同视图中名称与责任一致;所有跨边界关系都有方向、意图和协议;DSL 校验与 inspection 持续通过;关键视图能从干净环境生成;代码或部署发生结构变化时模型在同一变更链路更新;静态发布物不泄露内部细节;工具版本、Server 许可、凭证和 owner 都有人维护。
当团队只需要会议草图时,Excalidraw 更轻;需要精确拖拽和自由版式时,draw.io 更直接;需要一份模型生成多层一致视图、查询关系并设置漂移门禁时,Structurizr 才开始显示价值。选择的关键不是图是否好看,而是团队愿意为哪种事实源、协作模型和治理成本负责。
