ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

用代码画架构图:Graphviz与dot语言实战

用代码画架构图:Graphviz与dot语言实战 画架构图这件事我以前一直是“打开画图工具拖拽、连线、对齐、导出”这一套流程。图小的时候还行一旦节点超过二三十个改一处布局就得连带动好几条线评审意见回来的时候往往不是改内容而是在跟线条位置作斗争。后来我把图表改成用代码来设计和维护项目代号就叫 diagram-design核心思路很简单图也是代码应该被版本管理、被 review、被自动化渲染。这套方式我用了一年多从架构图到时序图、ER 图基本都迁过来了这篇文章就把我给团队内部整理的方法、踩过的坑、沉淀下来的习惯完整写出来。如果你也在为“画图五分钟改图两小时”头疼或者需要把图表放进文档、PPT 和 Wiki 里又要保持一致性这篇内容应该对你有用。我会从设计思路、语法选型、实操步骤、样式经验到问题排查完整讲一遍尽量做到能直接照着落地。1. 为什么我最终选择用代码来设计图表1.1 手动画图的三座大山先说说我可视化画图的真实体验。早期在文档里放图用的是白板类工具和桌面绘图软件。遇到的第一座大山是版本管理图导出的 PNG 放进仓库里改一次就生成一个新文件过两周根本分不清哪张是最新的Review 时也只能看到最终图片改动过程完全丢了。第二座大山是协作多人同时编辑一张图经常出现有人覆盖了别人的布局调整或者节点的连线被挪得乱七八糟最后只能靠人工“劝架”。第三座大山是复用这个项目里的系统架构在下个项目里只有部分类似想要复制过去改一改得对着图重新拖半天。这些问题不是不能忍但频率高到一定程度就让人烦躁。尤其是给外部团队同步接口调用关系时我总是要先打开编辑器确认每个节点位置没丢再导出、压缩、贴到文档过程很机械。1.2 用代码定义图表的本质diagram-design 解决这些问题的思路是把图的结构和样式都用文本描述出来配合渲染引擎生成图片。核心价值有三个第一是图与文字一样能进 Git每次改动都是可追踪、可回滚的 diff第二是渲染完全自动化文档构建时执行一段命令就能重新生成图片不会出现“文档更新了但图还是旧的”这种低级错误第三是结构可以复用复制一份.dot文件改几个节点名新图就出来了。这个理念和“基础设施即代码”很像都是把原本靠手工维护的东西变成可声明、可执行的文本。一开始可能会觉得写代码比拖拽慢但当你只需要改一个节点名称然后重新渲染一次其余布局全自动调整时效率优势就体现出来了。尤其是图表经常更新的项目用文本描述几乎是唯一稳妥的方式。1.3 什么场景不适合代码画图当然代码画图也不是万能的我试下来有三种情况不适合硬上。第一种是快速草图比如开会时随手画个思路或者白板上草拟流程这时候工具的即时性和自由度远比可维护性重要打开编辑器敲代码反而耽误事。第二种是高度视觉化的设计图比如运营海报、产品 UI 稿、带大量图标和手绘风格的示意图Graphviz 这类声明式工具在精细视觉控制上很吃力用矢量设计软件更合适。第三种是节点位置有严格像素级要求的图比如必须精确对齐到某个坐标这种需求在绘图工具里直接拖很容易但在代码里要用 pos 属性手动指定坐标维护成本比较高。我的建议是凡是会被反复修改、需要多人维护、要放进正式文档的图优先考虑代码化一次性草图、纯创意视觉内容继续用手工工具。两种方式并存不冲突。2. 从零开始diagram-design 的核心语法与绘图模型2.1 一条边和一个节点背后的绘图模型diagram-design 底层我主要用 Graphviz 的 dot 语言来描述图表它的核心模型非常简洁只有三个元素图、节点、边。声明一张有向图用digraph无向图用graph节点就是出现名字的实体边就是两个节点之间的连接。一个最小的示例digraph demo { A - B; A - C; }这段代码定义了一张有向图节点 A 指向 B 和 C。渲染引擎会自动计算节点位置不需要你指定坐标。这正是代码画图和画布类工具最大的区别你描述的是“对象之间的关系”而不是“对象在画布上的位置”。在此基础上可以给节点和边附加属性比如形状、颜色、标签从而控制最终呈现效果digraph demo { node [shapebox, stylerounded,filled, fillcolor#EAF2F8]; edge [color#718096]; A [labelGit 仓库]; B [label构建任务]; A - B [label触发]; }这里node [ ... ]声明的是所有后续节点的默认样式edge [ ... ]同理。如果你只想修改单个节点就在节点名后面的方括号里写属性只想修改某条边就在箭头语句后面的方括号里写属性。这种默认值加局部覆盖的设计让我能先把整张图的风格统一起来再对重点节点做差异化强调。2.2 布局引擎如何决定位置刚接触代码画图的人最容易困惑的问题就是“我没写坐标它是怎么知道节点该放哪里的”。Graphviz 的布局引擎会先分析图结构构建一个有向图模型然后通过一系列布局算法计算节点坐标。常见的引擎包括dot、neato、fdp、circo、twopi。其中 dot 引擎适合有层次、有方向的图比如流程图、架构图它会尽量让边朝一个方向流动减少交叉neato 基于力导向布局适合无向图和关系网络circo 适合环形结构。实际使用时通过-K参数指定引擎或者直接在文件开头声明digraph demo { rankdirLR; A - B - C; }rankdir是一个高频参数控制图的整体方向。TB从上到下适合流程顺序清晰、步骤较多的图LR从左到右适合系统架构成分较多、希望按模块横向展开的场景。我的习惯是流程类图用 TB模块架构类图用 LR。布局引擎不是让你完全失控ranksame、constraintfalse、nodesep、ranksep这些属性可以用来干预局部布局后面我会专门讲。2.3 与 Mermaid 的定位差异很多人可能听说过 Mermaid它也是文本生成图表但和 diagram-design 侧重点不太一样。Mermaid 胜在简单、学习成本低适合直接嵌入 Markdown 文档比如画简单的流程图、时序图、甘特图语法到渲染只要十几秒很多技术博客和项目文档都在用。Graphviz 的优势在于布局引擎更成熟对复杂图的表达力和控制力更强尤其是节点数量较多、层次关系复杂、需要精细控制样式和分组时Graphviz 的稳定性和输出质量会更胜一筹。我的实际选择标准是如果图比较简单结构不超过十来个节点用 Mermaid 写进 Markdown 最方便如果图要支撑正式技术文档、架构评审或者节点数量较多、层级关系明显我用 diagram-design 这套 Graphviz 工作流。两者的关系和“脚本语言”与“传统编程语言”的区别类似各有定位按需取用。3. 实操用 diagram-design 画一张 CI/CD 部署架构图3.1 确定图的结构与分层我用一个真实的例子来走一遍完整流程这张图是持续集成与部署流程的架构图会包含开发环境、CI/CD 平台、生产环境三块以及它们之间的数据流关系。画图之前先梳理结构不急着写代码。我的做法是先在纸上列出所有节点然后确定它们之间的关系再按层次分组。这张图的组成开发环境Git 仓库、本地 IDECI/CD 平台流水线触发、构建镜像、推送镜像仓库生产环境应用服务器、健康检查、告警通知流程方向是开发者从本地 IDE 推送代码到 Git 仓库Git 仓库通过 webhook 触发流水线流水线依次执行构建、推送镜像随后部署到生产服务器部署后执行健康检查异常时触发告警通知。3.2 从零写 dot 文件确定结构后我写下完整 dot 文件。为了便于展示先给出全文再逐段说明关键点digraph cicd { rankdirLR; pad0.5; node [shapebox, stylerounded,filled, fillcolor#EAF2F8, color#2B6CB0, fontnameMicrosoft YaHei, fontsize11]; edge [color#718096, fontnameMicrosoft YaHei, fontsize10, arrowsize0.7]; subgraph cluster_dev { label开发环境; stylerounded,dashed; color#805AD5; dev1 [labelGit 仓库, shapefolder]; dev2 [label本地 IDE, shapenote]; } subgraph cluster_ci { labelCI/CD 平台; stylerounded,filled; color#2B6CB0; fillcolor#EBF8FF; ci1 [label流水线触发]; ci2 [label构建镜像]; ci3 [label推送镜像仓库]; } subgraph cluster_prod { label生产环境; stylerounded,filled; color#38A169; fillcolor#F0FFF4; p1 [label应用服务器]; p2 [label健康检查]; p3 [label告警通知]; } dev2 - dev1 [labelpush 代码]; dev1 - ci1 [labelwebhook]; ci1 - ci2; ci2 - ci3; ci3 - p1 [labeldeploy]; p1 - p2; p2 - p3 [label异常时, styledashed]; }3.3 逐段拆解关键写法先看全局设置。rankdirLR让整张图从左向右展开这样“开发环境 - CI/CD - 生产环境”的主链路符合阅读习惯。pad0.5是给画布四周留白避免节点贴边。node [ ... ]和edge [ ... ]是全局默认样式。我统一设置了圆角填充的方框、蓝灰色边框、中文字体、字体大小这样整张图基础风格一致后面单个节点只需要覆盖需要变化的属性即可。这个习惯很重要如果你在每个节点上重复写样式文件会变得又长又难维护。三个subgraph是结构分组。注意命名规则subgraph 的 id 如果以cluster开头才会渲染成带边框、带背景色的分组框否则它只是逻辑分组不会影响布局和显示。这是我刚开始踩过最明显的坑。cluster_dev、cluster_ci、cluster_prod都以 cluster 开头所以三个环境框能正确显示。每个 cluster 内部我设置了独立的边框颜色和背景色形成三块环境的视觉区分。节点形状上Git 仓库用folder文件夹形状本地 IDE 用note便签形状其他流程节点保持默认方框。节点形状是传达语义的一种低成本手段不需要额外画图标但读者能快速形成印象。边的部分比较直接dev2 - dev1 [labelpush 代码]表示一条带标签的有向边。健康检查到告警通知的边用了styledashed和label异常时用来表达“仅异常时才触发”的条件关系虚线在语义上能明显区分主流程与旁路。3.4 渲染与导出图片写完后执行渲染命令。我一般用命令行直接导出最常用的命令是dot -Tpng cicd.dot -o cicd.png如果要更高清的图加 dpi 参数dot -Tpng -Gdpi150 cicd.dot -o cicd.png如果希望放到网页或文档中可缩放的矢量图导出 SVGdot -Tsvg cicd.dot -o cicd.svgSVG 的好处是无限缩放不模糊而且可以被文本工具搜索和编辑。我的习惯是文档内嵌图用 SVGPPT 和对外交付的文件用高 dpi 的 PNG。注意如果 PNG 导出后文字发虚先检查是不是 dpi 太低而不是急着换字体。3.5 接入自动化构建流程单次手工渲染只是第一步更有价值的是把渲染过程自动化。我自己会在项目仓库里放一个脚本循环渲染目录下的所有 dot 文件#!/bin/bash for f in diagrams/*.dot; do name$(basename $f .dot) dot -Tsvg $f -o output/${name}.svg dot -Tpng -Gdpi150 $f -o output/${name}.png done配合持续集成流水线文档目录一旦有 dot 文件变更就自动重绘图片并提交到产物目录。这样文档站点、Wiki、API 说明里的图永远和源代码保持一致不会出现“图是上个迭代的”这种尴尬。自动化之后review 图表就变成了 review 文本 diff改动了几行、改了哪个标签一目了然。4. 让图更好看样式设计与可读性经验4.1 全局样式与局部覆盖的配合图表的第一要义是信息清晰其次才是美观。我的通用做法是先在文件顶部定义全局默认样式把字体、边框、底色、线条颜色统一再针对重点模块做局部覆盖。全局默认值相当于主题局部覆盖相当于强调。一个相对稳定的主题配置node [fontnameMicrosoft YaHei, fontsize11, shapebox, stylerounded,filled, fillcolor#F7FAFC, color#4A5568, fontcolor#1A202C, margin0.15,0.08]; edge [fontnameMicrosoft YaHei, fontsize10, color#A0AEC0, arrowsize0.7, penwidth1.2];这套主题的特点是颜色偏中性不喧宾夺主字体统一中文不乱边框颜色和文字颜色有足够的对比度打印出来也能看清。如果你对颜色不擅长少用高饱和度颜色多用带灰度的柔和色整体立刻会高级不少。4.2 分组框的层次设计分组框不只是把节点圈起来它还能承担“阅读暗号”的功能。我在 cluster 的样式里一般会设置stylerounded,filled让分组框有浅色背景内部节点保持白色或更浅的底色这样两层视觉层次就出来了。如果分组框内部节点也用深色背景整张图会变成“一堆色块”层次反而消失。嵌套是另一个实用特性cluster 里还能再放 subgraph适合表达复杂系统的多级分组。但嵌套层级不建议超过三层否则渲染出的边框、间距会占用大量空间图会变得很散。如果超过三层我的做法是拆成多张图而不是硬塞进一张。4.3 调整布局的三个高频属性实际场景里布局引擎给出的结果不一定完美这时候用几个属性微调就够了ranksame把一组节点强制放在同一水平线上。比如集群内的多个服务节点希望它们并排显示就在这些节点后用{ ranksame; svc1; svc2; svc3; }指定。constraintfalse让某条边不参与排序计算。比如两个模块之间有数据回调但我不想让这条边影响整体方向就给边加上constraintfalse它只画线不挤占布局。nodesep和ranksep分别控制同层节点间距和层与层之间的间距。图太挤就调大太大就调小。这三个属性覆盖了 90% 的布局微调需求。剩下的一些极端情况比如两节点之间距离不满意可以用weight属性调整边的权重权重高的边会尽量保持短且直。4.4 突出主链路的经验架构图往往同时存在主流程和辅助流程如果不做区分读者第一眼抓不到重点。我的做法是主链路线条用更深的颜色、更粗的宽度辅助流程用灰色或虚线。比如 CI/CD 主流程用color#2B6CB0, penwidth1.8异常告警旁路用color#A0AEC0, styledashed。另一个经验是标签不要全写在边上。边太多时标签会重叠尤其是交叉区域几乎无法阅读。我的策略是主干流程的边尽量不写标签靠节点命名本身表达语义只有关键的触发条件、传输协议才在边上标注。图上文字密度越低信息传递效率越高这个理念在代码画图里同样适用。5. 常见问题与排查实录5.1 中文字体变方块或乱码这是最常遇的问题几乎是每个中文用户入门的必经坑。现象是节点里的中文显示成一排小方块或者干脆空白。原因基本就一个渲染环境里没有可用的中文字体或者没有正确指定字体名。解决方案分两步先在node和edge的属性里明确指定中文字体比如fontnameMicrosoft YaHei如果是 macOS 可以用PingFang SCLinux 可以用Noto Sans CJK SC。其次确认系统确实安装了对应字体用fc-list能查到字体列表。补充一条经验如果本机预览正常但 CI 环境里导出乱码多半是 CI 机器没装中文字体。需要把字体文件一起放进项目或在 CI 初始化时安装否则本地能过、线上就挂。5.2 图片导出模糊导出 PNG 后发现文字或者线条有锯齿第一反应不应该是换绘图工具而是检查 dpi。Graphviz 默认的渲染 dpi 偏低导出社交图片和文档插图时明显不够加上-Gdpi150或-Gdpi200会改善很多。dot -Tpng -Gdpi200 architecture.dot -o architecture.png如果希望完全不靠 dpi 解决则导出 SVG在文档里按矢量图引用。5.3 布局方向不受控制有时候明明指定了rankdirLR某些节点还是不在预期位置这通常是边的方向或权重影响导致的。排查思路是先看有没有边把两个大分组“横向拉”在一起导致后续节点被挤到奇怪的位置再看有没有constrainttrue的边参与了跨层排序。我的经验是对不参与主方向的边统一加上constraintfalse主方向立刻清晰。5.4 标签太长撑坏节点节点标签过长时Graphviz 会按照标签长度扩展节点宽度整张图会沿着某一行被拉得很长。解决办法是给标签换行不要在 label 里写一长串句子。如果用的 HTML 标签型节点可以用BR/换行普通标签可以用\n转义换行。同时在全局节点属性里设置margin0.15,0.08控制内边距让节点紧凑一些。5.5 边交叉严重边交叉过多时可读性会断崖式下降。我的排查顺序是先检查是否有不必要的跨模块连线比如两个不同 cluster 的节点之间直接相连这种边通常是交叉的主要来源其次考虑把 cluster 内部的细节隐藏或用注释说明不在主图上展开最后才用ranksame和weight属性手动调整。还有一种经验是不要试图在一张图里表达所有信息一张图对应一个主题是最有效的防交叉手段。6. diagram-design 进阶与文档自动化深度联动6.1 图表进入 Git 后的 review 体验把 dot 文件纳入版本管理带来的最大变化是代码评审的维度变了。以前评审图表只能看一张最终图片现在可以直接看 diff一个节点改了标签、一条边的方向反了、某个 cluster 的颜色变了全部以文本形式呈现。这种可读性让非绘图者也能参与评审甚至能在 Review 评论里直接写“把 p2 到 p3 的边改成虚线”因为大家讨论的是代码而不是对着图片比划。为了进一步提升 diff 可读性我会在提交前对 dot 文件做一次格式化统一缩进和换行。Graphviz 自带了规范化输出参数dot -Tdot cicd.dot -o cicd_normalized.dot这样输出的 dot 文件会自动统一结构减少无意义的 diff 噪声。6.2 把 SVG 嵌入文档系统SVG 是文档集成里最友好的格式。无论是 Markdown、reStructuredText还是各种 Wiki都能直接引用 SVG 文件。我常用的做法是让文档构建时先执行渲染脚本然后通过相对路径引用生成的 SVG![CI/CD 部署架构图](output/cicd.svg)这里需要注意SVG 文件里的字体依赖阅读环境的字体配置如果读者的系统没有对应的中文字体SVG 会回退到其他字体显示可能和设计好的排版有差异。所以如果是 PDF 或打印场景我更倾向导出高 dpi 的 PNG保证字体完全可控。6.3 批量渲染与增量更新项目里的图会越来越多我的建议是建立统一目录规范比如把 dot 文件放在diagrams/目录输出到output/。然后写脚本批量渲染同时利用文件的变更时间做增量更新避免每次全量重绘。一个轻量的实现可以基于 makeDIAGRAMS : $(wildcard diagrams/*.dot) OUTPUTS : $(patsubst diagrams/%.dot, output/%.svg, $(DIAGRAMS)) all: $(OUTPUTS) output/%.svg: diagrams/%.dot mkdir -p output dot -Tsvg $ -o $这样每次只重新渲染发生过变化的 dot 文件效率高也不会产生多余的提交记录。6.4 用 SVG 做图表内文本检索SVG 还有一个隐藏优势它内部是 XML 文本节点标签和边标签都会保留下来。这意味着你可以用普通的文本搜索工具在图里找到某个关键词而不需要逐个打开图片看。我有时候会把所有 SVG 拼成一个索引文件或者用 grep 直接搜索grep -l 告警通知 output/*.svg这个功能在维护大型架构文档时非常有用尤其是当你不确定某张图里有没有提到某个组件时一条命令就能定位。另外Graphviz 还能通过-Tcmapx生成图像映射文件配合 SVG 或者 PNG 可以做出带超链接的交互式图表。节点可以链接到具体的代码仓库、接口文档、监控面板团队内部用来做系统概览页非常高效。最后分享一个我坚持很久的习惯每张图在 dot 文件头部都写清楚这张图的目的、更新人和日期即使图很小也不省略。原因很简单图表一旦变成代码它就有了生命周期后续维护的人需要知道这个图为什么要存在、什么时候改过。代码化的图表最怕的不是画不出来而是退化成“没人愿意维护的死图”。diagram-design 的价值在于让图的维护成本降到最低但前提是有人愿意维护。把图表当代码来规范它就能像代码一样长期健康地生存下去。
RELATED READING

延伸阅读

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