1. 为什么我们总是把架构图画成"技术债"
1.1 视图永远只有一张:概念、逻辑、物理全糊在一起
先说一个我观察了很长时间的现象:很多人画架构图,一张图画到底。业务对象、服务边界、部署环境、数据流转、认证链路,全部塞进同一张画布里。这种"一图流"的做法看着省事,实际上是在给自己的项目埋雷。
为什么会这样?因为大多数人接触架构图的第一个场景,是汇报或者复盘。PPT上需要一张图,那就画一张。这张图既要有业务模块,又要有技术组件,还得标注交互关系。画的人心里清楚,这图是给人"看个大概"的,不是给人"照着做"的。可问题是,一旦这张图进入技术文档库,它就会变成后续所有人理解系统的唯一依据。新同学看这张图,会以为图中的每一个元素都处在同一个抽象层级——于是照着这张图去排查问题、去做容量评估、去写接口文档,每一步都可能跑偏。
我参与过多次系统交接,接手方拿着上一任留下的架构图,问得最多的三个问题是:这个模块到底是逻辑概念还是独立部署的服务?这条连线的含义是RPC调用还是数据依赖?这个框为什么画在那边,是不是物理机房的部署边界?当一张图需要靠"猜"来回答这些问题,这张图就不是文档,是技术债。
diagram-design这个名字,最初就是我为了对抗这种混乱而起的。它不是指某一种工具,而是一套"画图前先分层、分层后再表达"的设计方法。先明确这张图服务的对象是谁,再看它需要回答什么问题,最后才决定画成什么样子。
1.2 颜色即语义:大多数配色的潜台词是"随机"
另一个让架构图失真的大问题,是配色完全没有语义约束。我见过不少团队内部用的架构图,同一个颜色一会儿表示"已上线模块",一会儿表示"核心链路",图例里写的是A含义,图上实际表达的是B含义。用色的人自己清楚,但看图的人只能记住"蓝色好像是重要的"这种模糊印象。
颜色在架构图里应该承担的职责,是编码信息而不是装饰门面。一条链路里,哪些节点是本次改造涉及的范围,哪些是外部依赖不可触碰,哪些是异步消息的中间代理,这些如果只靠边框长短和文字大小来区分,信息传达效率极低。颜色是廉价的编码维度,但同时也是最容易被人滥用的维度。
我在diagram-design的实践里,给自己定了一条死规矩:一张图最多使用三种语义色,且每种颜色必须在图例中登记。色相用来区分"模块类别",饱和度用来区分"状态强弱",明度用来表达"层级深浅"。更多维度交给形状和纹理去表达,不靠无限堆颜色。这条规矩听起来保守,但执行下来以后,图的可读性提升非常明显,因为读者的注意力不用浪费在破译配色上。
1.3 为画图而画图:没有读者的图不值得存在
还有一类架构图,画出来以后没有人看。不是因为内容没用,而是因为图本身没有服务的具体问题。比如把系统的Web层、Service层、DAO层画一个三层结构,然后每层内部画一堆组件框,整个图占了屏幕四分之三,但你要问"这图能回答什么决策问题",回答不上来。
好的图是有读者的。给运维看的部署拓扑,要画清物理节点、网络分区、端口映射;给研发看的模块依赖,要画清接口边界、调用方向、幂等依赖;给产品看的业务链路,要画清角色动作、数据形态、异常分支。读者不同,图的结构重心完全不同。把三类读者塞进一张图,最后每个读者看到的都是一堆自己不关心的元素。
所以我在任何一次画图之前,都会先写一行"本图读者:xxx",然后写一行"本图要回答的问题:xxx"。如果这两个问题没法在两分钟内想清楚,这个画图需求本身就是假的,不如不画。这个习惯救过我很多次,也让我淘汰了大量无效的绘图需求。
2. 先定规矩再动手:一张好图的三层信息架构
2.1 概念视图、逻辑视图、物理视图如何分工
架构图界有一个流传很广的分法,来自软件架构的4+1视图理论:逻辑视图、开发视图、进程视图、物理视图,再加上场景视图。这套理论很多人上学时都学过,但工作以后一画图就忘。我个人的沉淀是把它压缩成三张图:概念视图、逻辑视图、物理视图。
概念视图回答"系统在业务上由哪些部分构成"。这张图里没有技术名词,只有业务域、角色、核心流程。它给产品经理和业务方看,帮助他们确认系统边界和职责划分。逻辑视图回答"软件层面模块之间如何协作"。这张图里有模块、接口、依赖方向、数据流,它给研发团队看,帮助他们理解代码组织方式。物理视图回答"系统部署在哪里、如何网络打通"。这张图里有主机、容器、网关、存储、网络策略,它给运维和SRE看,帮助他们做容量规划和故障排查。
三张图不是三个画法,而是三个抽象层次。很多团队只有一张图,本质上是把三个层次的信息强行压平了。diagram-design的核心理念,就是把"压平"重新"撑开"。画图之前先做主成分分析:哪些信息属于业务域抽象,哪些属于软件模块抽象,哪些属于基础设施抽象。分完类以后,再决定每个元素放在哪张图里。
实际执行时,我通常用"三层目录"来管理图集,而不是拼命在一张图里塞三个层次的内容。
2.2 从"一图流"到"目录化":用图集代替单图
一个稍具规模的系统,想用一张图画得又全又清,本身就是伪命题。人的工作记忆容量有限,一张图上的节点超过20个,读者就需要反复扫视才能建立整体认知。超过50个节点,绝大多数人的阅读体验就是迷路。既然一张图容纳不下全部信息,那就别硬画一张,画一套。
我管理架构图的方式,是建立一个图集目录。第一张是landing view,相当于整个系统的总索引,只画顶层业务域和它们之间的核心依赖,不展开任何内部细节。往下每一层对应一个业务域或者一个技术主题,画各自的内部结构。如果想看更细的模块级依赖,再进入下一层。每一层图之间用统一的ID关联,这样既能在全局视角定位问题,又能在局部视角追查细节。
这样做还有一个隐藏好处:图的维护代价降下来了。单体系统演进到微服务时,landing view需要更新的只有少量的域级连线,而不用动几十个子图。反过来,当某个子系统的内部结构变化时,也只需要替换对应层级的那一张图,其余部分不受影响。架构图最怕的就是"牵一发而动全身"——那不是图,那是迷宫。
从"一图流"到"目录化"的变化,表面上是画图习惯的改变,本质上是"建模思维"和"文档思维"的区别。文档思维追求一次写全,建模思维追求分治和抽象。diagram-design这个名字里的"design",我理解的不是视觉设计,而是这种结构化设计。
2.3 信息密度的护城河:边界、节点、连接线的取舍原则
画图久了,你会发现最难的其实不是画"有",而是画"无"。一坨信息全都堆上去并不难,难的是判断什么该省略。省略的标准是什么?我的标准是:如果删掉这个元素,读者仍然能理解当前视图的核心逻辑,那就删。如果删掉以后读者会问"这个模块去哪了",那这个元素在当前视图里就不属于该省略的范围,而是可以被折叠或弱化。
边界、节点、连接线,这是架构图的三类基本元素。边界表达归属和职责范围,节点表达实体和能力,连接线表达关系和依赖。每添加一个元素,都要问一次:它是在增加理解,还是在增加噪声。
我见过最典型的反面案例,是有人把一条链路上的超时时间、重试次数、熔断阈值全部标在连线上。先不说这张图有多拥挤,单从认知负担上讲,这种"参数级细节"应该出现在单独的配置说明文档里,而不是架构图里。图要回答的是关系和结构,不是参数。把参数塞进结构图,只会稀释读者对结构本身的注意力。
每一次取舍都要服务于一个目标:让读者用最短的时间建立正确的系统心智模型。我经常跟团队讲一句话:图不是数据库,不必忠实记录所有事实;图是心智模型的地图,重要的是帮人建立正确的认知路径。
3. 把DSL当作第一公民:我如何搭建diagram-design的绘图骨架
3.1 为什么放弃纯拖拽画布,改用文本化DSL
聊完方法论,来说说我的落地实践。拿diagram-design这个项目来说,我最初尝试过直接在白板工具上拖拽画图,也试过在通用绘图软件里精修样式。但很快就遇到了几个非常现实的问题:第一,拖拽画布产生的文件本质上是二进制或私有格式,不便纳入代码评审;第二,架构图会随系统演进频繁修改,拖拽环境中每次微调都要重新对准网格线,效率极低;第三,团队协作时,两个人同时改一张画布,合并冲突几乎没法处理。
于是我把方向转到了文本化DSL上:用一段结构化的文本来定义图的元素和关系,再通过解析器渲染成图形。这样架构图就从"画出来的产物"变成了"代码的产物",可以纳入版本管理、可以做diff、可以走审查流程。
用文本DSL画图并不是新鲜事,业界已经有不少成熟方案。但我没有直接照搬某个固定语法,而是按自己的需求定义了一套子集。原因在于,很多通用语法为了覆盖所有绘图场景,提供了海量配置项,但我的使用场景聚焦在架构图上,需要的是"少而明确"的表达能力。一个能覆盖所有场景的通用语法,也就意味着每一个具体场景都要做大量的裁剪和取舍,而裁剪后的规则往往只有自己记得住。
3.2 最小可用语法设计:nodes、groups、edges、layers
我设计的DSL核心只有五类声明:node定义节点,group定义分组边界,edge定义连接关系,layer定义层级顺序,style定义视觉样式。先看一个最小示例:
layer "业务域" 0 layer "应用层" 1 layer "基础设施" 2 group "订单域" { node "订单服务" placement "left" node "库存服务" placement "right" } group "数据层" { node "订单库" placement "bottom" node "库存库" placement "bottom" } edge "订单服务" -> "订单库" label "读写" edge "订单服务" -> "库存服务" label "RPC" style "dashed"这段文本表达的是:订单服务负责订单业务,通过RPC调用库存服务,两者分别读写自己的数据库。layer把图的纵轴划分为三个泳道,group把节点圈进对应的边界框,edge表达关系。渲染器拿到这段DSL之后,自动完成布局计算和样式渲染,我不用手动拖拽任何一个框。
在语法设计上,我刻意让表达式偏向"声明"而不是"命令"。声明式的好处是,图的内容和图的布局解耦。不管我调整格式还是换布局引擎,核心的关系数据是不变的。这也为后续做自动化对比和演进分析留了空间。比如我可以写一个脚本,比较两个版本的DSL之间有哪些edge发生了变化,从而自动总结出"本次架构改动涉及哪条链路"。这在拖拽画布的工作流里几乎不可能实现。
3.3 渲染管线的分层:解析、布局、着色、导出
diagram-design的渲染管线我分成了四个相对独立的阶段:解析、布局、着色、导出。这四个阶段各司其职,互不越界。
解析阶段负责把DSL文本转换成内存中的图模型。这个阶段只做语法检查和语义校验,比如节点ID是否重复、边的两端是否存在、分组是否有环。尽早暴露DSL的写法错误,能避免后面渲染阶段产生莫名其妙的视觉错乱。我在这个阶段花了不少功夫,把错误提示写得尽量贴近"人话"——曾经有同事反馈说解析器的报错比某些IDE还友好,我觉得这是很高的评价。
布局阶段负责计算每个节点的位置。架构图最常见的布局需求是分层、分组、对齐。我会根据layer的值决定纵向泳道,根据group决定横向聚集,然后在组内做差分收缩。这里有大量细节要处理,比如连线交叉最小化、节点重叠消除、标签防遮挡。为了不让这个阶段拖慢整体迭代速度,我默认使用自动布局,同时保留手动坐标的覆盖入口——自动布局兜底,手动微调兜精。
着色阶段负责把语义映射成视觉属性。我在前面提到三种语义色,这个阶段就是把节点类型、状态、层级映射到具体的颜色、边框、线条样式上。所有映射规则集中在一个样式配置里,而不是散落在DSL的每一行。这样换肤和换主题就变成了改一份配置文件的事。
导出阶段则负责输出不同媒介的渲染结果。设计文档里用SVG,有交互需求的时候用Canvas,PPT场景下导出PNG。这里我还踩过一个具体的坑:SVG导出时如果字体没有嵌入,到了别人电脑上字体渲染就乱了。后来我把导出流程固定为"先转路径再导出",这个坑才算彻底填平。
4. 从单一场景到多场景验证:diagram-design的边界在哪里
4.1 微服务调用关系图:最基础的"盒子+箭头"却最容易画错
微服务架构图是diagram-design最基础的应用场景,也是我拿来练手的第一个场景。按理说"盒子+箭头"是最简单的表达方式,但实际操作中很容易画错三个地方。
第一个是方向的语义。调用关系必须区分"发起方指向接收方"还是"依赖方指向被依赖方"。很多图喜欢画A依赖B,结果箭头从B指向A,看图的人还得自己绕一圈。我规定所有edge的方向统一为"请求发起方指向请求接收方",并且在图例里写明。第二个是连线的粗细与虚实。同步RPC用实线,异步消息用虚线,批量任务用双线,这个约束能让人一眼看出交互模式。第三个是避免把外部依赖画得跟内部模块一样重。外部依赖通常用浅色或弱轮廓表达,内部模块用饱和色,不然读者会误以为外部系统也归你管。
我在diagram-design里为微服务场景内置了几种常用模板:网关、服务、数据库、消息队列、外部依赖、定时任务。每种模板预设了对应的形状和配色,新项目接进来时只需要填节点名和关系,不用重新搭样式。
4.2 事件驱动架构:时序与并发的手工补偿
事件驱动架构比同步调用复杂得多,因为同步调用是"一问一答"的线性关系,而事件驱动天然带有异步、并发、因果和时间窗口。在一张静态图里表达"消息什么时候发出、什么时候处理完、失败的补偿路径是什么",远比表达"谁调用了谁"要难。
我用的办法是,利用layer泳道表达时间轴,把每个事件的处理过程拆成多个阶段,分布在连续的泳道里。每个节点用状态后缀表达生命周期状态,比如"订单已创建"、"支付已验证"、"库存已扣减"。这样,读者沿着泳道从左往右读,就等于沿着时间线看了一遍完整的流程。对于并发分支,我用多条edge从同一个节点出发,配上不同的label条件,表达"同一事件驱动了多个消费者"。
这套做法当然有局限:当出现大量的重试循环和分支合并时,静态图还是会变得复杂。遇到这种情况,我不会硬要在图里塞下所有时序细节,而是画一张主流程图,再把重试、死信、补偿等异常路径单独画成子图,用超链接关联起来。图的职责是导航,不是把异常路径也做成高密度迷宫。
4.3 基础设施拓扑图:节点之间的"距离"是有物理含义的
基础设施拓扑图和业务架构图是两个物种。业务架构图关注逻辑关系,拓扑图关注物理部署。同一个"订单服务",在业务图上是一个模块框,在拓扑图上可能对应十几个容器实例。抽象粒度不同,表达方式自然不同。
我刚开始画拓扑图时犯过一个错误:把所有节点按逻辑关系排列,导致网络连线乱七八糟。后来我意识到,拓扑图的第一责任是反映"物理近似":同一个机柜、同一个交换机、同一个可用区、同一个VPC,这些空间关系应该通过布局的距离和分组边界表现出来。读者应该能从图上一眼看出"哪些组件在同一个网络分区内,跨分区流量走的是什么链路"。
diagram-design在拓扑场景下,我会额外标注三类信息:端口号、协议类型、所属网络策略。端口信息直接写在edge标签上,协议用线型区分,网络策略则通过group的标题栏来表达。这样一来,基础架构评审会上很多关于"这个链路是否放通了""那台机器在哪个环境"的问题,都能直接指着图回答。
4.4 数据流与状态机:把"动态"在静态图里画明白
还有一类场景专门画系统的"动态"行为:数据如何流转、状态如何迁移。这类图之所以难画,是因为静态媒介本身不擅长表达变化。
数据流图我借鉴了流程图的画法:每个节点代表一个数据加工步骤,edge上标注数据形态的变化,比如"原始日志→解析后的JSON→聚合后的指标"。这样读者看到的不是一组孤立的处理单元,而是一条有始有终的流水线。
状态机图则处理另一类问题:一个实体在不同事件驱动下的状态变更。我在diagram-design里表达状态机时,用节点表达状态,用edge表达事件,并在edge上标注"事件(条件)"的格式。为了避免状态图变成"意大利面条",我要求所有状态必须先按"初始态、中间态、终态"分泳道,再画转移线。这个简单的分层约束,能让大多数状态机图保持清晰。
5. 进阶能力与坑:协作、版本化、动画与AI辅助
5.1 版本化:把图纳入Git后,diff的粒度怎么设计
把架构图纳入Git版本管理,这一步很多人都会做,但很少有人想清楚diff的粒度。如果每次DSL改动导致整个渲染输出都变了,review的人就只能看到"整张图重画了",等于白diff。
我的做法是:把语义层和渲染层分离后,diff分成两档。第一档是结构diff,比较节点和边的增删改,输出一份"哪些节点新增、哪些连线被移除、哪些方向的依赖发生了反转"的纯文本报告。报告走代码评审,可以精确到某个关系的变化是否合理。第二档是视觉diff,比较渲染出来的SVG文件,但只把前后差异的局部用高亮框标出来,而不是展示两张并排的全图。这样可以快速定位"原本对齐的组件是否发生了错位"这类纯视觉问题。
这套机制还有个附带收益:架构演进的历史记录变得可追溯。每当有人问"这个依赖是什么时候引入的""后来为什么删了那个模块",我都可以直接回溯那个时间点的DSL变更和对应的review记录。架构图不再是一个"当前快照",而是一套"有演进的版本档案"。
5.2 动画与交互:PPT讲解场景下的渐进式渲染
架构图在静态文档里服务阅读,在宣讲场景里服务讲解。同一个图,两种场景,要求完全不同。宣讲时,我不希望一上来就把整个系统的复杂度甩到听众脸上,而是希望先呈现全局骨架,再一层层钻入细节。这个需求在diagram-design里对应一个渐进式渲染模式。
渐进式渲染的实现思路很简单:每个节点和edge都打上一个"出现时机"的属性,渲染器按时间轴依次显示。讲解订单链路时,先渲染网关和订单服务,再渲染库存服务和支付回调,最后补充数据库和消息队列。画面上的元素是逐步增加的,听众的认知负荷也被分散到更小的时间片里。
交互方面,我做了两个实用的细节。第一个是hover高亮:鼠标悬停在一个节点上,与它直接相关的edge会高亮,其余的所有元素自动降低透明度。这相当于是"局部聚焦",读者不需要在大脑里手动过滤无关信息。第二个是点击钻取:点击一个group的标题,会跳转到该group对应的子图。这样从landing view一路点下去,就能从几十个节点的总览图一直钻到某个服务的内部依赖图,全程不用离开绘图环境。
5.3 AI辅助生成:从"手绘"到"人审"的工作流转移
AI辅助画图,现在也是绕不开的话题。我试过好几条路线,从自然语言直接生成DSL,到给定代码仓库自动抽取服务和依赖关系。效果有好有坏,但真正有价值的改变,是整个工作流的重心从"手绘"转移到了"人审"。
让AI先生成一张草稿,然后我来修正,远比从零开始拖拽画框高效得多。尤其当系统已经存在时,AI可以从代码里识别出服务名、接口路径、数据库表结构,生成一份粗糙但完整的候选图。我的任务就变成了:审查这份候选图,调整抽象层级,修正错误的关系方向,补充缺失的业务语义。换句话说,AI承担了"信息收集员"的工作,我承担"架构建模"的工作。
当然这里也有坑。AI生成的图往往在结构上很工整,但容易缺少"为什么要这样设计"的信息,因为它只能从已有代码里看到"现状是什么",看不到"设计意图是什么"。所以我不会把AI生成的图直接当作架构文档,而是把它当作一个素材库,再经过手工的语义标注和取舍之后,才会正式纳入图集目录。这也是diagram-design里"design"这个含义最完整的体现:生成只是开始,设计才是核心。
6. 一段收尾的想法:我在一线画图那些年的朴素心得
最后分享一点我自己的体会。画架构图这件事,看起来是"表达"问题,本质上是"理解"问题。你画不清楚,通常不是因为你没用对工具,而是因为你对系统的理解还不够结构化。反过来,当你真的想清楚了三层信息架构,想清楚了每张图的读者和问题,大部分的绘图细节都会自然地各归其位。
我至今保留了几个习惯。画图之前先写一行读者和一行问题,写完这两行才允许自己打开工具。每张图只保留三种语义色,任何新增颜色都必须在图例中登记。DSL一旦生效,就立即跑一次结构diff和视觉diff,绝不攒到一起。架构图的价值不在画的那一刻,而在后续每一次被人阅读、被系统演进验证的时候。一套好的架构图集,是可以陪着系统长大、长跑而不过时的。