去年年中我接手了一个内部平台的后端重构,光是架构图前前后后改了十几版。最崩溃的不是画图本身,而是每次评审会前一晚,甲方的意见传过来,我发现自己记不清图上某个节点连的是哪条线,只能开着绘图软件一点一点找。那段时间我认真想了一个问题:能不能像写代码一样,把架构图也变成仓库里可以 diff、可以 review、可以自动再生成的东西?于是就有了我独立维护的这个内部代号叫diagram-design的小项目。它不是什么标新立异的框架,就是一套基于 Python 代码来设计和生成系统架构图的工作流。
这篇内容我会把整个思路、工具链、真实踩过的坑,以及我认为最有价值的落地做法全部写出来。适合谁看?如果你的工作里需要频繁产出架构图、拓扑图、流程图,或者你也受够了"图改一版、人改半宿"的状态,这篇文章应该能给你一些可以直接抄作业的方案。我默认你至少写过一点 Python,也用过常见的关系数据库,但即使基础偏弱,跟着步骤走也能跑通。
1. 为什么我把图表从"画"改成了"写":diagram-design 解决的几个真实痛点
1.1 传统拖拽画图在项目协作里的五大致命伤
先不急着上代码,我想认真聊聊为什么我最终会走上"代码画图"这条路。很多团队现在还在用通用绘图工具画架构图,这没什么问题,单人小项目怎么画都行。但一旦图表进入协作流程,拖拽式画图工具的短板就会集中爆发。
第一是修改成本不可控。架构调整往往不是简单挪个方块,而是整体依赖关系的变动。拖拽式工具里,你改了位置,线条的走向、跨区域绕线、分组边界全都跟着乱。第二是不可 diff。代码可以 review,图形文件很难 review。"你这次改了什么"只能靠肉眼对比。第三是版本混乱。团队里每个人本地都有一份最新版图,到底谁的准?没准你改完那一版根本没上传。第四是难以复用。你有几十个微服务,每个服务的架构图大体相似,拖拽模式下你复制一份再一个个改,效率很低。第五是容易格式漂移。不同人画出来的图风格迥异、字体大小不一、颜色随心所欲,最后拼进文档里非常丑。
这些痛点集合在一起,让我意识到,架构图本质上也是一种"文档代码",它应该被版本管理、被审查、被复用,而不是躺在绘图软件的私有格式里。
1.2 "图即代码"的思路与传统做法的本质差异
所谓 diagram-design 的思路,换一个词说就是Diagram as Code(图表即代码)。你不再用鼠标拖节点,而是用 Python 描述节点、分组、连线,然后由渲染引擎自动排版生成最终图片。
这个转变表面看只是"操作方式"不同,背后其实是思路范式的差异。拖拽式画图是把图当作"最终产物",每次修改都是直接改产物;代码画图则是把图当作"程序输出",你维护的是生成这棵图的指令,产物每次都可以被重新生成。就像用 CSS 定义样式,用 HTML 定义结构,而不是每张网页都手工用绘画工具拼出来。
在这个范式下,"该连哪条线、该用什么图标、该放哪个分组"变成了代码审查的一部分,而不是某个人脑子里的记忆。任何一个新同学拿到仓库,他可以读代码理解图表结构,也可以跑一遍命令重新渲染。这对我来说是质变。
1.3 哪些场景适合、哪些场景不适合代码画图
我实践下来,架构拓扑类、系统模块关系类、网络分层类的图表非常适合代码化。这类图表结构性强、节点关系明确、需要频繁跟着系统变更走,代码生成事半功倍。
但也要说句公道话,代码画图不是万能解药。如果你想画的是对外宣传用的精美海报式架构图,需要对颜色渐变、图标细节、留白间距做像素级打磨,那拖拽式软件效率仍然更高。另外一些非常自由的信息图,比如"一张图讲清楚业务运营链路",这类高度依赖人类排版灵感的场景,也不适合用代码硬写。工具选型从来不是越高级越好,而是边界越清晰越好。
2. 环境准备与对象模型:安装 Python 图表库后,先搞懂这四件事
2.1 最小可用的安装步骤与系统依赖
我用的核心渲染引擎是一个老牌开源工具,负责把图表描述语言转换成图片。Python 端只是用它来生成描述文件。整个环境安装非常简单,我直接贴我验证过的命令。
# 以 Ubuntu / Debian 系为例 sudo apt-get update sudo apt-get install -y graphviz pip install diagramsmacOS 上如果你用 Homebrew,对应的是brew install graphviz。Windows 上需要去官网下载安装包,并且一定要把安装目录加入系统 PATH。
这里我先提醒一个新手最容易踩的问题:pip install diagrams很顺利,但运行第一段脚本时报"Executable not found",十有八九是系统里没有装 graphviz 渲染引擎。Python 库本身只是"翻译官",真正的"画家"是 graphviz。所以安装顺序建议先装 graphviz,再装 Python 这个图表工具。
2.2 最核心的四个对象:Diagram、Node、Cluster、Edge
我把这个库的对象模型简化成四类,你和它们打交道的时间占百分之九十五。
Diagram是整个画布对象。它负责定义图的名字、方向、输出格式和样式。你可以把它理解为一块无限大的画板。
Node是图中的节点。每个节点代表一个组件,比如一个服务、一个数据库、一个负载均衡器。节点通常还会带上图标,表示它是哪种类型。
Cluster是节点的分组容器。当你希望一组节点在图上被框在一起,表示一个子系统或一个可用区,就用 Cluster 把它们包起来。
Edge是连线。它描述节点之间的依赖或调用关系,可以设置单向、双向、不同颜色等属性。
用面向对象的思路来看:Diagram 是一个最外层容器,Cluster 是中间层级的分组,Node 是被分组的叶子对象,Edge 是节点间的连接关系。理解了这个四层模型,后面什么图都能拆解成这四样东西的组合。
from diagrams import Diagram, Cluster, Edge from diagrams.generic.compute import Compute from diagrams.generic.database import Database from diagrams.generic.network import Network # 最简单的三层结构示例 with Diagram("最小示例", show=False): client = Network("客户端") svc = Compute("应用服务") db = Database("数据库") client >> Edge() >> svc >> db这段代码里,>>表示连线方向。你可能会觉得这个写法和面向对象没关系,但它非常直观:a >> b就是 a 指向 b。
2.3 方向的底层逻辑:为什么图会自动排版
很多第一次接触代码画图的人会问:我明明没写任何坐标,图是怎么摆出来的?这得益于 graphviz 的自动布局算法。简单理解,它把整张图当作一个"总线网":先计算每个节点的尺寸,再根据节点之间的边关系,通过启发式算法让连线尽量短、交叉尽量少。你只需要定义"谁和谁连着",不需要管"谁在谁的左边"。
但自动排版也不是完全没有"方向感"。Diagram 构造函数里的方向参数默认是向下(bottom to top),你还可以改成从左往右。方向参数会影响整张图的阅读顺序,我一般习惯用direction="LR"表示从左到右,让调用链从左边的客户端一路流向右边的存储层。
with Diagram("从左到右的图", direction="LR", show=False): pass搞清楚这个逻辑之后,你就不会纠结"为什么我画的图和预想的不同"——因为它不是像素级预设,而是算法排版的结果。你要调整的不是坐标,而是连接关系。
2.4 生成第一张图的完整命令与预期结果
看完整流程,写一个最简单的脚本,保存为first_diagram.py:
from diagrams import Diagram from diagrams.generic.compute import Compute from diagrams.generic.storage import Storage with Diagram("我的第一张图", show=False): app = Compute("业务服务") db = Storage("数据存储") app >> db运行命令:
python first_diagram.py执行完成后,当前目录下会生成一个my_first_diagram.png文件(文件名由 Diagram 的名字自动决定)。预期结果是:上方一个服务节点,下方一个存储节点,中间一条带箭头的线。
show=False表示不自动打开图片预览。如果你在本地开发想看效果,可以改成show=True,程序跑完会直接弹出渲染结果。到这一步,整个链路就算打通了。
3. 三个可以直接抄的实战模板:从单服务到跨可用区架构
3.1 单应用加数据库的最小架构骨架
先从一个最基本的后端服务架构说起。这个架构里只有一个应用服务和一个数据库,但我会把它写得稍微"专业"一点,把客户端、防火墙、缓存都加上,方便你看懂代码习惯。
from diagrams import Diagram, Edge from diagrams.generic.compute import Compute from diagrams.generic.database import Database from diagrams.generic.network import Network with Diagram("单应用架构", direction="LR", show=False): client = Network("客户端") cache = Compute("缓存服务") cdn = Network("CDN") # 用网络节点作为映射 app = Compute("应用服务") db = Database("主数据库") client >> Edge(label="HTTPS") >> cdn client >> Edge(label="API") >> app app >> Edge(label="读写") >> db app >> Edge(label="热点") >> cache这里我把 CDN 放在客户端和应用之前,表示用户流量先经 CDN 再到达后端。Edge 标签是我非常喜欢的功能,它能让线的语义更清晰,比单纯一条线高到不知道哪里去了。
为什么我要推荐从单应用开始练手?因为代码画图的学习路径里,最小骨架跑通比追求复杂重要。你只有先熟悉 Node 怎么建、Edge 怎么连、方向怎么调,后续加 Cluster、加样式时才有信心。
3.2 带负载均衡与多副本的内部分层架构
单应用的架构太简单,实际项目里更常见的是"多副本 + 负载均衡 + 分层"的形态。下面这段代码演示了三个关键能力:集群(Cluster 分组)、多副本(循环创建多个 Node)、负载均衡器与节点组的连接。
from diagrams import Diagram, Cluster from diagrams.generic.network import Network, LB from diagrams.generic.compute import Compute from diagrams.generic.database import Database with Diagram("分层架构", direction="TB", show=False): user = Network("用户入口") lb = LB("负载均衡") with Cluster("应用层"): apps = [Compute("应用副本 1"), Compute("应用副本 2")] with Cluster("数据层"): db = Database("主库") db_slave = Database("读副本") db >> db_slave # 主从同步 user >> lb >> apps apps[0] >> db apps[1] >> db这段代码有两点值得解释。第一,apps是一个节点列表,lb >> apps表示负载均衡器指向列表里的所有节点,一次就能连多条线。第二,我在 Cluster 内部写了db >> db_slave,这个连接会生成在集群框内部,视觉上非常自然地表达了主从复制关系。
为什么这个结构实用?因为它对应了业务系统中非常典型的"接入层-应用层-数据层"三明治模型。你可以通过改循环里的数量来模拟横向扩容后的图变化,这是代码画图天生擅长的事。
3.3 同城双活多可用区拓扑:Cluster 嵌套的边界把握
最体现 Cluster 嵌套能力的是跨可用区架构。假设你的系统部署在两个不同的可用区,每个可用区内部又分为接入层、应用层和数据层。这种情况下,外层 Cluster 代表可用区,内层 Cluster 代表技术层次,嵌套关系要清晰。
from diagrams import Diagram, Cluster from diagrams.generic.compute import Compute from diagrams.generic.database import Database from diagrams.generic.network import Network with Diagram("双可用区架构", direction="LR", show=False): client = Network("外部客户端") with Cluster("可用区一"): with Cluster("接入层"): lb1 = Network("负载均衡 1") with Cluster("应用层"): app1 = Compute("应用服务 1") with Cluster("数据层"): db1 = Database("数据库主节点") lb1 >> app1 >> db1 with Cluster("可用区二"): with Cluster("接入层"): lb2 = Network("负载均衡 2") with Cluster("应用层"): app2 = Compute("应用服务 2") with Cluster("数据层"): db2 = Database("数据库备节点") lb2 >> app2 >> db2 client >> lb1 client >> lb2 db1 >> db2这里我想特别提醒一个实践心得:Cluster 嵌套别超过三层。从可用区到技术层已经是两层,再往下继续嵌套"机房机架"之类的层级,图的视觉复杂度会指数级上升,自动排版很可能会混乱。如果你发现某个领域确实需要更细的层级,建议把局部细节单独画一张子图,而不是全部堆在一张总图上。
4. 真实项目里最常踩的五个坑,以及我当时的排查过程
4.1 渲染失败:graphviz 路径缺失,看图还分不清是哪种错
我最初遇到的问题是"Executable not found: dot"。直译就是找不到 dot 引擎。这个问题最常见的原因就是 graphviz 没装或者不在 PATH 里。
我的排查流程是:先确认系统里有没有安装。Linux 下用which dot,macOS 下同样。Windows 下则在命令行输入where dot。如果找不到,就重新安装或者手动把安装目录加入 PATH。装好之后在 Python 交互环境里检查:
import graphviz graphviz.__version__如果版本输出正常,再跑 Diagrams 脚本。这套排查链路我写进过团队的知识库,因为真的每隔一段时间就会有人遇到。
4.2 布局不理想:线的方向完全反了,问题其实在 Edge 语义
有一回我画一张调用关系图,明明写的是"客户端调用应用",渲染出来却变成"应用调用客户端"。排查了半天,发现原因是我理解错了>>的方向语义。在代码里a >> b表达的是a 到 b 有一个指向边,也就是从 a 指向 b。如果写反了,那整个数据流向就反了。
但还有一种更隐蔽的情况:你和团队成员对"依赖"和"被依赖"的理解不一致。比如"用户模块依赖订单模块",有人会画成 用户 -> 订单,也有人画成 订单 -> 用户。这种语义分歧比代码写错更难排查。我的经验是:在画图之前,先定义清楚"箭头表示数据流向还是依赖方向"。最好写成注释放在脚本顶部,让每个人看图之前先看规则。
4.3 节点太密:当自动排版开始"崩坏"时的应对策略
当一张图的节点超过三四十个,自动排版就会出现明显问题:边交叉严重、某些节点被挤到角落、整个图的结构变模糊。第一个直觉是去调布局算法参数,我试过,但收益有限。真正有效的做法是分而治之。
我当时画一个微服务架构总览,四十多个服务节点全塞在一张图里,最后的效果惨不忍睹。我停下来重新划分边界:总览图只出现八个核心服务,每个核心服务再单独用 Cluster 展开内部的子模块,并用不同文件渲染局部图。这个改变让总览图恢复了可读性,局部图又能保留细节。代码画图的优势在这里最明显:你可以把图的生成代码做成模块,复用同一个节点定义,总览图和局部图共用一组对象。
4.4 中文字体与样式定制:为什么你渲染的中文变成方块
默认情况下,graphviz 使用的字体对中文支持不好,渲染出来的中文经常是方块。解决办法要么改全局字体,要么在节点里指定中文字体。我自己验证过 macOS 和 Linux 两种环境,最稳定的方式是设置环境变量或者传入 fontname 参数。
from diagrams import Diagram from diagrams.generic.compute import Compute with Diagram("中文标题", show=False): node = Compute("业务服务") node.attr["fontname"] = "Microsoft YaHei"当然,你系统里得有对应的中文字体名称。Linux 服务器上如果没装中文字体,就算指定了名字也渲染不出来。建议在部署环境里提前执行fc-list | grep -i "microsoft\|noto\|wenquanyi"确认。这个问题不算难解决,但体验影响非常大,值得提前处理。
4.5 多文档与团队协作中的版本管理问题
代码画图解决了"可以 diff"的问题,但它也带来了新问题:如果一张架构图由多个脚本分别生成,怎么保证它们之间的口径一致?我见过团队里出现两张图,一张用旧图标集,一张用新图标集,色彩风格也不统一。
这个问题我在 diagram-design 项目里是用一个共享的主题配置文件解决的。把颜色、字体、节点默认样式集中定义在一个 Python 常量文件里,所有图表脚本都从这个文件引用。同时,所有的.py图表脚本和生成的.png图片都必须放进同一个仓库,确保任何时刻图表都对应上版本。
5. 把架构图从"一次性产物"变成"常驻资产"的进阶玩法
5.1 利用 CI 自动重绘:每次代码变更,架构图自动同步
一旦图表脚本进了 Git 仓库,"代码变更时图表应该同步变化"就成了自然的需求。我最喜欢的实践是在持续集成流程里加一个步骤:当检测到架构图脚本或相关配置文件变更时,自动运行渲染命令,输出新的图片,并把图片作为构建产物提供给文档站点。
这个流程看似简单,但其实能避免大量"文档过期"的问题。以前团队里经常出现架构图和实际代码对不上,大家各有各的理由,最终结果就是没人信任图。现在,只要代码合入,图会同步生成,并且每个版本的图都能从构建记录里找到。任何提升信任度的习惯,我都建议坚决落地。
5.2 从配置或代码直接生成图表:连画图的步骤都省了
还有一个进阶玩法:不要手工写节点和连线,而是把配置文件或者代码扫描结果当作数据源,自动生成图脚本。比如我维护过一个内部服务的清单文件,里面记录了每个服务依赖哪个数据库、调用哪个下游接口。我写了一个简单解析脚本,把这些关系转成 Diagrams 代码,然后渲染成架构总览图。
这么做最大的收益是架构图不再是"某个人手动维护的产物",而是现有真实数据的可视化"投影"。只要配置文件是正确的,图就一定是对的。不过要注意,自动生成往往需要先规划好数据结构和映射规则,初期投入比手写脚本高,适合节点数量多、关系规则明确的场景。
5.3 我把整个流程落地后的工作流,以及给刚开始实践者的建议
现在我的日常流程是这样:架构评审用图走代码画;图脚本和主题配置存在仓库里;任何架构变更先改配置或脚本,再讨论变更内容;CI 会自动渲染最新图;文档站点直接引用渲染产物的图片地址。
如果你准备在自己的项目里开始尝试,我建议第一步先别追求全面替换现有工具。找一个小的内部系统,画一张非核心架构图,跑通从写代码、渲染、提交仓库、在文档引用的完整链路。哪一步卡住了,再回来查这一篇的排查部分。跑通一两个真实场景之后,你自然能感受到"画图半小时改图五分钟"的快乐。
最后再分享一个小技巧:给 Diagram 的show参数写环境变量,本地开发时展示图片,CI 里关闭,省得每次跑脚本都弹窗口。这个细节虽然小,但对自动化流程的健壮性帮助很大。