1. 为什么大多数技术图表又乱又难懂:先解剖通病再谈设计
1.1 图的本质是降低认知成本,不是增加工作量
画图这件事,绝大多数人败在第一步:没想清楚这张图到底要讲什么。diagram-design 做到后面你会发现,它根本不是"怎么画得好看"的问题,而是"怎么让人十秒钟内看懂"的问题。图是一种信息压缩手段——把一段需要读三千字才能讲明白的关系,压缩成一个可以一眼扫完的空间结构。这个"压缩"做得好不好,直接决定了图的价值。
我见过很多架构图,密密麻麻全是框,每个框里还塞着几百字的功能描述,连线交叉得跟毛线团一样。读者拿到这种图的第一反应不是"明白了",而是"我该从哪看起"。问题就出在画图的人把自己当成了"摆放方块的人",而不是"设计信息路径的人"。
我自己踩过最大的坑,是喜欢往图里堆信息。觉得不多画几个模块、不多连几条线就体现不了系统的复杂度。后来在一次评审会上被同事问住:"你这条虚线到底代表调用还是异步消息?"我当时居然答不上来。那一刻我才意识到,一张图里的每一个元素都必须有明确语义,否则它就是噪声。好的技术图表,不是信息越多越好,而是让人在最短时间内建立起正确的心理模型。
1.2 "丑但不自知"的七种典型症状
我把这些年见过的"问题图"总结成了七种症状,你们可以对照一下自己画过的图:
- 把系统截图当架构图。直接把IDE里的工程目录、或者某个监控面板截个图贴上去,再加三个箭头。这图的唯一作用就是告诉别人"你不想画图"。工程目录是给机器看的,不是给人理解系统关系用的。
- 方框大小全凭手气。明明两个模块地位相同,一个框占了半个画布,另一个小得看不见。视觉体量一旦和逻辑权重不一致,读者会自动产生错误判断。
- 箭头含义混乱。实线、虚线、粗线、细线、带箭头、不带箭头,全在一个图里混用。问就是"实线表示调用,虚线表示依赖"——但图里没有图例,读者只能靠猜。
- 交叉线密如蛛网。节点摆放完全随缘,导致连线在画布中央结成团。人眼对交叉线非常敏感,一旦交叉超过一定密度,大脑会自动放弃解析。
- 配色像彩虹糖。每个模块一个颜色,颜色本身不携带任何语义。读者看完只能记住"花花绿绿",记不住系统边界在哪里。
- 图里放整段文字。一个节点里写了三四行需求描述,字体还缩到八号。图的优势在于关系和结构,不适合承载大段文字。文字超过二十个字,就该考虑拆分或者挪到图下方的注释里。
- 没有标题、没有图例。一张图脱离了上下文,读者既不知道这是哪个系统的图,也不知道各种形状和颜色的含义。这在技术文档里尤其致命,读者翻到图时往往已经跳过了前文。
这七种症状本质上是同一个病根:画图的人没有站在读者视角去设计信息的呈现顺序。
1.3 一张"好图"的四个判断维度
后来我给自己定了一个标准,画完任何一张图,先问四个问题:
- 相关性:图里每个元素都是必要的吗?删掉它会影响理解吗?我画架构图时经常删掉数据源的具体表名,只保留"数据库"这一个节点,让读者先建立高层认知。
- 清晰性:第一次看这张图的人,十秒内能复述出图的主题和主要关系吗?如果不能,说明视觉路径设计失败了。
- 一致性:同一张图里,同一种含义是否永远用同一种颜色、形状、线型?我的习惯是红色调一律代表告警或风险,绿色调一律代表正常或成功路径。
- 美感:留白够不够?间距是否均匀?对齐是否严格?美感不是装饰,它能大幅降低眼睛的扫描成本。
这四个维度可以当成一个自检清单。画图之前想一遍,画完之后再过一遍,出来的东西基本不会太差。
2. 代码化设计:把图表当成代码来写,而不是当成画来画
2.1 为什么我最终放弃了拖拽画图
早期我也用拖拽式工具画架构图,比如各种在线白板、桌面绘图软件。后来遇到三个问题,让我彻底转了方向。
第一个是版本管理问题。图形文件格式对人不可读,保存一个"架构图-final-v3"之后,过两天又出来一个"架构图-final-v3-真改版",最后根本不知道哪个是最新的。代码化图表本质是纯文本,可以直接扔进Git,和代码一起做Code Review,改动记录一清二楚。
第二个是复用和批量修改问题。拖拽式画图,画十个微服务的依赖图,得手动拖十组框和线。代码化之后,我只要写一个循环或者用变量替换,一个模板就能生成几十张同类图。微调一个节点样式,改一行全局配置就全部生效。
第三个是与文档系统的集成问题。我写的技术文档大多基于Markdown,拖拽式工具生成的图片得手动上传、手动维护,图一更新就要重新导图、传图、换链接。代码化图表可以直接在文档构建流程中自动渲染,图随文档走,文档更新时图也顺手更新了。
这三个问题直接决定了我的选择:把画图这件事当成一次小的软件开发来对待。有需求文档(想表达什么信息)、有设计稿(布局和配色)、有代码(dot语言或Python脚本)、有测试(打开渲染结果检查布局)、有持续集成(提交后自动导出图片)。
2.2 主流代码化图表工具怎么选
目前市面上主流的代码化图表工具,我基本都用过,这里给一个横向对比:
| 工具 | 语言风格 | 强项 | 弱项 | 适合场景 |
|---|---|---|---|---|
| Mermaid | 极简Markdown | 上手最快,和Markdown生态无缝集成 | 复杂布局控制弱,大型图会乱 | 快速流程图、时序图、甘特图 |
| Graphviz | DOT语言 | 布局算法强大,绘图精确可控 | 垂直布局需要技巧 | 架构图、依赖图、状态机、集群图 |
| PlantUML | 类Java描述 | 时序图、用例图非常专业 | 自定义样式偏复杂 | UML类图、时序图、活动图 |
| Diagrams(Python) | Python代码 | 用代码编程生成云架构图 | 需要Python环境 | 云原生架构、基础设施拓扑 |
我个人最常用的组合是Graphviz + DOT语言作为主力。原因很简单:它把"布局计算"这件最烦人的事交给算法,同时又保留了精确控制的接口。我只需要描述"哪些节点连到哪些节点、哪些节点应该属于哪个集群",至于每个节点放哪个坐标,不用我操心。对于复杂架构图来说,这就省掉了百分之八十的体力活。
Mermaid我也不是不用。写一个快速的流程图给同事看思路,Mermaid的体验是无敌的,三分钟出图,还不用编译。但一旦图稍微复杂,比如超过二十个节点、有分组和跨组连线,Mermaid的自动布局就开始放飞自我。所以我的惯例是:草图用Mermaid,正式图用Graphviz。
2.3 环境准备与最小可用示例
Graphviz的安装非常简单,各平台都有对应方式:
# macOS brew install graphviz # Ubuntu / Debian sudo apt-get install graphviz # Windows choco install graphviz装完之后写一个最简单的DOT文件:
digraph demo { rankdir=LR; A -> B; B -> C; }然后用一行命令编译:
dot -Tpng demo.dot -o demo.png如果你愿意,也可以直接输出SVG,后续编辑和放大都不失真:
dot -Tsvg demo.dot -o demo.svg从"能出图"到"图好看",中间差的就是后面几章的内容。但先把工具链打通,这是第一步。
3. 核心实操:用Graphviz画一张可维护的系统架构图
3.1 dot语言的核心概念,一次讲清楚
DOT语言上手其实比很多人想象中简单,核心概念就五个:
- digraph / graph:声明这是一张有向图还是无向图。架构图我百分之九十九都用有向图,因为依赖和调用天然有方向。
- 节点(node):用名称表示,例如
api_gateway。严格来说,节点不需要显式声明,只要出现在连线语句里就算存在。但为了设置样式,我通常会显式声明一批节点。 - 边(edge):用
->表示,例如api_gateway -> user_service。边也可以叠加 label 来说明关系,比如label="HTTP" - 子图(subgraph):以
subgraph cluster_xxx { }的形式定义。注意cluster_这个前缀很关键,带了它Graphviz才会把这个子图渲染成一个带边框的分组区域,否则只是一堆节点的逻辑分组。 - rank:同一层级的节点会被布局算法放在同一水平(或垂直)线上。"rank 相同"是实现对齐最常用的手段。
掌握了这五个概念,你已经能看懂绝大部分DOT文件了。真正需要花时间琢磨的,是"如何让布局结果符合你的预期",这部分在第四章细讲。
3.2 一张真实项目架构图的完整代码
这里我给一个我在实际项目中用过的简化版架构图。不要小看这个例子,它包含了主流的元素:外部用户、网关层、应用服务、数据层,以及服务之间的调用关系。我的思路是"分层 + 分组",让读者第一眼先感知到水平方向上的层次。
digraph system_arch { rankdir=LR; // 全局样式 node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=12]; edge [color="#5F6368", arrowhead=normal]; // 外部用户 subgraph cluster_client { label="客户端"; style=dashed; color="#9AA0A6"; client [label="移动端 / Web", fillcolor="#E8F0FE", color="#1A73E8"]; } // 接入层 subgraph cluster_gateway { label="接入层"; style="rounded,filled"; fillcolor="#F8F9FA"; color="#DADCE0"; gateway [label="API 网关", fillcolor="#E8F0FE", color="#1A73E8"]; } // 应用层 subgraph cluster_app { label="应用服务"; style="rounded,filled"; fillcolor="#F8F9FA"; color="#DADCE0"; user_svc [label="用户服务", fillcolor="#E6F4EA", color="#188038"]; order_svc [label="订单服务", fillcolor="#E6F4EA", color="#188038"]; pay_svc [label="支付服务", fillcolor="#E6F4EA", color="#188038"]; } // 数据层 subgraph cluster_data { label="数据层"; style="rounded,filled"; fillcolor="#F8F9FA"; color="#DADCE0"; mysql [label="MySQL 主库", shape=cylinder, fillcolor="#FFF8E7", color="#E37400"]; redis [label="Redis 缓存", shape=cylinder, fillcolor="#FFF8E7", color="#E37400"]; mq [label="消息队列", shape=cylinder, fillcolor="#FFF8E7", color="#E37400"]; } // 连接关系 client -> gateway [label="HTTPS"]; gateway -> user_svc [label="HTTP"]; gateway -> order_svc [label="HTTP"]; gateway -> pay_svc [label="HTTP"]; user_svc -> mysql [label="读写"]; order_svc -> mysql [label="读写"]; order_svc -> redis [label="读写"]; order_svc -> mq [label="投递"]; pay_svc -> mq [label="投递"]; pay_svc -> mysql [label="读写"]; }编译命令和前面一样:
dot -Tsvg system_arch.dot -o system_arch.svg打开渲染结果,你第一眼看到的就是横向五层分区:客户端、接入层、应用服务、数据层。每个节点都有明确的样式语义——蓝色的属于接入和入口,绿色的属于业务服务,橙黄色的属于数据存储。这就是我在1.3里说的一致性。
3.3 布局引擎怎么选:dot不是唯一选项
Graphviz内置了多个布局引擎,默认dot处理有向分层图最好,但其他引擎各有各的用处:
| 引擎 | 适用场景 | 实际体会 |
|---|---|---|
| dot | 有向图、分层结构 | 架构图和流程图首选,层级清晰 |
| neato | 无向图,弹簧模型 | 适合拓扑关系,节点会自动弹开 |
| fdp | 无向图,简化力导向 | 和neato类似,但更好看 |
| sfdp | 超大规模图 | 上千节点才会用到 |
| circo | 环形布局 | 适合协议交互回路展示 |
| twopi | 放射性布局 | 适合以某个中心点为根的依赖图 |
我的经验是:画技术架构图,90%的情况用dot就够了。如果用了dot之后发现交叉线太多,不要急着换引擎,先检查是不是子图划分和层级设计有问题。引擎只是背锅侠,问题通常出在图本身的"结构"上。
3.4 实战中我踩过的三个坑
这三个坑,我基本每次带新人都会讲一遍:
第一个坑:中文字体显示成方块。这个问题几乎人人碰到。根源不是Graphviz不支持中文,而是默认字体里没有中文字形。解决方案是在节点或全局设置里指定一个中文字体:
node [fontname="Microsoft YaHei"];Linux服务器上我一般用fontname="Noto Sans CJK SC",macOS上用fontname="PingFang SC"。字体名字填错了,Graphviz会静默回退默认字体,然后你又看到方块。
第二个坑:子图方向乱了。你以为subgraph cluster_app里的三个服务会水平排开,结果实际渲染出来垂直排了。这是因为整个图的rankdir=LR是水平方向,子图内部默认继承这个方向,但如果子图里的边没连好,布局算法就会自己乱排。解决办法是强制加一个不可见的骨架边,或者直接给子图里的节点加rank=same。后面第四章我会专门演示。
第三个坑:每次改动一点,重新生成的图布局"跳来跳去"。原因很简单,Graphviz的布局是基于能量最小化的,一个节点的位置变化可能带动全局重排。虽然这不是一个"错误",但在迭代对比时会很痛苦。我的经验是:先定好骨架和层级,再调整标签和样式。骨架一旦定了,后续改动就集中在属性上,布局基本稳定。
4. 层级、分组与布局:把信息密度变成清晰的视觉路径
4.1 用子图承载系统边界
很多人的架构图看起来"平",是因为所有节点都在同一个平面上,没有系统边界的概念。真实系统是有边界的:客户端在一层,网关在一层,服务在一层,数据在一层。如果不把这些边界画出来,读者就只能靠箭头方向脑补层次感。
子图(subgraph)就是用来承载边界的。回到3.2的代码,你会发现我用了四个subgraph cluster_xxx,每个都带着label标明这一层的名字。实际效果就是四块带边框的区域,每块区域里住着属于这个边界的节点。读者不需要你解释,自己就能领会"哪些服务属于应用层、哪些属于数据层"。
构建子图时有一个小细节:子图之间不要互相跨越。如果在应用层的子图里直接去连数据层的子图外部节点,Graphviz一般也能正确渲染,但风格上会破坏"区域"的感觉。我一般会把跨层访问的边放在最后统一声明,让连接关系在视觉上穿过各层的边界,而不是让某个子图本身跨界。
4.2 rank 与不可见边:把对齐变成代码
rank是Graphviz里最强的布局工具之一,它的作用是把某些节点强制放在同一水平线或垂直线上。比如我想让"用户服务""订单服务""支付服务"三个服务在视觉上绝对对齐,就可以在文件末尾加一句:
{ rank=same; user_svc; order_svc; pay_svc; }注意这里用了一个匿名子图结构{ rank=same; ... },它不是cluster,不会产生边框,只会告诉布局引擎"这三个节点的rank必须一样"。在rankdir=LR的图里,rank相同就意味着它们在同一垂直线上,整个应用服务层的边界就特别清晰。
rank还有一种高级玩法,叫不可见边。有时候你发现两个区域之间的"隐形逻辑"没有连线,但你又希望它们在空间上靠近或者错开。我经常用带style=invis的边来"撑开"布局:
user_svc -> order_svc [style=invis];这条边不会渲染出任何线条,但它会影响布局算法,让user_svc和order_svc保持相邻关系。遇到"我明明没连线,为什么这俩节点跑一起去了"之类的玄学问题,用不可见边手动干预是最快的解法。
4.3 一张反例到正例的改造流程
我拿一个具体的反例来说明改造流程。以前我画过一个订单系统依赖图,节点直接平铺,连了三十几条线,渲染出来交叉严重。后来我做了三步改造:
第一步,按职责划分层。把所有节点归到"接口层、领域层、基础设施层"三个子图里。这是宏观结构,先把整体骨架立住。
第二步,确定关键对齐路径。把所有节点之间最重要的那条调用链拎出来:controller -> service -> repository -> datasource。用rank=same把它们对齐,让读者第一眼就看到这条主线。剩余的非主线依赖,比如缓存、消息队列,放在主线两侧。
第三步,用不可见边做微调。主线对齐后,我发现服务A和服务B偶尔会跑到奇怪的位置,就加了四条style=invis的边,硬是让布局稳定下来。
改造完之后,交叉线从十几处降到三处以内,图的可读性完全不一样了。这三步不只是画图顺序,背后其实是个"先宏观后微观"的信息设计思路——和写代码时先分层再实现细节是一个道理。
5. 配色、字体与排版:图表的"气质"藏在细节里
5.1 为什么配色必须有语义,而不是为了"好看"
关于图表配色,我见过两种极端:一种是完全不管,全部黑色方框,整张图灰蒙蒙一片;另一种是每个节点一个颜色,把图画成儿童涂鸦。
正确的心态应该是:颜色是信息通道,不是装饰。如果有读者问"为什么这个节点是绿色",你能回答"因为它属于正常的业务服务",那这个颜色就用对了。如果他问"为什么这个节点是这个颜色"而你答不上来,那这个颜色就是噪声,不如删掉。
我这里给一套我自己用了很久的语义色方案:
| 语义 | 填充色 | 边框色 | 文字色 | 用途 |
|---|---|---|---|---|
| 主要入口 | #E8F0FE | #1A73E8 | #174EA6 | 网关、Controller |
| 业务服务 | #E6F4EA | #188038 | #0D652D | 正常业务模块 |
| 数据存储 | #FFF8E7 | #E37400 | #B06000 | 数据库、缓存、MQ |
| 外部依赖 | #F1F3F4 | #5F6368 | #3C4043 | 第三方系统 |
| 风险/告警 | #FCE8E6 | #D93025 | #A50E0E | 异常路径、降级开关 |
这套配色并不是我的原创,而是参考了主流设计系统的语义色,做了一点微调。饱和度不高,放在文档里不刺眼,也方便打印。你完全可以直接抄过去用。
5.2 字体、箭头、圆角、留白的统一规范
配色之外,四个细节决定了图的专业程度。
字体。一张图里最多出现两种字重:常规和加粗。标题用加粗14号,节点文字用常规12号,注释文字用灰色10号。不需要在单个节点里再强调某几个字,Graphviz也不方便做富文本。
箭头。箭头类型全图统一。我的默认配置是普通实线箭头arrowhead=normal,只有"异步消息"才用arrowhead=empty,"依赖"用style=dashed。这些语义要写进图例,或者至少在文档正文里交代一句。
圆角。所有节点统一用style="rounded,filled",让方框的圆角半径保持一致。不要有的节点圆形、有的节点方形、有的节点带阴影——每多一种形状,读者要多记一条规则。
留白。节点内部留白通过margin=0.2之类的参数控制,节点外部通过nodesep和ranksep控制。我常用的全局配置是:
graph [nodesep=0.4, ranksep=0.6, pad=0.2];这几个参数决定了节点之间、层级之间的间距。数值太小图会挤成一团,太大图会散成一篇。实际使用时,我一般先取一个初始值,看渲染效果再微调 0.1 的幅度。
5.3 一套可直接复用的全局模板
为了方便直接在项目里落地,我把上面的经验打包成了一个小模板。以后画任何图,先把这个头部贴上去,再填业务节点:
digraph common { rankdir=LR; graph [nodesep=0.4, ranksep=0.6, pad=0.2, fontname="Helvetica", fontsize=12]; node [shape=box, style="rounded,filled", margin=0.2, fontname="Helvetica", fontsize=12, color="#5F6368"]; edge [color="#5F6368", arrowhead=normal, fontname="Helvetica", fontsize=10]; // 把业务节点和连线写在这里 }这个模板看起来简单,但它把我在第五章列的所有规则都固化成了默认值。从全局模板开始画图,意味着你不会在画到一半时突然纠结"这个节点要圆角还是直角"——规则已经在最开始定好了,剩下的只是填充业务内容。
6. 从单张图到图表体系:diagram-design 的团队落地体会
6.1 沉淀模板库,而不是每次从零开始
单人画图,画完就完。但在团队里做 diagram-design,如果不沉淀模板,就会出现一个文档里十张图十种风格的情况。我的做法是建立一个diagrams目录,里面按类型放好模板:架构图模板、时序图模板、依赖图模板。每个模板都遵循第五章的配色和字体规范。
团队里的同事要画图,先复制模板,再修改业务部分,而不是从空白文件开始。这样做的好处是,经过几次迭代之后,所有图的视觉风格会自然收敛,读者看任何一张图都不需要重新学习编码规则。我墙裂建议:模板库本身就是团队技术资产的一部分,和代码库一样需要维护和评审。
6.2 图表与CI集成:SVG随文档自动更新
代码化图表最大的红利在持续集成。我有过一个项目,文档里嵌了二十多张架构图。以前用拖拽工具维护这些图的时候,每次架构调整都要手动重画、手动上传、手动替换链接,至少半天时间。换成Graphviz后,我在CI脚本里加了一步:
for f in diagrams/*.dot; do dot -Tsvg "$f" -o "docs/ diagrams/$(basename "${f%.dot}").svg" done每次提交代码,CI都会自动把所有.dot文件重新编译成.svg,再发布到文档站点。架构改了,只要顺手改了对应的.dot文件,文档里的图就自动更新。从此再也不会出现"架构图还停留在三个月前"的尴尬。
这一步对团队的收益最大。画图不再是文档编写阶段的一次性工作,而是和代码演进同步的日常任务。
6.3 团队审图清单:拉齐所有人的质量标准
我在团队里推行过一张审图清单,每次评审架构图时逐条过。这张清单只有六项,但每一项都能挡住大部分劣质图:
- [ ] 图有明确的标题和日期吗?脱离文档上下文能看懂吗?
- [ ] 所有节点和连线用的颜色/线型有图例或说明吗?
- [ ] 同一个系统边界内的节点是否用子图框起来了?
- [ ] 关键主线是否有层级/rank对齐?交叉线是否控制在三处以内?
- [ ] 节点内文字是否简洁?有没有超过二十个字的文本块?
- [ ] 渲染后是否检查过中文字体和导出高清版本?
这张清单我贴在团队文档里,每次画完图先自查一遍再发出来,审图效率高了很多。它不限制创意,只保证底线。
6.4 关于 diagram-design 的一点个人体会
说句实话,从"随便画一画"到"把画图当工程做",最大的变化不是图变好看了,而是想问题的思路变清晰了。每次动手画一张图,我都要先回答三个问题:这张图主要讲什么?谁是读者?他们需要从这里带走什么结论?
一旦这三个问题想清楚,工具、配色、布局都是水到渠成的事。Graphviz只是把"想清楚"的结果忠实地渲染出来而已。
最后分享一个我在实际项目中养成的习惯:每张正式发布的图,我都会用dot -Tsvg导出一个高清版本,同时在代码注释里写明这张图的维护者。这样即使半年后某张图出了问题,也能找到责任人快速修复,而不是让一张过期的图在文档里躺到天荒地老。diagram-design 不是一个画图技巧,而是一条把信息讲清楚的路,你走得越远,回头越觉得值得。