如何选择 Graphviz 布局引擎并生成可复现架构图
Graphviz 1991 年起源于 AT&T 贝尔实验室,由 Stephen North 与 Emden Gansner 写来作为研究工具,用来可视化函数调用图与流程图。2004 年开源,过去 35 年里几乎所有“想取代它”的工具,要么整个抄了 DOT 语言,要么悄悄在底层用了 Graphviz 的布局引擎。原因:把一张有向图布得不像泼出去的意大利面,是个被研究得很透的难算法问题——把这件事解决了的,就是写 Graphviz 的那批人。
如果你用过 dot -Tpng input.dot > output.png,你就直接用过 Graphviz。如果你见过 Doxygen 的调用图、pip 的依赖树、Linux 内核子系统图、Ansible 库存图、tcpdump --graphviz 的网络拓扑——你间接用过它。DOT 语言有一个稀有性质:人能读、人能手写、机器能生成——这正是它存活下来的最大原因。
这篇是写给"在哪里见过 digraph G { ... },想理解什么时候该用它"的人。
它到底是什么
Graphviz 是一个布局引擎。你给它一份图的描述——节点、边、可选属性——它计算节点位置和边的走线,让结果可读。输出可以是 SVG、PNG、PDF、PostScript、xdot 交互文件,或者只把算好的坐标用 JSON 给你。
输入语言是 DOT:
digraph deploy {
rankdir=LR;
client -> ingress -> api;
api -> db;
api -> cache;
cache -> db [style=dashed, label="invalidate"];
}
这是一份完整程序。digraph 开有向图(无向用 graph)。rankdir=LR 让布局水平,默认从上到下。边 -> 是有向,-- 是无向。属性放方括号里。
DOT 是一个小型 DSL——所有属性合起来约 500 个关键字,但你常用就那么几十个。dot 的 man 页和 graphviz.org/doc/info/attrs.html 上的属性参考是规范文档。
七个布局引擎
Graphviz 自带七种独立算法,每种适合不同图形:
| 引擎 | 算法 | 适合 |
|---|---|---|
dot |
层次式 / Sugiyama | DAG、流程图、依赖图 |
neato |
弹簧模型(Kamada–Kawai) | 带边权的小型无向图 |
fdp |
力导向(Fruchterman–Reingold) | 中小型无向图,"团块"形状 |
sfdp |
多层级力导向 | 大型无向图(数千节点) |
circo |
圆形 | 环形结构、环网拓扑 |
twopi |
径向 | 单根树状数据 |
osage |
簇打包 | 大量嵌套的簇图 |
dot 是默认。如果你的图大致是层次的(大多数从代码生成的图——依赖图、状态机、流程图——都是),用 dot。输出按层整齐,边大致单向,交叉数最少化。
如果是几十节点的无向关系图,neato 或 fdp 看起来更干净——dot 会把它硬塞进并不存在的层次里。
非常大的图(数千节点)只有 sfdp 能在有限时间内跑完。它用多层级方法:聚合相似节点、为简化图布局、再展开。代价是布局是近似的。
CLI 命令名就是引擎名:dot、neato、fdp、sfdp、circo、twopi、osage。本站工具七个都暴露——按图形选,不要按你最熟悉的那个选。
dot 怎么布层次图
dot 用的 Sugiyama 风格算法大致四步:
- rank 分配。 给每个节点分配一个水平层(或垂直层),让所有边都从低层流向高层。跨多层的长边用虚拟节点(dummy node)拆段。
- 层内排序。 在每层内排列节点,最小化与相邻层之间的边交叉数。这一般是 NP-hard,
dot用启发式 + 迭代。 - 坐标分配。 在每层内算 X、各层算 Y,平衡审美约束(父节点居中在子节点之上、相似节点对齐)。
- 边走线。 在不与节点框相交的前提下绘制样条曲线。
输出像组织架构图是有原因的——这个算法本质上就是组织架构图算法的推广。
对你作为用户的最大影响:输入顺序很重要。如果两个节点在某层内可以是任意顺序,dot 按你输入的顺序选。重新排你的 DOT 文件行会改变布局。这有时让人沮丧("我把边排序后图怎么变了?"),有时有用("我可以通过节点顺序暗示一个好布局")。
DOT 语法巡礼
你真正会用的特性:
节点带形状和标签:
api [shape=box, style=filled, fillcolor=lightblue, label="API\nServer"];
db [shape=cylinder, label="PostgreSQL"];
形状:box、ellipse(默认)、circle、diamond、cylinder、note、folder、tab、component 等等几十种。\n 在 label 里是换行。
边带属性:
api -> db [label="reads", style=dashed, color=red, arrowhead=empty];
子图(cluster) 把节点视觉分组:
subgraph cluster_backend {
label = "Backend Services";
style = filled;
color = lightgrey;
api;
worker;
scheduler;
}
cluster_ 前缀是必需的——没它 dot 不画外框,子图就只是逻辑分组而无视觉效果。
HTML-like 标签 用于表格与富内容:
node1 [shape=plaintext, label=<
<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD>Server</TD><TD>192.168.1.1</TD></TR>
<TR><TD>Service</TD><TD>nginx</TD></TR>
</TABLE>
>];
HTML 标签解析的是一小子集——TABLE、TR、TD、FONT、B、I、U、SUB、SUP。这是嵌入多行多列内容的方式。不是真 HTML——别试图嵌图片、脚本或复杂 CSS。
rank 约束 强制布局:
{ rank=same; web; api; } // web 与 api 同层
{ rank=min; users; } // users 在最上
{ rank=max; storage; } // storage 在最下
dot 默认 rank 不符合心智模型时有用。
为什么 dot 给了你一张丑图
最常见两个原因:
- 引擎选错。 一个本质力导向的图(比如没明显层次的网络拓扑)硬塞
dot看起来糟糕,因为dot坚持要分层。试试neato或fdp。 - 交叉太多。 边交叉无法消除(图非平面),
dot会最小化但不能消除。拆分子图或加 rank 约束有时有帮助。
其他调旋钮:
nodesep与ranksep—— 节点 / 层之间的水平 / 垂直间距。默认 0.25 英寸;调到 0.5 或 1 会更舒展。splines—— 边走线风格。splines=ortho直角边(适合电路图风),splines=line直线边(容易乱),默认 Bezier 样条。neato/fdp加overlap=false防止节点重叠,代价是更占空间。compound=true配lhead=cluster_x/ltail=cluster_y让边在 cluster 之间画,而不是穿过具体节点。
Graphviz vs Mermaid vs D2
"从文本生成图"赛道:
- Graphviz / DOT(1991)。成熟、稳定、每个 Linux 发行版都有,最强布局引擎,对新手最神秘的语法。
- Mermaid(2014)。基于 JavaScript,嵌进各种 Markdown 编辑器(GitHub、GitLab、Notion、Obsidian),语法借鉴 DOT 但简单情况下更易读。布局是它最弱的一环——非平凡图常常比
dot难看。Mermaid 的flowchart大致是带额外步骤的 DOT。 - D2(2022)。前 Mermaid 工程师设计的更新选手,原生布局走 Graphviz/ELK + 自家引擎,语法比 DOT 友好。2024 年起达到生产可用。
- PlantUML(2009)。基于 Java,专注 UML(时序、类、状态图)。某些布局底层用 Graphviz。
- Diagrams.net(前 draw.io)、Excalidraw、Lucidchart —— 交互式画图,非文本驱动。
2026 年继续用 Graphviz/DOT 的论据:图是 DAG / 依赖图 / 流程图 / 层次结构;事实源放在代码里(构建系统生成的依赖图、入仓的架构图);想要稳定。我 2010 年写的 DOT 至今还能正确渲染。Mermaid 语法改过好几次,D2 还太新。
切换的论据:图是时序 / 类 / 状态 UML(PlantUML 专门设计);团队在 GitHub/GitLab 工作,行内渲染受益(Mermaid 原生支持);图小到布局质量无所谓(任何工具都行)。
没人提的论据:复杂 DAG 上 Graphviz 的布局比手画或自动布局对手好得多,节点超过一百时质量差距复利累积。
从代码生成 DOT
Graphviz 的杀手用例是 DOT 极易程序化生成。半页 Python 或 Go 就能遍历数据结构、把边写成 a -> b;、管道给 dot:
def emit_dot(graph):
print("digraph G {")
print(" rankdir=LR;")
for node, attrs in graph.nodes.items():
print(f' "{node}" [label="{attrs["label"]}"];')
for src, dst in graph.edges:
print(f' "{src}" -> "{dst}";')
print("}")
pip-graph、cargo deps、gradle dependencies --visualize、terraform graph、kubectl get ... -o dot(通过 kompose 等)以及一长串工具就是这么工作的。图按需从事实源生成,永远不过时,不需要交互编辑器。
如果你正在 SaaS 工具和代码库里重复维护同一张图,正确做法是从代码生成图(或反过来),选一边作事实源。DOT 适合这件事,因为它纯文本、可入仓、diff 友好。
常见坑
- 引擎选错。 层次图用
dot、无向用neato/fdp、大规模用sfdp。默认不总对。 - 子图忘了
cluster_前缀。 没它框框不画。 - HTML 标签不是真 HTML。 支持子集很小且 Graphviz 专属。
- 引号问题。 含空格、连字符、标点的节点 ID 必须引起来:
"my node"。给已带引号的节点再加引号会破坏解析。 - 布局不稳定。 重排输入行会改布局。如果把渲染图入仓,对输入行排序(或确定性生成),让 diff 有意义。
- 巨型图丢给
dot。 数千节点以上运行时间爆炸、输出不可读。用sfdp,并思考要省略什么。 - 边标签重叠节点。 长边标签盖到节点上,把它移到节点属性(
xlabel)或缩短。 - cluster + 边歧义。 cluster 间的边要
compound=true加lhead/ltail。否则边走具体节点,cluster 边界看起来对不齐。 - 嵌入图片。 可以用
image=属性,但路径相对dot运行目录解析,CI 里通常不对。内联 base64 SVG 是个能用的 hack。 - 想"双向"操作 DOT 布局。
dot是单向的:输入 → 图像。没法手动挪个节点然后让改动写回 DOT 输入。
什么时候用 Graphviz、什么时候不用
用 Graphviz 当:
- 图从源码、配置或数据库生成。
- 图是 DAG / 依赖图 / 多于若干节点的层次结构。
- 想要一种 diff 干净的文本格式入仓。
- 布局质量比编辑速度重要。
用别的 当:
- 图很小(约 10 节点),写一段 Mermaid 就够。
- 在 Markdown 生态里写文档、宿主原生支持 Mermaid。
- 图需要 UML 专用形状(时序、类)—— PlantUML。
- 需要交互编辑 —— diagrams.net 或 Excalidraw。
2026 年技术架构图最接近"对的答案"的工作流是:源放 DOT、用 Graphviz 渲染、把渲染出的 SVG 和源一起入仓。任何装了 dot 的人都能从仓库里重新渲染,审阅人无需工具链就能看到 SVG。代码是事实源,图像是产物。
这个工作流就是 Graphviz 1991 年被设计来做的事,意外地经得住时间。
主要参考资料
用于核对本文技术细节的标准与官方文档。
在浏览器内渲染 DOT 图
本站 Graphviz 工具用上游引擎的 WebAssembly 构建在本地渲染 DOT 图,七种布局引擎(dot、neato、fdp、sfdp、circo、twopi、osage)全支持,可导出 SVG / PNG。适合勾画架构图、依赖图、流程图,并且不会被锁进某家 SaaS。数据不会离开浏览器。
打开 Graphviz 工具相关文章
继续阅读同一主题领域的实践指南。
Node 生产 Dockerfile 里到底该有什么,不该有什么
网上大多数 Node Dockerfile 都把 node_modules 直接拷进镜像、用 root 运行、最后产出一个 900 MB 的层。本文只讲那几个真正影响构建时间、镜像体积和运行时安全的决定:基础镜像、多阶段构建、依赖层缓存、NODE_ENV 陷阱,以及为什么你的 docker-compose 不该照搬生产。
在用户之前发现缺失的翻译键和插值参数不匹配
缺失的翻译键会把原始键路径直接渲染给用户,插值参数不匹配会渲染出空串或崩溃。这两者在评审中都容易漏,因为开发者的语言包永远有全部键。本文讲如何结构化比较 locale JSON 文件、找出缺失键,并在发布前抓住参数不匹配。
能真正压测 UI 的 mock 数据(而不是只把页面填满)
大多数 mock 数据是同一行复制十遍、只换个 id。它填满页面,却什么都测不到。本文讲如何生成能压测布局边界、长名字、缺失字段、空状态,以及会破坏格式化代码的日期和数字格式的 mock 数据,并通过字段推断让一个 JSON 样本一步变成贴近真实的数据集。