← 返回博客
语言: English 中文
Visual 2026-05-22 9 分钟

如何选择 Graphviz 布局引擎并生成可复现架构图

Graphviz 1991 年起源于 AT&T 贝尔实验室,由 Stephen North 与 Emden Gansner 写来作为研究工具,用来可视化函数调用图与流程图。2004 年开源,过去 35 年里几乎所有“想取代它”的工具,要么整个抄了 DOT 语言,要么悄悄在底层用了 Graphviz 的布局引擎。原因:把一张有向图布得不像泼出去的意大利面,是个被研究得很透的难算法问题——把这件事解决了的,就是写 Graphviz 的那批人。

GraphvizDOT布局MermaidD2可视化

如果你用过 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。输出按层整齐,边大致单向,交叉数最少化。

如果是几十节点的无向关系图,neatofdp 看起来更干净——dot 会把它硬塞进并不存在的层次里。

非常大的图(数千节点)只有 sfdp 能在有限时间内跑完。它用多层级方法:聚合相似节点、为简化图布局、再展开。代价是布局是近似的。

CLI 命令名就是引擎名:dotneatofdpsfdpcircotwopiosage。本站工具七个都暴露——按图形选,不要按你最熟悉的那个选。

dot 怎么布层次图

dot 用的 Sugiyama 风格算法大致四步:

  1. rank 分配。 给每个节点分配一个水平层(或垂直层),让所有边都从低层流向高层。跨多层的长边用虚拟节点(dummy node)拆段。
  2. 层内排序。 在每层内排列节点,最小化与相邻层之间的边交叉数。这一般是 NP-hard,dot 用启发式 + 迭代。
  3. 坐标分配。 在每层内算 X、各层算 Y,平衡审美约束(父节点居中在子节点之上、相似节点对齐)。
  4. 边走线。 在不与节点框相交的前提下绘制样条曲线。

输出像组织架构图是有原因的——这个算法本质上就是组织架构图算法的推广。

对你作为用户的最大影响:输入顺序很重要。如果两个节点在某层内可以是任意顺序,dot 按你输入的顺序选。重新排你的 DOT 文件行会改变布局。这有时让人沮丧("我把边排序后图怎么变了?"),有时有用("我可以通过节点顺序暗示一个好布局")。

DOT 语法巡礼

你真正会用的特性:

节点带形状和标签:

api [shape=box, style=filled, fillcolor=lightblue, label="API\nServer"];
db  [shape=cylinder, label="PostgreSQL"];

形状:boxellipse(默认)、circlediamondcylindernotefoldertabcomponent 等等几十种。\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 给了你一张丑图

最常见两个原因:

  1. 引擎选错。 一个本质力导向的图(比如没明显层次的网络拓扑)硬塞 dot 看起来糟糕,因为 dot 坚持要分层。试试 neatofdp
  2. 交叉太多。 边交叉无法消除(图非平面),dot 会最小化但不能消除。拆分子图或加 rank 约束有时有帮助。

其他调旋钮:

  • nodesepranksep —— 节点 / 层之间的水平 / 垂直间距。默认 0.25 英寸;调到 0.5 或 1 会更舒展。
  • splines —— 边走线风格。splines=ortho 直角边(适合电路图风),splines=line 直线边(容易乱),默认 Bezier 样条。
  • neato / fdpoverlap=false 防止节点重叠,代价是更占空间。
  • compound=truelhead=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)ExcalidrawLucidchart —— 交互式画图,非文本驱动。

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-graphcargo depsgradle dependencies --visualizeterraform graphkubectl 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=truelhead/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 工具

相关文章

继续阅读同一主题领域的实践指南。

查看全部文章

Cookie 同意

我们使用 Cookie 来增强您的体验并展示相关广告。您可以自定义您的偏好。