
做技术方案最烦的一件事就是画架构图。业务那边催着要评审文档开发这边代码一天改八遍图也跟着改八遍等你终于把架构图调好下个迭代一来整个拓扑又变了。时间一长图就成了摆设文档过期比牛奶还快。这个项目“diagram-design”就是冲着这个痛点去的把架构图、流程图、拓扑图的“绘制”变成“编写”用一套代码和配置来驱动图表生成让图和源码保持同步改架构像改代码一样简单还能走 review、走版本管理彻底告别手动拖拽和反复截图。这篇文章我会从方案选型、核心实现、真实场景到排坑心得完整拆解这个项目适合后端工程师、DevOps、架构设计岗以及任何一个被“文档维护”折磨过的人。1. 整体设计思路与方案选型1.1 为什么不做拖拽工具非要“代码画图”先说需求核心diagram-design定位是“Diagram as Code”图表即代码。拖拽工具比如各种在线白板、桌面绘图软件确实上手快但有两个致命伤一是不可追溯你没法对一张 PNG 做代码评审也没法轻松看出两张图之间改了什么二是不可复用下一个项目画类似的拓扑你还得重新拖一遍耗时且容易走样。换成代码驱动之后图变成了一种文本资产。可以放进 Git 仓库每次修改都有 diff可以绑到 CI 里自动渲染预览可以对数据进行批量生成——从云环境拉一批实例清单脚本一跑几十个节点的拓扑图就出来了手动画要一个下午代码画图只需要几秒钟。这种“生成式”的优势在节点多、变化频繁的场景里尤其明显。1.2 核心候选方案对比Graphviz、Mermaid、PlantUMLdiagram-design 的第一项技术决策就是选底层渲染引擎。当时圈子里主流的有三套Graphviz、Mermaid、PlantUML。我挨个试过简单列下感受方案布局能力样式控制适用范围上手成本Graphviz极强多引擎可选dot/neato/fdp精细到节点边框、字体、端口、集群架构图、拓扑图、状态机、依赖关系需理解 DOT 语法学习曲线略陡Mermaid一般适合常规流程图和时序图中等主题定制有限Web 文档、Markdown 内嵌很低几乎零学习成本PlantUML偏 UML时序图效果好中等UML 类图、时序图很低语法结构化最终选 Graphviz原因是 diagram-design 的核心场景偏向“系统架构图”和“链路拓扑”这类图的重点是大规模节点下的自动布局。Graphviz 的图布局引擎最强几十上百个节点它依然能排出相对清晰的层次而且它通过 Python 的 graphviz 库绑定之后可以非常方便地与数据源、脚本结合属于“既能画得复杂也能自动化”的那类工具。1.3 分层设计基础层、生成层、定制层整个项目我没有把逻辑塞在一个脚本里而是分了三层基础层围绕 Graphviz 的 Python 绑定做封装统一节点样式、颜色、字体、输出格式避免每次调用都重复写一堆 styling。生成层负责把结构化的数据元数据、配置、JSON、YAML转换成图谱对象。比如你给我一份“服务名、调用方、被调用方、协议、环境”的表我直接生成一张微服务调用图。定制层暴露 template 和 hook 机制允许对某些特殊节点加图标、加边框、分区展示保持灵活性。分层的好处是“大多数人只改生成层的数据就能干活”而不用碰到底层渲染细节。这个设计思路直接决定了整个项目实施过程各方打交道的成本也让 diagram-design 从“一个脚本”扩展成“一套可维护的工具”。2. 核心细节解析与实操要点2.1 环境搭建与依赖安装先搭基础环境。我当时的操作系统是 LinuxmacOS 和 Windows 也支持流程基本一致# 安装 graphviz 二进制 sudo apt update sudo apt install -y graphviz # 安装 python 绑定 pip install graphviz装完之后先跑一条命令确认没有问题dot -VGraphviz 的核心是一个命令行工具集python 绑定只是帮你生成 dot 文本再调用这条命令所以二进制必须装好。常见入门者会漏装二进制只装 pip 包结果一运行就报“Executable not found”这个问题我后面专门排查过遇到概率非常高。2.2 DOT 语法与核心概念DOT 是 Graphviz 的领域描述语言。简单说它描述的是图的对象节点、边、子图。最容易被忽略但无比重要的是三个概念rank、cluster、port。rank 控制垂直方向的层级。rankdirLR 可以让图从左往右延伸ranksame 可以让几个节点排在同一水平线。cluster 是视觉上的“组”可以实现“微服务内部一组数据库一组外部服务一组”的分区效果。cluster 子图的命名必须以 cluster 开头才能被识别。port 是节点的“端口”用于精确控制连线的出入口位置比如 A 的出口和 B 的入口分别打在哪一侧避免交叉线。这三个概念直接决定一张架构图是“能看”还是“看得舒服”。很多人用 Graphviz 画出的第一张图很乱不是引擎不好是没理解 rank 和 cluster 的用法。2.3 样式定制的关键参数Graphviz 的默认样式是学术味很浓的黑白图放在技术文档里稍显生硬所以 diagram-design 里我自定义了一套视觉参数。最重要的几个bgcolortransparent让图整体透明方便嵌入各类文档背景。node 的 shapebox、stylerounded,filled、fixedsizetrue 可以让节点变成精致圆角卡片。fontname 必须设置尤其是处理中文标签时不设置字体很容易变成方块乱码。splinesortho 让边变成正交折线适合架构图里那类“从左侧进、从右侧出”的连线视觉效果整齐很多。还有一个非常实用但容易漏的参数dpi。网页里展示的图一般 96dpi 就够但如果要拿去打印或者放进 PPT需要提高 DPI 保证放大不糊。直接在渲染参数里调dpi300即可不需要手动用 PS 放大。3. 实操过程与核心环节实现3.1 模型层的接口设计先定义生成图时需要的数据模型。我建议不要直接把杂乱的 dict 给渲染层而是定义两个基础数据结构Node 和 Edge。看起来好像多写了几个类但后面扩展属性和改样式时省很多事。from graphviz import Digraph from typing import List, Optional class DiagramNode: def __init__(self, name: str, label: str, shape: str box, style: str rounded,filled, fillcolor: str #E6F0FA): self.name name self.label label self.shape shape self.style style self.fillcolor fillcolor class DiagramEdge: def __init__(self, tail: str, head: str, label: Optional[str] None, style: str solid, color: str #333333): self.tail tail self.head head self.label label self.style style self.color color设计要点是默认值要“好看且通用”。shape 默认给 rounded boxfillcolor 默认给浅蓝这样即使调用方什么样式都不写渲染出来也不丑。Edge 的 label 用 Optional[str]因为不是每条边都需要说明空标签时不显示文字避免图上挂一堆多余信息。3.2 渲染引擎封装这一步是把 graphviz 对象再包一层让调用方完全不感知 DOT 语法。封装时记得把图方向、节点默认样式、边默认样式都集中处理hack 技巧在这里class DiagramRenderer: def __init__(self, rankdir: str LR): self.graph Digraph(commentdiagram-design) self.graph.attr(rankdirrankdir) self.graph.attr(bgcolortransparent) self.graph.attr(dpi300) self.graph.attr(splinesortho) self.graph.node_attr.update(shapebox, stylerounded,filled, fontnameArial, fillcolor#E6F0FA) self.graph.edge_attr.update(fontnameArial, fontsize10)单独抽一个 renderer 的收益在后续会慢慢显现如果你有 50 张图要统一改配色只需改这里不用逐张图去调参数。样式统一、更换成本低这在“文档维护”项目里是决定生死的特性。然后实现 add_node 和 add_edge 两个方法负责往 graph 对象里填充def add_node(self, node: DiagramNode): self.graph.node(node.name, labelnode.label, shapenode.shape, stylenode.style, fillcolornode.fillcolor) def add_edge(self, edge: DiagramEdge): self.graph.edge(edge.tail, edge.head, labeledge.label or , styleedge.style, coloredge.color) def render_to_file(self, output_path: str): self.graph.render(output_path, formatpng, cleanupTrue)注意 render_to_file 里我传了 cleanupTrue。这个参数的作用是渲染完成后自动删除中间生成的 .dot 源文件。第一次跑的时候我还不清楚这个参数后来发现工作目录里散落着大量编译缓存文件全是这类中间产物设置 cleanup 后干净多了。3.3 一个完整可运行的架构图示例用上面这些基础组件就可以搭建一个微服务调用的完整示例。模拟项目X 有三部分网关、业务服务、数据层。我现在用 diagram-design 生成它的总体架构图from graphviz import Digraph # 构建一个有向图rankdir 控制方向 dot Digraph(commentArchitecture Overview) dot.attr(rankdirLR) # 全局默认样式 dot.attr(bgcolorwhite) dot.node_attr.update(shapebox, stylerounded,filled, fillcolor#E6F0FA, fontnameArial) dot.edge_attr.update(fontnameArial) # 核心节点 dot.node(gateway, API Gateway) dot.node(order_svc, Order Service) dot.node(pay_svc, Payment Service) dot.node(auth_svc, Auth Service) dot.node(order_db, Order DB\n(PostgreSQL)) dot.node(pay_db, Payment DB\n(PostgreSQL)) dot.node(cache, Redis Cache) # 边 dot.edge(gateway, auth_svc, labelJWT 校验) dot.edge(gateway, order_svc, labelPOST /orders) dot.edge(gateway, pay_svc, labelPOST /payments) dot.edge(order_svc, order_db, labelSQL) dot.edge(pay_svc, pay_db, labelSQL) dot.edge(order_svc, cache, labelGET /SET) dot.edge(pay_svc, cache, labelGET /SET) dot.render(example_architecture, formatpng, cleanupTrue) print(generated example_architecture.png)这段代码跑完当前目录下会得到一张 example_architecture.png。七个节点和七条边的关系清晰可读已经能达到项目评审文档的标准。如果想插入到某个 Markdown 文档直接把图片路径引入即可而且因为用的是确定的 renderer每次跑出来的图视觉风格都一样批量文档拼接出来观感非常统一。3.4 集群分组实现cluster 用法单层的节点图只能表达“服务与关系”表达不了“系统边界”。架构评审时大家关心的是哪些服务属于同一个子系统哪些依赖属于外部这种信息需要用 cluster 来分组解决。继续扩展刚才的例子把“核心业务域”和“基础设施域”分开with dot.subgraph(namecluster_core) as c: c.attr(stylerounded,filled, fillcolor#F0F8F0, label核心业务域, fontnameArial) c.node(order_svc, Order Service) c.node(pay_svc, Payment Service) with dot.subgraph(namecluster_infra) as c: c.attr(stylerounded,filled, fillcolor#F8F0F0, label基础设施域, fontnameArial) c.node(order_db, Order DB) c.node(pay_db, Payment DB) c.node(cache, Redis Cache)这里有个非常关键的命名规则subgraph 的 name 参数必须以 cluster_ 开头否则 Graphviz 不会把它当成分区视觉上就是普通节点。相当多第一次用 Graphviz 的开发者在这里踩坑图怎么画都不分组最后发现只是命名不对。设置 fillcolor 加 style 后分组区块会有淡色底纹一眼看得出边界。3.5 批量生成与自动化管线diagram-design 最后能坚持下来靠的是批量能力。实际使用中很少只画一张图往往是几十个服务在一起的完整拓扑。手写每张节点和边不现实于是我写了一个 JSON schema 的数据源映射工具已知服务清单后自动生成图并导出缩略图import json def generate_from_json(path: str): with open(path, r, encodingutf-8) as fp: data json.load(fp) dot Digraph(commentTopology from JSON) dot.attr(rankdirTB) dot.attr(bgcolorwhite) dot.node_attr.update(shapebox, stylerounded,filled, fillcolor#E6F0FA, fontnameArial) for s in data[services]: dot.node(s[name], s[label], fillcolors.get(color, #E6F0FA)) for e in data[edges]: dot.edge(e[from], e[to], labele.get(label, )) out_name data.get(output_name, generated_topology) dot.render(out_name, formatpng, cleanupTrue) print(generate done:, out_name .png) data_example { services: [ {name: web, label: Web Frontend, color: #E0F7FA}, {name: api, label: API Service, color: #E0F0FF}, {name: worker, label: Worker, color: #FFF3E0} ], edges: [ {from: web, to: api, label: HTTPS}, {from: api, to: worker, label: Queue} ], output_name: auto_topology } with open(topology.json, w, encodingutf-8) as fp: json.dump(data_example, fp, ensure_asciiFalse, indent2) generate_from_json(topology.json)这段代码把“图和数据的耦合”彻底解开了。日常维护只改 JSON不改 Python。如果服务数量暴涨还可以从配置中心拉数据直接灌进这个 JSON 再渲染实现拓扑图每天自动更新。真实架构演进最怕什么最怕文档更新不及时。自动生成管线直接把“人为更新”这件事去掉了。4. 常见问题与排查技巧实录4.1 中文字体显示为方块Graphviz 默认字体不包含中文字形不配置 fontname 的话标签全是方块。解决办法分两步第一步系统里装好中文字体Linux 下确认 fonts-noto-cjk 已安装第二步在 DOT 里给 node_attr 和 graph 都指定 fontname。比较典型的配置dot.attr(fontnameNoto Sans CJK SC) dot.node_attr.update(fontnameNoto Sans CJK SC) dot.edge_attr.update(fontnameNoto Sans CJK SC)三个地方都要设置不能只设置 node_attr。graph 属性控制的是 cluster 标签和标题字体edge 控制连线标签漏掉哪个哪个就乱码。4.2 节点过多导致布局拥挤画依赖关系图时一旦超过 30 个节点默认引擎输出往往变成一团毛线。我的经验有三条路线先加 cluster 做分组让图按系统域分块再把非核心节点聚合掉比如把三个缓存节点合并成一个“Cache Cluster”节点最后调整 rankdir节点层次多时用 TB从上到下链路长时用 LR从左到右试几次选观感最合适的。还有一个低成本的必杀技调overlapfalse和sep20。前者强制节点不重叠后者控制节点间最小间距很多时候只是这两个参数就能让图从“没法看”变成“还行”。4.3 连线交叉严重怎么都排不干净Graphviz 的自动布局倾向于优化边的长度但不保证完全不交叉。如果交叉严重先检查是否所有连接都“必须”存在再考虑用splinesortho让所有边折行视觉上会整齐很多。最后利用 port 机制给节点指定出口和入口例如dot.node(a, A) dot.node(b, B) dot.node(c, C) dot.edge(a:e, b:w, label1) dot.edge(a:e, c:w, label2)a:e 的意思是节点 A 的东侧出边b:w 是节点 B 的西侧入边。这样控制后A 向 B 和 C 的连线从同侧出发图上交叉概率大幅下降也符合人工绘图习惯。4.4 中文文档导出 PNG 后不清晰架构图如果要贴进 Word 或者打印屏幕显示的 96dpi 完全不够文字边缘会虚。正确做法是渲染时调高 dpidot.attr(dpi300)同样一张图300dpi 输出体积会大一些但无论放大还是缩小观感都更专业。尤其评审文档打印成 PDF文字依然锐利。4.5 直连边重叠在同一个节点上多个边同时指向一个节点时如果都从同一个方向进来入口处会糊成一团。建议在边上加不同的端口位置让一个从北侧进、一个从南侧进避免全挤在一起。配合 color 区分不同协议也会进一步降低识别成本。4.6 render 时报 Executable not found这是最常见的入门级报错本质是图库二进制没有安装。pip 装的 graphviz 只是 Python 绑定真正干活的是系统里的 dot 命令。遇到这种报错先跑dot -V如果提示 command not found就回到 2.1 安装 graphviz 二进制。macOS 上用brew install graphvizWindows 需要去官网下载安装包并加入 PATH。很多时候明明“装过”重启终端之后 PATH 还是没加载需要确认安装路径已加入环境变量。5. 实战项目模拟某跨平台系统的自动化拓扑生成5.1 场景设定模拟项目X一个跨平台系统线上环境有 20 个服务节点、一个网关、三种存储方案、两条外部依赖链路。传统手动维护拓扑图方式每周至少消耗半天且版本之间差异难以追踪。diagram-design 在这个项目中的目标很简单从账号体系拉取服务列表后一键生成最新拓扑免人工绘图。5.2 数据准备与映射我先写了一段清单映射脚本把服务模型转成渲染器需要的节点和边services_data [ {name: gateway, label: 网关, color: #FDE9E9}, {name: auth, label: 认证服务, color: #E8F5E9}, {name: order, label: 订单服务, color: #E8F5E9}, {name: inventory, label: 库存服务, color: #E8F5E9}, ] edges_data [ {from: gateway, to: auth, label: REST}, {from: gateway, to: order, label: REST}, {from: order, to: inventory, label: 内部 RPC}, ]这里我用“接口模型映射源码”的思路生成这些 Python 字典保证服务和线上环境实时一致。接着调用封装好的 generate_from_list 函数def generate_from_list(services, edges, output_namesystem_topology): dot Digraph(commentSystem Topology) dot.attr(rankdirLR, bgcolorwhite) dot.node_attr.update(shapebox, stylerounded,filled, fontnameArial) for s in services: dot.node(s[name], s[label], fillcolors.get(color, #E6F0FA)) for e in edges: dot.edge(e[from], e[to], labele.get(label, )) dot.render(output_name, formatpng, cleanupTrue) generate_from_list(services_data, edges_data)整个操作过程耗时不超过 15 秒网络拓扑图就刷新了。相较于传统手段这个流程最大的优势是无论服务增至 50 还是 100我都不需要重新画图只改列表数据即可。5.3 与 CI 文档流程的集成后续我把这个生成步骤嵌进了文档构建流程。每次构建项目配置先构建数据映射再调用 generate_from_list最后把 PNG 发布到静态文档目录。这样团队每次看到的架构图都是当次构建的真实状态彻底杜绝了“图跟代码不一致”的争议。这个经验后来被我应用到多个项目里评价普遍不错。6. 体验总结与后续扩展方向6.1 对“图即代码”的落地体会diagram-design 最核心的价值不是画图本身而是把“图”这种产物拉进了软件工程的版本管理体系中。我在实际操作中的体会是这个方案敲开了一扇门它让我第一次看到“图也变得可评审、可追溯”的样子。大家评审代码时可以指出某个函数实现对不对现在评审架构图时也可以直接指出某条链路是否多余、某个依赖方向是否合理这是拖拽绘图工具无法提供的核心优势。6.2 值得尝试的扩展方向这套项目后续还有两个我觉得值得尝试的方向供参考动态数据源接入从服务注册中心直接拉实例列表生成监督大屏类的实时拓扑图。图会随着服务发现的变化自动刷新。导出更多格式目前默认输出 PNG但 Graphviz 底层支持 SVG、PDF、PostScript 等。SVG 特别适合嵌入 Web 端可以局部交互、动态高亮节点。多语言渲染已经跑通过用统一数据源同时输出中文和英文版架构图对外文档直接剥离翻译工序。图数据库联动把节点和边的关系同步到图数据库用于后续做链路追踪、故障根因定位。最终这项目沉淀下来的不是某张图而是一整套“把图当代码维护”的工作习惯。强烈建议还在被文档维护逼疯的人下一个迭代从 Diagram as Code 开始。