做了这么多年技术方案,我发现自己有个习惯一直没改:先画图,再写文档。画图不是目的,画图是为了让自己把思路理顺,把边界、依赖、链路、异常路径都摆到台面上,让团队在讨论时不用靠猜。这个习惯后来沉淀成了一个个人项目,我用它来统一管理所有架构图、流程图、时序图和部署图,项目代号就叫"diagram-design"。
diagram-design 不是什么新框架,也不是什么了不起的产品,它就解决一个很具体的问题:让图表设计这件事可维护、可版本管理、可自动化、可多人协作。简单说,就是用代码来"画"图,而不是用鼠标来"拖"图。这套方案我前后迭代了好几轮,从最开始纯手写代码片段,到后来形成一套有规范、有模板、有校验规则的工作流,中间踩了不少坑,也沉淀了很多心得。这篇内容不是教程,更像是一次项目复盘,把我做 diagram-design 时的设计思路、工具选型、实操参数、排查经验都摊开来讲,希望能给正在折腾同类方案的人提供一点参考。
这套东西适合谁?我觉得最适合两类人:一类是经常要画系统架构图、网络拓扑图、业务流程图的技术人,另一类是团队里承担技术文档维护责任的人。如果你只是偶尔画一张简单的图,那用在线白板就够了,没必要折腾代码方案。但如果你跟我一样,图纸要反复改、多人协作、还要历史版本,那 diagarm-design 这套思路值得你花半小时看完。
1. 项目整体设计思路:从"画图"到"设计图表系统"
1.1 传统拖拽画图的三个痛点
先说为什么我会从拖拽工具出走。当年团队里统一用的在线绘图工具,说实话,单纯画图挺快的,但一旦图变复杂,问题就全出来了。
第一个痛点是不可复用。一张架构图画好了,下个项目要做类似的架构,你只能对着旧图重新拖一遍,或者复制一张再改,改完之后旧图和新图就彻底分家了。没有组件的概念,一个服务节点、一条链路、一个存储模块,都是死图形,带不走也传不开。
第二个痛点是改图的连锁反应。尤其是大图,调整一个节点的位置,旁边的几条线可能全乱掉,你得不厌其烦地重新拉线、重新对齐。有一次我花了两个小时调一张微服务调用图,改完第三个版本后,第一个版本已经被改得面目全非,而且我根本说不清每个版本之间到底差在哪。
第三个痛点是无法自动化和无法审查。图是图片或者在线画布,没法进 Git 做 diff,没法在代码评审里逐行看变化,也没法加自动化检查。就算开了评论功能,大家也是在图上指指点点,最后合并意见还是得人工操作,非常费劲。
这三个痛点在我经历一个真实项目的时候集中爆发了。那个项目要从单体应用迁移到微服务架构,涉及的模块大概有三十多个,调用关系错综复杂。我用拖拽工具画了第一版架构图,结果评审会议上,架构评审的老师提了十几条修改意见,光改那一版我就改了一晚上。改完之后我就在想,这事情绝对有更本质的解法。
1.2 把图表拆成"数据模型"和"渲染规则"两层
我后来想明白了一个道理:画图这个动作,本质上应该被拆成两层——一层是数据,一层是表达。数据是什么?是节点列表、连边列表、分组列表、依赖关系、分层关系。表达是什么?是布局算法、配色方案、节点形状、文字排布。
想通这一点后,整个方案就豁然开朗了。我不再需要在一张画布上"画"图,我只需要定义数据,然后用一套规则让引擎替我完成排版和渲染。就像做网页一样,HTML 是结构,CSS 是样式,浏览器负责把效果画出来。diagram-design 项目的核心就是这个理念:把架构图的所有信息抽象成结构化文本,让源码管理、版本差异、人肉 review 都变为可能,同时把排版工作交给确定性算法去处理。
这套思路还有个隐藏好处:数据是确定的,渲染规则也是确定的,那同一份图,我可以随时导出成 PNG、SVG、PDF,也可以嵌入到网页文档里,甚至在 CI 流水线里自动生成预览图。这些都成了可能。
1.3 设计目标清单
在开工之前,我给 diagram-design 列了一份目标清单,后面所有方案取舍都对照着这份清单来:
- 所有图表内容必须是纯文本源代码,能进 Git,能 diff
- 关键图表的生成过程必须可自动化,比如提交代码后自动渲染预览图
- 同一套图能导出多种格式,至少包含 SVG、PNG、PDF
- 支持图与图之间的引用,比如一张总览图里能引用另一张细节图
- 风格必须统一,不能每次画出来都不一样,尤其是配色、字体、线型要有全局规范
- 学习成本要可控,普通后端同学能在一小时内上手
这些目标看着朴素,但每一条都排除了不少工具。比如有些工具支持代码生成图,但导出的格式单一,或者不支持图间引用,或者样式自定义能力太弱。能同时满足这些条件的,选择面其实很窄。
2. 工具选型解析:为什么最终选了 Graphviz 体系
2.1 几类代码绘图方案的横向对比
我系统性调研过市面上的方案,大致分成三类。
第一类是纯在线协作画布,比如各种白板工具。这类上手最快,但基本不可能自动化,数据也出不来,排除。
第二类是文档驱动的图表引擎,包括 Mermaid、PlantUML、Graphviz 这几类。它们都能通过文本描述生成图表,支持版本管理,但侧重点差异很大。
第三类是代码即图表库,比如用 Python 或 JavaScript 直接写绘图代码。这类灵活度最高,但需要写大量代码,不适合日常快速画图用。
我做了一个表格来对比第二类中的主流选择,这个表格后来也成了我向别人推荐方案时的标准参考。
| 引擎 | 擅长场景 | 布局控制力 | 渲染质量 | 生态与扩展 | 上手难度 |
|---|---|---|---|---|---|
| Mermaid | 流程图、时序图、甘特图 | 较弱,自动布局不可精细控制 | 中,偏简洁风 | 强,Markdown 集成方便 | 低 |
| PlantUML | UML 图、时序图、用例图 | 中,内置多种布局 | 中,默认风格需调优 | 中,创意commons 生态 | 低 |
| Graphviz | 复杂有向图、层级架构图、拓扑图 | 强,提供多种布局算法 | 高,可精确控制 | 老牌,工具链成熟 | 中高 |
2.2 为什么我对 Graphviz 情有独钟
对比下来,我的选择非常明确:以 Graphviz 为内核,配合少量自定义脚本作为辅助。
Graphviz 最让我服气的,是它的布局算法。它不是简单从左往右或从上到下排布,而是内置了多种算法,每个算法都针对特定图结构做了优化。dot 算法适合有向分层图,neato 适合无向图和力导向布局,fdp 适合大型无向图的能量模型布局,circo 适合环形结构,twopi 适合星形结构。这意味着不管你是画 CI/CD 流程图、微服务调用链,还是网络拓扑、部门组织架构,都能找到合适的布局方式。
而且 Graphviz 对"DAG 有向无环图"的支持非常出色。像微服务调用关系这种典型的分层依赖图,dot 算法生成的排布天然层次分明,层级对齐后视觉非常舒服,这是很多在线拖拽工具都做不到的。
当然 Graphviz 不是没有缺点。默认样式确实丑,中文字体处理也曾经让我头疼过一阵,节点样式自定义的语法有点底层,刚开始写会比较琐碎。但这些问题都可以通过封装解决。我的做法是沉淀一套自己的图规范模板,把字体、配色、间距、节点形状等参数全部抽出来做成常量,所有图统一引用,这样既绕过了学习曲线,又保证了风格统一。
2.3 为什么没选 PlantUML 作为主力
PlantUML 我其实用过挺长一段时间,它在画 UML 图时确实方便,类图、序列图的语法都做得非常直观。但有两个硬伤让我最终放弃了它作为主力引擎。
第一个是布局控制力。PlantUML 虽然内置了布局引擎,但它更偏向"开箱即用",当你要精确控制节点排布、连线走向、组间间距时,能调的参数非常有限。我画复杂业务依赖图时经常有一种"它的布局跟我的表达逻辑不在一个频道"的错位感。
第二个是性能和稳定性。一张包含四五十个节点的复杂图,PlantUML 渲染时间会比较长,偶尔还会出现布局抖动——同样的代码,换一个环境渲染出来的节点位置略有不同。对于追求确定性的我来说,这不能接受。Graphviz 的布局算法确定性很强,同样的输入在任何环境下渲染结果完全一致,这在自动化流水线里是个非常关键的特性。
3. 核心实现细节:Layout、样式与美化参数全解
3.1 布局算法选型:kindred dot 的配置要点
diagram-design 里,90% 的图用的是 dot 布局算法。这个算法适合有向分层图,但默认参数下生成的图往往偏"松散",节点挤在一起或线条交叉严重,所以需要针对性地调参数。
我常用的核心参数有这么几个:
rankdir:控制整体方向,TB(从上到下)、LR(从左到右)、BT(从下到上)、RL(从右到左)。我习惯用 LR,对于服务调用链,从左到右的阅读方向更符合代码执行直觉。ranksep:控制层与层之间的间距。默认值往往偏大,图会显得空洞,我一般设成 0.6 到 1.0 之间。这个参数单位是 inch,设太大图巨长,设太小层次感消失。nodesep:同层节点之间的最小间距。微服务图里同层节点多,我一般设 0.5 左右,太密会跟连线纠缠。splines:连线样式。画架构图我推荐ortho(正交线),画依赖图推荐spline(平滑曲线),而默认的polyline效果最丑,通常要避开。rank子图约束:这个是我反复用到的技巧。你可以在子图里定义rank=same,强制几个节点排在同一水平线上,这样表达"同时/并行"的关系非常合适。我经常用这个把同一时间部署的多个服务放在一层。constraint=false:当一条连线只是表示关联而不是严格的层级依赖时,把这个属性置为 false 可以告诉布局引擎"不要因为这条线调整层级顺序",能有效减少层级错乱。
这些参数看起来很零碎,但它们直接决定图的骨架。一次调对了,后面的样式微调就只是润色。
3.2 配色体系:为什么看着高级的图都很"克制"
我看过很多架构图,第一眼的吐槽往往不是线条乱,而是颜色乱。有的人喜欢把每个服务节点都涂一种颜色,结果整张图变成了彩虹糖,看一分钟眼睛就累。
我的配色原则有四条:
第一,色系不超过五种。一套图的语义角色一般就几类:核心服务、基础组件、外部依赖、存储、消息队列。我用五种色系分别对应这些角色,再多就要考虑是不是图的抽象层级出了问题。
第二,饱和度和明度统一。取色的时候不要这个亮那个暗,我的方法是固定在 HSL 模型里取色:色相 H 分布在色环几个主区间,饱和度 S 统一控制在 35% 到 50% 之间,明度 L 统一控制在 60% 到 75% 之间。这样做出来的色系哪怕色相不同,放在一起也舒服。
第三,连线用灰阶。节点可以有颜色,但我强烈建议连线用统一的灰色或者浅中性色,让视觉重心保持在节点内容上,而不是被花花绿绿的线条带走。
第四,边框描边与悬浮细节。Graphviz 里节点支持样式、颜色、线宽、填充。我给核心服务用的填充色偏浅,边框用同色系的深一号色,这样既温和又有边界感。如果追求更高级的质感,可以给有状态服务加虚线边框,无状态服务用实线边框,这个语义暗示在交流时很有用。
我把这些规则全部固化在一个样式模板里。后来团队里任何一位同学画架构图,只要引用模板,出来的风格就跟我亲手画的一模一样。
3.3 字体与中文显示:一个容易被忽略的坑
Graphviz 默认渲染是比较"理工"的,中文字体如果配置不到位,出来的图里中文就是一堆方块。网上的方案五花八门,试过一圈后,我的配置是这样的。
在 dot 脚本里,通过fontname和fontsize控制字体。中文场景下,不能用默认的 Helvetica,Linux 上我用"Noto Sans CJK SC",macOS 上用"PingFang SC",Windows 上用"Microsoft YaHei"。但只设 fontname 还不够,因为 Graphviz 渲染时是通过 fontconfig 系统字体数据库来找字体的,如果系统里没有安装对应字体,就会回退到默认字体。我的做法是在 CI 环境里预装字体,同时在脚本顶层统一写fontname="Noto Sans CJK SC",这样不管在哪台机器上跑,出来的字体一致。
还有个细节:中文标点。老版本的 Graphviz 对中文全角引号、破折号的渲染支持不完美,容易变成乱码或豆腐块。我的规避方案是两种,要么把标题里的中文标点替换成横向排版更稳妥的符号,要么用 HTML-like 标签的节点文本做局部字体控制。经过这个调整后,中长文本的标题再没出过问题。
3.4 导出与多端适配
图做出来之后,导出是另一门学问。SVG 适合网页插入,PNG 适合文档,PDF 适合打印,但如果一张图特别大,直接导出 PDF 经常会遇到内容被页面裁切的问题。
我的做法是,用 Graphviz 的-T pdf导出前通过环境变量或参数指定页面尺寸,也可以用-Gpage和-Goverlap=false来避免节点重叠。另外,SVG 导出后可以在文本编辑器里微调,加上超链接和 tooltip,这个能力在为图片文档加跳转时非常实用。每个节点只要在 dot 里配置URL和tooltip属性,生成的 SVG 就天然支持点击跳转。我把这个用在了技术架构文档上,读者点一个服务节点,直接跳到该服务的详细接口说明页,效果非常惊艳。
4. 实操过程:一个微服务架构图的完整落地
4.1 从需求到草稿
为了展示 diagram-design 的用法,我完整复盘一张"订单中台微服务架构图"的制作过程。这个图是某团队在技术汇报前要用的,要求表达十几个服务如何协作、如何依赖基础组件、外部系统通过什么网关接入。
第一步不是写代码,是梳理结构。我在纸上列出节点清单、分组关系、层级关系。这个环节我坚持用"一句话描述一条边"的方式来梳理,比如"订单服务调用库存服务"、"网关路由到用户服务"。当所有依赖关系都用白话文写出来后,再转成 dot 语言,效率非常高。
4.2 写 dot 脚本与调整参数
下面是我简化后的核心脚本结构。节点定义、边关系、分组都清晰分块。
digraph order_middle_platform { rankdir=LR; splines=ortho; nodesep=0.6; ranksep=0.8; node [shape=box, style="rounded,filled", fontname="Noto Sans CJK SC", fontsize=12, fillcolor="#E8F0FE", color="#1A73E8", penwidth=1.2]; edge [color="#5F6368", fontname="Noto Sans CJK SC", fontsize=10]; subgraph cluster_gateway { label="接入层"; style="rounded,dashed"; color="#80868B"; gateway; auth; } subgraph cluster_services { label="业务域"; style="rounded,dashed"; color="#80868B"; order_service; stock_service; user_service; payment_service; } subgraph cluster_base { label="基础组件"; style="rounded,dashed"; color="#80868B"; mysql [shape=database, fillcolor="#FFF3E0", color="#FB8C00"]; redis [shape=cylinder, fillcolor="#F3E8FD", color="#9334E6"]; mq [shape=box, fillcolor="#E6F4EA", color="#188038"]; } gateway -> auth [label="鉴权"]; gateway -> order_service [label="路由"]; order_service -> stock_service [label="扣减库存"]; order_service -> payment_service [label="创建支付"]; order_service -> mysql [label="读写"]; stock_service -> redis [label="缓存"]; payment_service -> mq [label="发送消息"]; }写完后第一版渲染,整体结构基本出来了,但有两个问题:第一,三个基础组件节点排得太散,一眼看过去没觉得它们是一类的;第二,ortho 线型在两条边交叉处出现了多余的直角拐弯,视觉上略杂乱。
第一个问题的解法是用rank=same将三个基础组件约束在同一个 rank,视觉上形成"基础设施一栏"的效果。第二个问题我调了一下nodesep并给部分边增加了weight属性,权重高的边会尽量保持短直线。
4.3 自动化渲染与输出
我不可能每次都手动敲一遍命令。我在项目里加了一个 Makefile 或者脚本,把整套渲染流程固化下来:输入.dot源码,依次输出 SVG、PNG、PDF、以及带嵌入字体的版本。核心命令就一行,在 shell 里执行dot 架构图.dot -T svg -o 架构图.svg,再配合一个小脚本循环处理所有.dot文件。
做到这步,diagram-design 已经不再是一个"画图工具",而是一套文档生产流水线。每次更新.dot源码,跑一遍脚本,所有文档里的架构图自动刷新。技术文档团队后来完全接受了这套流程——他们不再维护图片文件,只维护源码文本。
4.4 用子图分区管理视觉信息
子图(subgraph)是 Graphviz 里最不该被低估的功能。很多人画图不用子图,所有节点平铺,最后图面层次全无。我的经验是,只要有三个以上语义分组的节点,就应该用子图把视觉区块做出来。
子图有两种用法,一种是用cluster前缀让它形成一个视觉区块并绘制边框,另一种是仅用subgraph来做 rank 约束但不显示边框。前一种用于分区管理,后一种用于精调层级。要注意,无论是哪种,子图的标签(label)默认会显示在图区的顶部,这正好可以作为分区标题来用。我常把"接入层"、"业务域"、"基础设施"这样的字眼作为子图的 label,一眼就能定位图的信息结构。
5. 常见问题与排查技巧实录
5.1 中文乱码和"豆腐块"问题
这是 Graphviz 新手遇到最多的一个问题。我归纳过典型现象:在某些 Linux 环境里,节点的中文标签全部显示成带问号的方块。这个问题的根源是字体缺失,而不是图语法错误。
排查顺序是:先检查系统有没有中文字体,命令用fc-list :lang=zh;再检查 dot 脚本里是否设置了fontname;最后检查渲染环境是不是之前装过一次字体缓存没更新,运行fc-cache -f强制刷新。
我的统一规避方案是,在 CI 镜像里预装字体包,并在所有 dot 的顶部统一写fontname="Noto Sans CJK SC"。加了这一行后,乱码问题基本绝迹。
5.2 大图渲染太慢,卡成幻灯片
有一张系统全链路图,里面大概是 90 多个节点、200 多条边,渲染一次要 40 多秒,完全没法在文档迭代中用。排查后发现,问题出在大量节点用了style="rounded"和较粗的penwidth,导致渲染引擎在抗锯齿和圆角计算上开销过大。
我的解决办法有三个手段:第一,把节点形状统一成box,省掉圆角计算;第二,导出 PNG 时降低位图尺寸渲染的负担,在不影响清晰度的前提下控制导出的 DPI;第三,把完全独立的子图拆成多个单独的图文件,先用总览图引用各子图的缩略版。拆开之后,每一张图渲染时间控制在一两秒,总览图只做信息索引,所有细节图独立维护。这比一张大图硬扛高效得多。
5.3 连线交叉严重,布局乱到没法看
有向图节点一多,连线交叉是必然的,但交叉多了图就读不懂了。我遇到过一张图,节点分布完全正确,但边交叉得像一团乱麻。这种问题通常是边关系没有表达出真正的"层级依赖性"。
Graphviz 里边的weight属性可以控制边对布局的影响力。给主干链路加上较高的 weight,比如 5 或 10,布局器会优先让主干链路保持短和直,次要链路绕开,交叉就会明显减少。还有一个很实用的技巧是增加不可见的边,用style=invis强制某些节点对齐,副作用是消除掉一些横七竖八的错位,视觉上瞬间整齐。
5.4 导出的 SVG 在网页里字体失真
SVG 是矢量图,按理说放大不糊,但也有例外。如果生成 SVG 时使用的字体在访问者机器上没有安装,浏览器会按系统默认字体回退渲染,这会导致字体形态和间距跟原图有细微差别,严重时换行都会变。
我的解法是,需要嵌入网页的图尽量用 PNG 导出,尺寸选大两倍,CSS 里控制显示宽度,这样既保持清晰度又避免字体回退波动。如果一定要用 SVG,那就需要把字体文件一起打包或者使用系统普遍预装的无衬线字体作为兜底,某某苹方和思源黑体在不同系统间表现不一致,不推荐直接依赖。
5.5 常见问题速查表
| 现象 | 可能原因 | 处理优先级 |
|---|---|---|
| 中文字体显示为方块 | 系统缺少中文字体,fontname 未设置 | 高 |
| 节点重叠在一起 | 布局松弛,ranksep/nodesep 过小 | 高 |
| 边过于扭曲 | splines 设置为默认值,建议用 ortho | 中 |
| 输出分辨率模糊 | PNG 分辨率不足,需要用 DPI 参数 | 中 |
| 渲染太慢 | 节点过多且样式复杂 | 中 |
| 不同环境下渲染结果不一致 | 字体未锁定,版本不一致 | 中 |
| 连线穿过了节点 | 布局算法对该结构不友好,调整 weight 或换 splines | 低 |
6. 这套方案我还在继续扩展的玩法
diagram-design 做顺了之后,你会发现它的价值远不止"画图好看"。我自己后续又加了很多扩展玩法,但目前最实用的有三个。
第一个是图表版本对比。图形本质是代码,代码就有 diff。以前文档审阅看到架构图更新,只能凭记忆比对两版图哪里改了。现在我可以直接在代码评审里看改动行,比如"订单服务新增了对消息队列的依赖"这个变化就是源码里新增了一行边定义。评审效率提升非常明显。
第二个是生成文档流水线。我在文档项目里配置了自动化任务,每次源码合并后自动重新渲染所有图,并且自动把 PNG 替换到文档仓库里。文档发布前不需要任何人手动截图,减少了人为遗漏。
第三个是图结构自动化检查。基于点边数据写一些简单的断言脚本,比如检查"所有服务节点至少被一个服务调用",或者"图里不能存在孤立的无入度节点"。这些检查挂在 CI 里,每次提交自动跑,能提前暴露架构设计里的依赖缺失。
我在实际使用 diagram-design 这套方案的过程中,最大的感受是:图表的本质不是"画得好看",而是"表达准确"。当你把表达本身抽象成代码和数据,所有的维护、评审、自动化、复用才成为可能。踩过几次坑之后,我现在画任何一张架构图,都会习惯性先问自己三个问题:这里面的节点是什么、边代表什么关系、谁负责维护这份图。只要这三个问题想清楚了,用什么工具反而不重要了。
如果你也在维护一套长期演进的技术文档,我建议你花点时间把图表生成流程沉淀成代码方案。刚开始可能会觉得写 dot 脚本比拖拽慢,但只要图的结构开始变化、开始有版本迭代,代码方案的优势就会迅速体现出来。一个小技巧是,把所有样式的常量都放到一个公共模板里,任何新图都从模板开始,你会发现自己画图的效率反而比拖拽工具更快,而且每张图都长着一张"家族脸"。