ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

代码画架构图:Diagrams库选型与自动化维护实战

代码画架构图:Diagrams库选型与自动化维护实战

diagram-design 这个词,字面意思就是“图的设计”。但在我这里,它有个更具体的身份:一套把系统架构图、云上拓扑图、甚至数据流转图,从画布拖拽变成几段可维护代码的完整打法。你拿着它,可以把团队里“画图两小时、改图一下午”的事情,压缩成一条命令;也可以让架构图跟着仓库的每一次 diff 走,再也不是某个人电脑里的私有文件。如果你正在写技术方案、做云架构评审,或者要给新来的伙伴讲清楚系统长什么样,这套玩法能直接帮你省掉最烦的那部分重复劳动。

这篇文章不会写太多虚的,我把从选型到落地、从样式微调到踩坑修复的过程,按真实顺序过一遍。适合被画布工具折磨过的人看,也适合那些不想让图烂尾的团队参考。就算你暂时不打算引入新工具,了解背后的图模型原理,对怎么把图画清楚也有帮助。

1. 图表设计这个事,为什么值得用代码来做

1.1 手动画图的三个老大难

先说手动画图的问题。这里不是要完全否定画布工具,Visio、draw.io 这些在临场开会时确实很好用,但一旦图要长期维护,问题就全冒出来了。

第一个问题是版本管理。手工画出的图,本质上是一张位图。这周拓扑改了两个节点,你保存为 v8 副本;下周同事又改一处,另存为 v9,两个版本之间到底有什么区别,没人说得清。图片文件很难像代码一样做 diff,也难做分支合入,谁的保存顺序靠后谁说了算,最后总会有人在群里喊一句:“这张图是最新的吗?”

第二个问题是复用性差。我经常在一个项目里见五种长得几乎一样的图,区别只是某些节点颜色不同、一些服务名不一样。想做成模板?画布工具的模板能力有限,结果通常是复制一份再改,导致同一个系统里图越来越多,但一致度越来越低。改图的人还得小心翼翼地避开那些“只属于这个视图”的特殊元素,稍不注意就动错地方。

第三个问题是自动化断层。手工图很难跟真实环境校验。线上挂了十台机器,图里还标着三台;数据库换了内核版本,图里的数据库图标还是老样子。这些漂移在日常维护里太常见了,而图的作者往往是最先离开项目的那个人。维护一张手工图,本质上是在维护一份“有人记得才有用”的记忆,可这种记忆在项目里恰恰最脆弱。

1.2 代码画图解决的核心问题

把图变成代码后,事情开始变得有趣。首先是版本可追踪。你在 Git 里改一行,给某个服务节点重命名,提交之后 diff 里能明确看到哪个节点变了。下次谁再问“图准不准”,直接把仓库和最新提交丢过去就行,不用再解释“我这版是改过的,你手上那版是上周的”。

其次是可组装复用。定义一个“标准 Web 服务”组件,负载均衡在后,计算节点在中间,数据层在最底下,这些函数可以在不同项目里 import,不用反复从零开始拖。我甚至会在团队里把这些组件定义抽成一个公共模块,谁要画新项目架构图,直接引用基础组件,再补少量自定义节点,半小时内就能出一张规范图。

然后是结构化验证。图不只是给人看的画,背后是有数据结构的。节点有哪些、边怎么连、哪些服务依赖数据库,这些都可以写测试去断言。我加过一个很简单的检查:所有写操作必须经过服务层,不允许业务节点直接连存储节点,一旦图里违反了规则,流水线就直接标红。这在手工图里基本不可能实现。

代码画图的终极形态,是从配置或者真实资源定义里去自动生成图。集群里跑着一个服务清单,脚本读取之后生成拓扑图,每天定时更新。图不再是人工维护的宝贝,而是资产文件的附属品。只要上游信息准确,图永远不会过期。

1.3 不是所有图都适合代码画

一件事值得做,不代表所有图都该往代码里塞。我自己的判断标准很简单:图的内容是否跟着代码结构变化?是否经常需要重新发布?是否有明确的节点和边结构?三个回答都是“是”,那就适合代码画。

反过来,那种给运营小伙伴看的用户路径宣传图、各种视觉冲击力强的营销一图流,或者需要大量手绘风格创意的海报图,还是交给设计师和画布工具更合适。还有那种临时头脑风暴画的草稿,直接拿白板画就行,别打开编辑器。不要用代码画画的冲动去对抗创意工作,工具各有所长,硬套反而拖慢节奏。

2. 工具选型解析:Graphviz、PlantUML、Diagrams库怎么选

2.1 三种主流方案的横向对比

选型之前,需要认识目前比较主流的三种代码画图路线。第一种是 Graphviz,最老牌,用所谓的 dot 语言描述图结构,然后由 dot 工具自动排版,输出成图片。第二种是 PlantUML,语法更口语化,尤其适合画 UML 类的图。第三种是 Diagrams 库,一个 Python 库,用对象建模的方式定义节点和边,底层还是会去调用 Graphviz 完成排版。

维度GraphvizPlantUMLDiagrams 库
描述方式dot 语法,偏底层类 UML 文本Python 对象,贴近代码思维
上手成本中等,需理解布局概念低,几行就能出图低,会写 Python 就行
图标生态基本全靠文本标签自带部分 UML/云图标云服务商组件齐全
二次开发/自动化一般靠命令行一般可以直接嵌入 Python 生态
适合画什么通用图、流程、拓扑时序图、类图、状态图架构图、云上拓扑

这个表格不是想选出谁最强,而是告诉你不同场景的舒适区。我见过有人用 Graphviz 硬画非常复杂的业务流程图,节点和边多到根本没法读;也见过有人用 Diagrams 库去画严格意义上的 UML 类图,结果绕了很多路。工具本身没有高低,只有匹配度。

2.2 底层不变量:图结构与自动布局

无论你选哪条路线,底下都逃不开同一个概念:图。一张架构图,拆到最小,就是一堆节点和连接它们的边。画图是个视觉行为,但设计图本质是建模行为。代码画图帮你把“建模”这一步提出来,把坐标、颜色、排版这些视觉杂事交给布局引擎。

你可以理解为:你只告诉导航软件起点、终点和途经点,路线长什么样由导航自动规划。很多新人第一次看到 Diagrams 库里几行代码就能生成一张干净图,都觉得神奇,其实就是这个原理。节点之间的“关系”才是你真正要维护的信息,而坐标和线形只是关系的投影。

这个思路对人的要求反而更高了。因为当你用鼠标拖图时,区间划分、前后层级这些你潜意识里会想清楚;用代码画图时,如果关系定义得模棱两可,渲染出来的图一定模棱两可。所以代码画图不是降低思考要求,而是把思考落成了可检查的文本。

2.3 我的选型结论:为什么主推 Diagrams 库

如果以架构图为目标,我个人最常用的其实是 Diagrams 库,理由有三点。第一,Python 生态能跟日常脚本无缝衔接。第二,它封装好的云服务图标已经相当全,AWS、Azure、阿里云这些都有,画云拓扑的时候很省事。第三,当你需要做更复杂的自动化时,可以拿 Python 直接生成图定义,天然适合动态拼装。

但这不代表 PlantUML 不好。团队如果全员用 PlantUML 做文档生态,那就别折腾,跟着生态走。而如果你只是偶尔画一张简单的数据流图,Graphviz 的轻量写法已经够了。工具选型不是站队,是为了省事。这也是“diagram-design”这件事的核心逻辑:先理解你的图是什么类型,再决定怎么设计它,最后才轮到选工具。

3. 从零实现一套可维护的架构图工程

3.1 初始化环境与最小编码

下面进入实操。我最常用的环境是 Diagrams 库加 Graphviz,先说安装。Diagrams 库本身的安装很简单,一条pip install diagrams就够。但它渲染依赖 Graphviz 这个底层的布局和渲染引擎,需要提前装好。macOS 可以直接brew install graphviz,Debian/Ubuntu 用apt-get install -y graphviz,Windows 则下载安装包后把 bin 目录加进 PATH。很多同学报错找不到 dot 命令,基本就是这一步没做全。

安装完可以先跑一个最简示例:

from diagrams import Diagram from diagrams.aws.general import User with Diagram("访客示意", show=False): User("访客")

show=False是让代码渲染后不要自动弹出图片,对自动化场景很友好。保存为 demo.py,执行python demo.py,当前目录就会生成一张访客示意.png。你可以用任意图片查看器打开,看到单个节点居中显示。虽然只有一个节点,但跑通了整条链路。看到文件生成,就说明你的 Graphviz 和 Diagrams 库已经打通了。

这个最小示例看起来没什么用,但它是后面一切的基础。我一般会把它作为环境是否正常的冒烟测试,换了新电脑第一件事就是跑这个。

3.2 设计一个“负载均衡+两类服务+数据库+存储”的拓扑图

现在演示一个小型应用拓扑,更贴近真实场景。这个结构模拟了一个很常见的局面:流量入口经过负载均衡,一路进入 API 服务,另一路进入异步任务;二者都需要访问主数据库,数据最终沉淀到对象存储。集群在这里把服务层和数据层包起来,视觉上一下子清楚很多,比所有节点全部平铺要好读。

from diagrams import Diagram, Cluster, Edge from diagrams.aws.compute import EC2 from diagrams.aws.database import RDS from diagrams.aws.storage import S3 from diagrams.aws.network import ELB with Diagram("小型应用拓扑", show=False, direction="LR"): lb = ELB("负载均衡") with Cluster("服务层"): api = EC2("API 实例") worker = EC2("异步任务") with Cluster("数据层"): db = RDS("主数据库") bucket = S3("对象存储") lb >> Edge(label="请求入口") >> api >> db >> bucket lb >> Edge(label="写入任务处理") >> worker >> db api >> Edge(label="数据读写") >> db

不要小看这一层 Cluster,它对应着真实架构里“逻辑边界”的表达。服务层和数据层在物理上可能跨机器、跨网络,但在架构图上,你希望读者第一眼就能理解这两个逻辑区域之间的关系。Cluster 就是用来承载这种逻辑分区的。

我在实际项目中会发现,很多新手画图时只关注节点本身,忘了画“边”的语义。其实边才是架构设计里真正需要花心思的地方。哪条链路是同步调用,哪条是异步,哪条是低频请求,这些信息在文字文档里要用大段话说明,在图上其实用一条带标签的边就能表达清楚。

3.3 布局方向、边标签和自定义样式

布局方向我用direction="LR"控制,也就是从左到右。默认是 TB,也就是从上到下。数据库的架构图常喜欢用 TB,长链路的调用用 LR 看着更顺,这个取决于项目习惯。边是可以加语义的,给负载均衡到异步任务的边加 label,图里的阅读者就明白这条链路是干嘛的,不用靠猜。

样式方面,Edge 可以设置 color 与 style,比如把关键链路标红、把异步流程画成虚线。Diagrams 库也支持自定义节点图标,可以用Custom("节点名", "图片路径"),这样即使没有官方图标,也能用本地图片素材撑住场子。比如有些私有协议组件,官方图标里没有,我一般会把内部系统截图抠成小图,然后用 Custom 节点放进去,图就不会出现“这个框是什么,为什么没有图标”的疑问。

还有一个容易忽略的功能是outformat。默认输出 PNG,但如果你要把图嵌进文档,SVG 更适合。改成with Diagram("示例", outformat="svg"),输出就是矢量图,放大不糊,很适合技术文档印刷或投屏。

3.4 把图接进文档和 CI 流水线

手动执行 demo.py 没问题,但真要长期用,必须接入工程流程。我通常会把图生成到docs/architecture/目录,然后文档里用静态资源方式引用。如果团队用了 MkDocs 或 Sphinx,还可以写一个小脚本,在构建文档前批量渲染所有架构图,文档站自动更新。

更进一步,可以在 CI 流水线里加一个任务:每次主分支有变更,就执行渲染脚本,再把生成的图片和之前的版本做对比。如果业务代码改了而架构图没有更新,测试直接标红,从流程上逼着大家同步维护图。这一步看着有点严,但对一个长期项目来说特别实用。截图神器没了,图就不会再腐烂了。

我举个例子。某个团队之前有个约定:每次服务扩容或下线,都要更新架构文档。这个约定靠口头提醒,坚持了两周就崩了。后来我把架构图渲染脚本挂进 CI,凡是涉及集群配置变更的目录有改动,就自动触发渲染,并把“架构图已自动更新,请人工确认”打印在流水线输出里。从那以后,架构图基本没再过期过。

4. 常见问题与排查技巧实录

4.1 布局变乱、节点重叠怎么办

很多人第一次生成的图都会遇到一个问题:节点一多就挤成一团,线乱飞,标签互相重叠。这不是你笨,而是布局引擎默认的算法有时候不够理想。解决办法有几个。

第一,用小集群拆分。把强相关的节点包进 Cluster,布局引擎会优先考虑它们之间的紧密程度。第二,控制整体方向。一个图里不要一会儿横一会儿竖,选定 direction 后保持统一。第三,把大图拆成多张。我从一次教训里学到:当一张图有超过四五个集群、二十多个节点时,阅读成本已经明显上升,不如拆成系统全景图和各模块子图,全景图用粗粒度节点,子图做细粒度连线。

最后还可以通过 graph_attr 直接改 Graphviz 底层的布局参数。

with Diagram("调整布局", show=False, direction="TB", graph_attr={"ranksep": "0.6", "nodesep": "0.5"}):

ranksep和nodesep控制的是层级间距和同层节点间距,数值调大一些,节点就松开了。这个参数组合我经常用来处理节点过密的问题,比手动回去调坐标省事得多。

4.2 中文乱码与字体问题

第二个高频问题是中文乱码。Graphviz 在渲染文本时需要一个能覆盖中文字符的字体,系统没装对应字体或者不认识,就画成一个个方块。解决办法不难:第一,系统层面装上中文字体,比如 Noto Sans CJK;第二,在 Diagram 里告诉 Graphviz 用哪个字体名。

with Diagram("中文架构图", show=False, graph_attr={"fontname": "PingFang SC"}):

Windows 上可以写Microsoft YaHei,macOS 上写PingFang SC,Linux 上写Noto Sans CJK SC。我这里强烈建议,标签优先用英文,结构清晰还不挑环境;实在要用中文,就把字体配好,否则换一台机器渲染就容易出问题。

字体问题不止影响标题,节点内的文本也会受影响。如果一张图里既有中文又有英文,最好统一字体族,避免某个字体缺字导致个别标签变成方块,那比整体乱码还难排查。

4.3 渲染成功但节点消失或连线错误排查

还有一种看起来很诡异的情况:代码运行成功,png 也生成了,但有节点就是不在图上。我先说一个最常见的罪魁祸首:重复的变量名。Diagrams 库在 with 块里,如果你把两个不同节点赋给同一个变量名,后面那个会把前面那个覆盖掉,图中就会少一个节点。这个坑很隐蔽,代码不报错,图就是缺东西。

另外,节点必须定义在 with 块内部。有些同学把节点构造放在 with 外面备用,结果会发现它们根本不会出现在图里。还有,边尽量按真实依赖走,不要为了视觉效果重复连接同一个节点,这会导致布局引擎多绕路,图越画越乱。

当节点真正缺失时,排查顺序建议是:先检查变量名是否重复;再检查节点是否在 with 块内;最后检查该组件图标在当前库版本里是否存在。升级 diagrams 后偶尔会有路径变动,报 ImportError 就换一个等价组件。这套顺序能解决九成以上问题。

4.4 高频问题速查表

现象可能原因处理方式
中文渲染成方框缺少中文字体安装字体并配置 fontname
节点消失变量名重复或节点放错位置检查 with 块和变量
布局拥挤重叠节点过多或未分组用 Cluster 拆分
边乱飞重复连线或方向混乱简化边并固定 direction
找不到输出文件未指定路径或目录不对确认工作目录和文件名
渲染报 dot 命令错误Graphviz 未安装或未加入 PATH安装 Graphviz 并配置环境变量

4.5 长期维护的经验沉淀

最后分享几个维护层面的体会。第一,图的命名要像代码命名一样讲究,别叫“最终版图1”,直接按模块名或系统名命名,比如“前端网关-拓扑”。第二,每次架构调整,顺手更新图定义,这个习惯比任何工具都重要;我自己甚至会把图渲染步骤写进项目 README,防止换人之后无人维护。第三,如果想画多个阶段的演进图,用多个 Diagram 文件分开画,然后把它们引用到一篇概览文档里,不要试图在一张图里塞下过去、现在和未来。

图上只画当前真实状态,这是让它长期可信的唯一标准。我见过太多人试图在架构图里做“规划”,把计划中的服务也画进去,结果图变成立体迷宫,谁也不知道哪部分已经上线,哪部分还在设计稿里。代码画的图保存的是结构关系,当它变成团队共识之后,就只保留真相就好。

我在实际维护这套图的过程中,最明显的感受是:它把“画图”这个动作从一次性劳动变成了持续的小成本投入。每次改完配置,顺手改几行图定义,提交时看一眼 diff,架构就已经跟着更新了。这个习惯一旦养成,你不再害怕别人问“图还准吗”,因为你随时能拉出最新渲染结果。如果你还在为手工图的版本和滞后问题头疼,不妨从今天这个最小示例开始,把第一张图变成代码试试。

RELATED READING

延伸阅读

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