ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

代码驱动图表设计:用D2告别过期架构图

代码驱动图表设计:用D2告别过期架构图 我最近接手了一个老服务的架构梳理任务文档里躺着一张已经一年没人敢动的架构图。说有勇气更新吧上面还挂着五个已经下线半年多的节点说没人管吧每次评审方案时又总有人把这张图翻出来当依据。问了一圈反馈高度一致画图太费劲了改了还得重新拉线对齐一弄就是半天。这事让我意识到一件挺反直觉的事真正能长期存活的图表设计diagram-design方式往往不是打开绘图软件拖拽而是打开代码编辑器写文本。diagram-design 在工程语境里就是用声明式的文本语言去描述节点、连线、分组和布局约束让图表像代码一样被管理、被 Review、被版本化。这个思路不是什么新发明Graphviz 二十多年前就在做只是到了今天工具链成熟度已经足够让它在日常团队协作里规模化落地。如果你也正在为架构图、流程图、时序图过期而头疼或者想给团队建一套能自动渲染、自动校验的图表资产体系这篇文章值得看完。我会以我内部代号改为 diagram-design 的这套图表体系为线索讲清楚选型逻辑、完整的实操路径以及我踩过的那些真实问题。1. 架构图维护的隐形成本和代码驱动的解法1.1 拖拽式绘图工具的低保真陷阱先别急着否定 ProcessOn 这类工具它们的快速上手体验确实优秀。团队初期画架构草图、和产品对流程图拖拽拉线几乎零门槛这没问题。但要命的是当这张图进入长期维护期后问题会集中爆发。首先是格式不可读。draw.io 和 ProcessOn 存的要么是二进制要么是压缩后的 XML而 ProcessOn 的私有格式更是没法直接看。你想在提交记录里看清楚上周到底改了哪个节点几乎不可能。其次是版本冲突两个人同时改一张图合并时要不是谁后存听谁的要不就是文件直接损坏。第三是图与代码脱节代码里有服务部署图上没有图上有服务依赖代码里已经删了。发展到最后这张图就成了没人信的装饰品谁也不看但谁也不愿意承认它可以删掉——因为它偶尔还有用。我把这个现象叫做低保真陷阱绘图过程低门槛换来的却是维护过程低清晰度。图的生命周期一旦超过两周拖拽式工具的信息损耗就会超过它带来的便利。1.2 代码驱动图表带来的四个关键收益后来我决定尝试另一种思路用代码定义图表。工具换成正则文本文件后收益其实是一连串涌现出来的远不止字面上的能用 Git 管理。可版本化。图表源码进入 Git 仓库每次改动都能 diff。有人改了架构图但没改对应部署脚本Review 时一眼就能看出来。可复用。代码图表天然支持组件化和变量。同一个服务模块出现在五张图里定义一次到处引用以后要改这个模块的颜色、名称、依赖只改一处其他图自动同步。可自动化。这是最核心的收益。写完.d2文件一条命令就能渲染出 SVG、PNG再挂到 CI 流水线里文档站点一更新图自动重出。每次架构变更不用等人肉去同步那一堆图片文件。可审查。代码即图表之后Pull Request 里就能对图本身做 Code Review。审查的不只是图画得像不像样更是节点关系是否反映了真实的系统结构。我用下面这张表总结过两种方式的差异给团队讨论时省了不少口舌对比维度拖拽式绘图代码驱动 chart上手门槛极低中等需要学语法diff 可读性差二进制/私有格式极好纯文本逐行 diff版本合并冲突多且难处理同普通代码冲突可解决自动渲染需手动导出命令行一键完成复杂图布局手动对齐费时自动布局一致性高适合场景临时草图、对外美观展示长期维护、团队共识图1.3 diagram-design 在团队落地时的边界代码驱动图表不是万能的这个边界一定要划清楚否则团队内部容易产生一人想推、另一人觉得没必要的内耗。它真正擅长的是承载结构和逻辑的图系统架构图、数据流向图、时序图、ER 图、部署拓扑图、状态机图。这类图的核心价值是信息准确、关系清晰、变更可追溯视觉表现力是次要的。它不擅长的是高视觉包装类内容比如对外发布的营销海报、需要精细手绘质感的插画式配图。代码图表就算再调样式也很难做出那种艺术感。团队里如果有人非要拿 D2 画一张拿去投标的高大上系统全景大图效果大概率不如交给设计师用 Figma 做。明确这条边界其实是在保护代码驱动方案的口碑别让它因为用错场景被误判为不好用。2. 四类主流代码图表工具的选型边界2.1 Mermaid文档内联的快速方案如果你的需求仅仅是在 Markdown 文档里顺手画一张流程图Mermaid 几乎是零成本选项。它最大的优势是 JavaScript 渲染不需要安装任何本地命令行工具GitHub 的 Markdown、各种笔记软件原生支持。语法也足够简单写一个流程图只要几十秒。graph TD A[用户请求] -- B{鉴权} B -- 通过 -- C[业务服务] B -- 失败 -- D[返回提示]但 Mermaid 在复杂场景下会露出短板布局引擎是全自动排布节点一多连线开始乱穿很难人为干预同时 Mermaid 的渲染结果在不同环境下差异比较大GitHub 上显示正常本地 Hexo 博客的版本又可能渲染出另一版效果。我的经验是Mermaid 适合轻量内联场景不适合作为大型架构图资产的管理底座。2.2 Graphviz元老级的稳定内核Graphviz 其实是很多代码图表工具的地基D2 的不少思路也受到它的启发。它用 DOT 语言描述图结构背后是一整套深耕多年的布局算法dot做层次布局neato做物理模型布局fdp、sfdp做无向大图布局。对于依赖关系图、集群图、网络拓扑图这种重结构场景Graphviz 仍然是效果最稳妥的选择之一。digraph G { rankdirLR; user - gateway [labelHTTPS]; gateway - order [labelRPC]; order - mysql [labelSQL]; }它的问题在于太底层。DOT 语法表达能力强但写起来繁琐默认输出样式朴素得可怕。你把一张架构图画出来没问题想让它好看、符合公司视觉规范就得折腾一堆style参数成本不低。Graphviz 适合作为后端渲染引擎不适合作为团队日常手写的主要语言。2.3 PlantUMLUML 序列图的一把好手PlantUML 在 UML 社区里地位稳固尤其是时序图几乎是我用过最舒服的代码画法。声明一个参与者、画一条消息语法直观到不用查文档startuml actor 用户 用户 - API网关 : 提交订单 API网关 - 订单服务 : 调用创建接口 订单服务 - 库存服务 : 扣减库存 订单服务 - 支付服务 : 发起支付 enduml序列图里的生命线、激活条、消息序号写代码时自然表达渲染完全自动。如果你主要画的是 UML 类型图类图、状态图、活动图PlantUML 的成熟度依然领先。它的短板是非 UML 类的自由架构图表现力不够比如系统架构图里那些云环境、容器、服务分组的视觉元素用 PlantUML 表达起来比较别扭模板感很强。2.4 D2面向现代架构图的新一代 DSL在我最终选型落地的方案里实际默认工具是 D2迪图语言它的定位恰好卡在表达能力和书写体验中间解决了上面几个工具各自最疼的点。先看一段 D2 定义架构图的代码direction: right 用户 - API网关: HTTPS API网关 - 订单服务: gRPC API网关 - 用户服务: gRPC 订单服务 - 数据库: SQL 订单服务.style.fill: #e8f5e9 API网关.style.strokeDash: 3语法表达层级关系标签写冒号连线写箭头样式写点路径。相比 DOT 的繁琐、Mermaid 的不可控D2 在声明性上做得最好相比 PlantUML 强烈的 UML 模板感D2 对架构图的表达自然很多。它还内置了主题系统一行命令就能切换整套配色。工具选型没有绝对最优关键看场景。我在 diagram-design 体系里最终确定为D2 为主Graphviz 处理超大依赖图PlantUML 应对 UML 类图的组合策略实际运行了半年团队普通工程师都能在一小时内上手 D2 写出可用的架构图。3. 用 D2 设计一张可维护的系统架构图的全过程3.1 先定信息架构再写代码很多初学者拿到 D2 就急着写代码结果写到一半发现组织结构不对整段重来。我在 diagram-design 的规范文档里强制要求的第一步是先在文本里列出这张图要表达的逻辑结构确定分组和依赖。举个我做过的订单服务架构例子先列出层次结构客户端层Web 端、移动端接入层API 网关、静态 CDN业务服务层订单服务、支付服务、库存服务、用户服务数据层MySQL 实例、Redis 缓存、消息队列外部依赖第三方支付路由同时标记出核心链路用户下单 → 网关路由 → 订单服务创建订单 → 调支付服务发起支付 → 支付成功回写 → 扣减库存 → 发送消息到 MQ。这个信息架构一旦清晰代码只是把文本翻译成 D2 语法。3.2 D2 核心语法与心智模型D2 的基本心智模型就是三个东西节点、连接线、容器。节点直接写名字冒号后面跟展示标签比如order_server: 订单服务连接线用箭头表达可以带标签order_server - payment_server: RPC容器用大括号表达分组容器和节点可以互相嵌套cloud_zone: 生产环境 { api_gateway: API网关 biz_group: 业务集群 { order_server: 订单服务 payment_server: 支付服务 } }样式走属性路径比如order_server.style.fill: red。整个语言刻意保持所见即所得读起来像是一份结构化文本而不是编程语言的抽象语法树。这就是我说 D2 适合团队协作的原因不懂代码的人也能读懂源代码表达的是什么意思。3.3 从零构建订单服务架构图我用上面那套信息架构直接给出完整 D2 代码示例标注好布局方向和关键链路direction: right # 客户端层 web_client: Web端 mobile_client: 移动端 # 接入层 cdn: CDN静态资源 api_gateway: API网关 { style.fill: #e3f2fd } # 业务服务层 order_service: 订单服务 payment_service: 支付服务 inventory_service: 库存服务 user_service: 用户服务 # 数据与中间件层 mysql: MySQL主库 redis: Redis缓存 mq: 消息队列 # 外部依赖 pay_router: 第三方支付路由 # 连接关系 web_client - cdn: 静态资源 web_client - api_gateway: HTTPS mobile_client - api_gateway: HTTPS api_gateway - order_service: 创建订单 api_gateway - user_service: 登录鉴权 order_service - payment_service: 支付请求 payment_service - pay_router: 调用外部支付 order_service - inventory_service: 扣减库存 order_service - mysql: 订单数据 payment_service - mysql: 支付流水 order_service - mq: 下单消息 order_service - redis: 缓存热点数据这段代码跑起来D2 会自动完成布局节点按依赖方向从左向右排列所有连线自动避让。你不需要手动调整一个像素这一点对长期维护的意义巨大每次新增一个服务代码加三行图自动更新。3.4 让图表具备长期可读性的设计习惯能渲染出来只是第一步能不能让三个月后的人一眼看懂才是 diagram-design 的核心命题。我在这套体系里总结了几条必须遵守的设计习惯第一层次用容器而不是靠坐标硬分。服务属于哪一层就放进哪个容器里依赖方向跟着容器结构走。这样即使增删节点图的可读性依然稳定。第二关键链路用标签说清楚。连接线上带协议名HTTPS、gRPC、SQL的标注成本极低但阅读效率提升极大。选标签时优先写语义创建订单而不是写泛泛的调用。第三颜色只表达分类不表达状态。我见过有人把正在重启的节点标成红色第二天图里一片红没人知道哪些是重点。颜色分类固定为接入层蓝、服务层绿、数据层橙、外部依赖灰。规范写进文档新人照着填就行。第四一张图控制在 30 个节点以内。节点一旦超过 30 个不管布局算法多聪明人脑的负担都会急剧上升。这时候就该拆图而不是硬塞。4. 图表如何接入文档仓库与自动化流水线4.1 图表即代码的目录结构设计在 diagram-design 体系中图表文件放哪和谁来维护同样重要。我采用的目录结构如下供你参考docs/ diagrams/ overview.d2 order/ sequence.d2 architecture.d2 payment/ architecture.d2 images/ generated/ overview.svg order-architecture.svg guides/ deploy.mddiagrams目录下按业务域组织.d2源文件images/generated是 CI 渲染输出的产物不进手改区Markdown 文档通过相对路径引用已生成的 SVG。这样切分后文档作者永远不需要处理图片文件他在文档里只做一件事引用generated目录里的图。渲染命令我用的是 D2 CLI执行非常简单d2 docs/diagrams/overview.d2 docs/images/generated/overview.svg如果希望字体更可控输出 PNG 时用--pad 0裁剪留白这是我在发布文档站点时不踩坑的关键参数d2 --pad 0 --theme 3 docs/diagrams/overview.d2 docs/images/generated/overview.png4.2 CI 流水线里的图表演进校验图表进了 Git 只是第一步真正让它永不掉线的关键是挂到 CI 流水线里。我的持续集成里做了三件事渲染检查。验证.d2文件语法正确、能成功渲染出 SVG。这个看似简单但能拦截大量图上改了代码没改或代码里写错个style字段导致渲染失败的低级错误。链接检查。用脚本扫描所有 Markdown 文档中引用的图片路径确认它们真实存在于generated目录。这条规则的意义在于防止有人新增了一张图写好了文档但忘了把渲染命令跑一遍导致文档站引用到 404。结构一致性检查。写了个轻量级脚本读 D2 源码里的节点依赖关系跟代码仓库中的服务注册表做比对。新服务加了节点但没在部署文件里出现直接 CI 失败。这一步才真正把代码图和系统代码绑到了一起。关键流程我封装成一个 Makefile 片段团队本地执行和 CI 用的是同一套逻辑render: mkdir -p docs/images/generated for f in docs/diagrams/*.d2; do \ name$$(basename $$f .d2); \ d2 --pad 0 $$f docs/images/generated/$$name.svg; \ done4.3 让团队真正会去维护图的机制工具链再完善如果团队没有维护动机一切白搭。我从三个方向解决这个问题第一把图的设计纳入评审流程。架构评审时先看图 diff而不是先看完 200 行文字描述。这会倒逼写方案的人主动更新图因为图上和实际不一致在评审会上非常丢面子。第二降低修改图的成本。D2 源码的阅读门槛低凡是写过几段 Markdown 的同事都能改。我在 wiki 里挂了一个 15 分钟上手示例出了问题群里喊一声基本都能自己解决。第三发版前自动通知图主人。我写了一个检测脚本如果 PR 里改了某个服务的部署配置而这个服务出现在某张图上机器人会自动评论提醒这张图的节点可能需要更新。虽然做不到全自动但至少把图过期这件事从无人知晓变成了显性提示。经验是维护图这件事不要指望靠自觉机制设计比动员和强调有效得多。5. 布局算法、乱码与分层实测中的问题排查记录5.1 布局结果与预期不符该怎么办D2 自动布局在多数场景下表现优秀但遇到下面两种情况时仍然需要人为干预。情况一想强制横向/纵向布局。解决办法是在文件开头写全局方向指示direction: up可选值有right、down、up、left。选定后整张图的依赖方向都被约束住。我个人习惯用right因为浏览器阅读习惯是从左到右节点顺序刚好对应请求链路。情况二局部想并排展示。全局方向定住后两个无连接的独立组件有时会被放很远。这时可以在它们外面套一层容器强制组成逻辑组infra: 基础设施 { redis: Redis mysql: MySQL }容器是引导布局最有效的手段比手动调坐标优先级高得多。5.2 中文标签渲染成方块的问题代码图表工具常见的中文坑本质都是字体加载失败。Graphviz 默认字库里没有中文字形D2 在部分 Linux 环境也依赖系统字体配置。我踩过一次CI 上渲染的 SVG 里所有中文标签全是方块但本地 Mac 上正常。排查后确认是 CI 机器没有安装中文字体。解决方案很简单# Ubuntu/Debian 环境安装中文字体 apt-get install -y fonts-noto-cjkD2 还支持在代码里指定字体栈我通常会把渲染环境的字体设置写在 D2 文件的开头例如font-family: Noto Sans CJK SC需要注意的是SVG 预览时字体来自客户端系统把 SVG 嵌入 PDF 或者直接用浏览器看都依赖目标设备是否安装了对应字体。如果对这点特别敏感建议渲染成 PNG 再嵌入文档虽然文件会变大但所见即所得更稳定。5.3 一张图塞进二百个节点的教训我在 diagram-design 体系落地早期做过一次把所有接口依赖画进一张架构图的努力结果产出了一张 200 多节点的巨图。布局引擎花了一分多钟不说渲染出来的图人根本无法阅读缩放滚动都找不到目标这图等于废纸一张。那次踩坑之后我把节奏改为概览图 分域图模式。概览图只画全局的分域关系和核心链路节点不超过 15 个分域图每个业务域一张画该域内的详细服务依赖。域之间通过import或指针引用来关联不在单张图里硬塞所有信息。order_domain: 订单域 { import from another.d2 }这个经验的本质是图表设计不是把所有信息压进一张图而是建立一套图与图之间的导航关系。5.4 样式覆盖和主题陷阱D2 内置了多套主题命令参数--theme可以切换。但主题和自主样式混用时容易翻车主题会覆盖部分自定义样式如果你写了style.fill没生效先检查主题优先级。我吃到的一个具体教训是D2 里给容器设置圆角字段是border-radius不是 CSS 里的border-radius完全一致不同版本的 DSL 会有细微区别在写样式前先查对应版本的语法文档。为了减少版本差异我在 CI 里固定 D2 CLI 版本而不是默认拉最新。另外利用 D2 的vars和严格模式可以减少这类低级问题vars: { apiColor: #e3f2fd } api_gateway.style.fill: $apiColor--strict参数会让未知字段直接报错而不是静默忽略把这个参数加进 CI 命令能拦住一堆拼写错误。最后分享几个我在实际使用中的体会跑了大半年 diagram-design 体系最明显的变化其实不是省了多少画图时间而是团队开始敢于频繁调整架构图了。以前改一张架构图要鼓起勇气排期现在一次 PR 里顺手就改了Review 成本也很低。图的新鲜度一旦保住了它在评审会上的可信度会越来越高慢慢真的会成为团队的单一事实来源。另一个值得推荐的小技巧是把图表的 diff 截图自动贴在 PR 描述里。我们在 CI 里加了一小步渲染完成后上传 SVG artifact机器人把图片贴到 PR 评论区。这样 Review 者打开 PR 的第一眼看到的就是图变成了什么样而不是得先点开源码看 D2 语言里改了什么。别小看这个交互细节它对提升团队维护意愿作用非常大。如果你的团队还在被架构图过期的问题困扰我建议先别急着全面铺开大而全的平台方案。挑一个最小的场景比如给当前核心链路画一张 D2 图跑通渲染、进 Git、挂 CI这周就能完成。对比一下延续旧拖拽方式和用代码维护的成本你大概也会得出和我一样的结论。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进