ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

系统模块拆解实战:从黑盒到清晰架构图

系统模块拆解实战:从黑盒到清晰架构图 做系统设计、重构或者技术交接的时候最怕的就是拿到一个“黑盒”。别人丢过来一套系统、一个产品模块你第一反应往往是这东西到底包含哪些部分各部分之间怎么协作改动一个点会不会牵连一大片我在做系统模块拆解这件事上踩过不少坑也总结了一套可以照抄的作业流程。这篇文章就围绕“第二阶段写拆解”展开系统说说怎么把一个黑盒拆成一张清清楚楚的结构图。可能有人会问拆解不就是画个架构图吗真这么简单就好了。模块拆解的本质是把一个复杂的系统按某种规则切分成边界清晰、职责单一、依赖明确的小单元然后再把这些小单元的相互关系、数据流转、异常处理全部落到纸面上。这个动作在系统改造、性能优化、团队扩容、甚至新人入职培训里都极其关键。无论你手里是一个运行多年的老系统还是一个刚立项的新平台拆解能力都是硬通货。这篇文章适合谁看适合刚接手新系统的后端开发、做系统梳理的技术负责人、准备做重构或迁移的架构师也适合产品经理想搞懂技术边界、项目经理想摸清工作量。我会按我实际做拆解的先后顺序来讲怎么理边界、怎么画视图、怎么写模块说明、怎么验证拆得对不对尽量把每一步都讲到可以直接复用的程度。1. 拆解之前先想明白模块边界到底怎么划1.1 拆解不是画图是回答四个问题很多人一上来就找画图工具先把网络拓扑、系统框图画出来这是本末倒置。模块拆解的核心目标是先回答几个更底层的问题这个系统具备哪些能力每个能力由谁负责这些能力之间是怎么通信的同步调用还是异步消息数据归属权在谁手里哪些数据是模块私有的哪些是全局共享的一个模块挂了影响范围是什么另一个模块会不会跟着遭殃我在给一个订单系统做拆解的时候最开始就拿着一张旧架构图开始看结果发现图上画了一个叫“数据中台”的框但谁也说不清这个框具体做什么。我只好去翻代码、查日志、问开发最后才搞清楚所谓数据中台实际上承担了订单数据同步、用户标签计算、报表统计三件完全不相关的事。这就是边界没划清楚导致的典型问题。所以在拆解时我给自己定了条规矩画图之前先用文字把问题回答清楚。宁可文字啰嗦一点也不能上来就用图形去掩盖认知盲区。1.2 高内聚、低耦合怎么落地到具体模块“高内聚、低耦合”这句话谁都会说但真到了划分边界的时候往往就变成拍脑袋。我总结出三个可以操作的标准第一模块内部的事务逻辑必须完整闭环。比如订单模块创建订单、查询订单、取消订单、订单状态变更这些事必须能在一个模块里说清楚不能一个订单状态变化要跨三个模块才能理解。第二模块对外暴露的接口要尽量少而稳定。如果一个模块对外有几十个接口首先要想的不是怎么拆小接口而是这个模块是不是承担了太多职责。第三模块之间只通过明确的契约通信不允许直接读别人的数据库表。我有一个挺直观的类比好的模块划分就像一栋写字楼里的公司。每家公司有自己独立的办公区有自己的门禁公司之间要通过前台预约才能见面沟通。而糟糕的模块划分就像开放式工位大家坐在一起看起来热闹但一个人打个喷嚏一排人都得跟着受影响。代码上体现出来就是服务之间绕过接口直接查数据库、共用同一张表、或者一个改动要同时发布好几个服务。1.3 拆解的粒度怎么才算刚刚好粒度是模块拆解里最让人纠结的问题。拆太粗一个模块几千行代码和没拆没区别拆太细一个模块就一个接口管理成本比开发成本还高运维想死的心都有。我个人的经验准则有两条一是看团队规模一个模块至少要能由一个团队在一个迭代周期内独立交付如果两周都交付不了说明拆得还不够二是看故障半径如果一个模块故障会导致核心链路全部瘫痪那这个模块就该继续拆直到故障影响范围可接受为止。举个实际例子。之前拆过一个支付相关的系统刚开始把所有支付方式微信、支付宝、银行卡都塞在一个模块里每次上新渠道改动面都特别大。后来我们把渠道适配层和核心交易引擎拆开交易引擎负责支付状态机和对账渠道适配层负责对接第三方接口。这样支付引擎稳定渠道适配可以各自迭代互不影响虽然多了一个模块部署运维但整体风险反而降下来了。2. 拆解前的信息收集先把自己的认知填满2.1 先建立全局视图再动手拆正式写拆解文档之前我建议先花一到三天时间做信息收集。因为你拆解的对象如果是老系统文档可能早就过时了如果是新系统可能在演进过程中已经偏离了当初的设想。全局视图这一步核心目的是回答三个问题系统上下游是谁依赖了哪些第三方被谁依赖核心链路是什么比如电商系统下单、支付、发货就是核心链路如果这条链路走不通拆解出来的其他模块做得再漂亮也没意义。哪些是主模块哪些是支撑模块主模块承担核心业务逻辑支撑模块提供通用能力比如消息推送、权限校验、日志埋点。这一步我习惯用最笨但也最有效的方法把代码仓库拉下来按服务目录列出清单再结合线上配置中心确认服务之间调了哪些接口。不用急着弄懂每行代码但要把服务清单、接口清单、依赖关系清单这三张表拉出来。2.2 梳理依赖关系区分强依赖和弱依赖不是所有依赖都值得同样级别的关注。我在拆解时会把依赖关系标注成强依赖与弱依赖两类。强依赖指上游挂了下游完全无法提供服务弱依赖指上游异常时下游可以用降级方案继续跑最多损失一些扩展功能。把这两类分清楚最大的价值是判断故障影响半径。比如用户模块挂了订单模块还能不能创建订单如果订单模块的“下单”动作强依赖用户模块做实名校验那用户模块故障订单链路就断如果只是在下单后异步给用户模块发一条消息那下单链路就不会被用户模块拖垮。我把这个结果整理成一张依赖矩阵放在拆解文档的最前面后面再细讲各个模块看的人脑子始终不会乱。2.3 摸清数据归属别让数据变成糊涂账模块拆解里最容易漏掉的是数据归属。很多系统的代码模块划得很清楚但数据库表共用得很随意订单模块要查用户信息直接去读用户库的表库存模块要判断订单状态直接去订单库里查。这种操作在短期看效率高长期看就是定时炸弹。拆解时我要求每个模块说明自己“拥有”哪些数据、哪些数据是复制过来读的、哪些数据是别人拥有但自己需要依赖的。一个简单易行的方法是把数据库里的核心表列出来标注它们的属主模块和消费模块。属主负责数据正确性消费方只能通过接口或明确的消息去读。一开始这么标的时候会发现很多表都是“小区公共花园”谁的都不是谁都敢改这种表通常就是要重点治理的隐患。3. 写拆解的落地姿势清单、模板与命名规范3.1 用一套维度化模板描述模块而不是写散文拆解文档最忌讳写成大段大段的描述性文字。二三十个模块每个模块写五百字合起来一万多字看起来很多但几乎没法检索也没法横向对比。我推荐的模块描述模板是“字段化”每个模块用一张表来说明字段说明示例模块名称统一命名团队内唯一订单服务模块职责一段话说清楚这个模块做什么负责订单创建、查询、状态流转与超时关闭核心功能点按功能粒度列出创建订单、取消订单、订单状态查询、超时自动关闭依赖模块运行时依赖哪些模块区分强弱强依赖用户服务弱依赖库存服务被谁依赖反向依赖被支付回调服务依赖数据归属拥有哪些表/数据订单主表、订单明细表、订单状态变更流水表对外接口核心接口清单/api/order/create、/api/order/query、/api/order/cancel关键流程内部核心链路说明下单状态机待支付→已支付→已发货→已完成取消待支付→已取消故障影响模块异常影响范围订单模块不可用则用户无法下单支付回调无法更新订单状态变更频率近半年平均改动频率平均每两周一个版本主要集中在渠道适配用字段化模板的好处一是写的人不容易漏信息二是读的人可以做到“只看一列”快速对比多个模块。我在团队里推行这套模板后新人上手老系统的速度明显快了一截因为他们终于不用从代码里猜业务边界了。3.2 模块命名规范命名混乱是拆解失败的信号模块命名的混乱程度往往和系统健康程度成正比。有的模块叫“核心交易”、有的叫“交易服务”、有的叫“trade-api”本质上说的是同一个东西开会的时候互相听不懂浪费时间不说还容易在排障时找错对象。我在拆解时会把命名规则一并定下来。规则很简单业务域 功能角色例如“order-api”“order-core”“inventory-worker”“user-query”。同步接口统一带 -api 后缀异步任务统一带 -worker 后缀核心逻辑统一带 -core 后缀。这样一看名字就能判断它属于哪一层、面向谁、承担什么角色。每次拆解文档做完我都会做一次命名对照核查把所有别名、历史遗留叫法列成一张对照表哪怕代码里没改文档里也必须用标准名。等到下一次重构时顺便把代码包名、服务名一起统一这个债迟早要还越早还损失越小。3.3 拆解文档的结构怎么排我一般把拆解文档分成五章顺序固定大家翻起来不费劲第一章是总体说明包括系统定位、全局架构图文字版或图版、拆解范围、涉及团队第二章是模块清单按核心模块到支撑模块排序每个模块用上面那张模板表描述第三章是依赖关系包括依赖矩阵和关键链路说明第四章是数据归属列出核心数据表、属主和消费方第五章是风险与待确认项记录还没搞清楚的边角信息。最容易被忽略的是第五章。拆解过程中总会碰到一些暂时无法确认的信息比如某个老接口是否有调用方、某张表的写入方到底是谁。千万不要把疑问憋着全部记到风险清单里后续再逐个求证。我在实际项目里很多影响判断的关键信息都是在风险清单里沉淀出来然后在后续阶段集中解决的。4. 三种好用的模块视图拆分结构、调用关系、状态流转4.1 结构视图看清模块的归属关系视图的核心价值是让人一眼抓住重点。我通常画三类视图。第一类是结构视图反映模块之间的层级和组织关系。拿电商系统举例从上到下可以分成用户触达层App、H5、小程序、业务服务层用户服务、商品服务、订单服务、支付服务、基础支撑层消息服务、文件服务、权限服务。这张图不用画得太细重点是让人知道系统分几层每层上有哪些模块。画出这张图之后拆解粒度是否合理就很容易看出来如果某一层放了十几个模块而另一层只有孤零零一个那大概率是划分不平衡需要进行合并或拆分。4.2 依赖视图看清调用方向与故障传导路径第二类是依赖视图重点表达“谁调谁”。我这里强调“方向”是因为依赖关系和调用关系的方向往往相反。从数据流看订单服务为了查询用户信息可能调用用户服务但从故障传导看用户服务故障会传导到订单服务。如果把这两个方向搞混排障的时候就会走很多弯路。画依赖视图我有个原则只画模块级别的依赖不画接口级别的依赖。因为模块太多了画到接口级别图基本没法看。接口细节放到模块说明表里由每个模块自己负责。依赖视图上我会用线条粗细表示调用频率用颜色或标签区分强依赖与弱依赖这样“核心链路在哪”“哪里最容易出事”一眼便知。4.3 状态视图看清模块内部生命周期第三类是状态视图用于表达模块内部的关键状态流转。不是所有模块都需要画状态图但核心业务模块必须有。订单模块至少要画清楚订单状态的合法流转路径已创建能不能直接跳到已完成已取消还能不能改成已支付如果拆解阶段不把这些状态机理清楚后面写代码的时候就会到处埋雷。我用文字加箭头的方式把状态流转列出来同时在旁边标注触发条件和异常分支。比如订单支付超时状态从“待支付”变为“已关闭”触发条件是“超过支付有效期且未收到支付回调”。异常分支则会额外记录支付回调与本地订单状态不一致时怎么处理是对账修正还是人工介入。这些细节才是模块拆解真正值钱的地方。5. 拆解完不能直接交差验证和沉淀5.1 自查清单用这七个问题印证拆解质量每次写完拆解文档初稿我会按下面的清单过一遍。如果有任何一条不过关就说明拆解工作还没做完每个模块能不能用一句话说清楚职责如果说不清说明模块划分有问题。模块之间的依赖能不能在图上闭环有没有循环依赖核心链路是否完整覆盖从用户发起请求到最终数据落库能不能讲清楚经过哪些模块数据归属是否明确每张核心表有没有确定属主有无隐藏的共享逻辑比如多个模块都去查同一个数据源只是实现不同异常和降级路径是否被覆盖不能只画主流程优雅降级、失败重试、补偿事务都要有交代。排查故障的人能否靠这份文档定位问题找一个不熟悉系统的同学让他根据文档回答“订单支付失败应该先看哪个模块”能快速给出答案才算合格。5.2 找三类人做交叉验证文档自己在写的时候容易陷入“已知视角”所以我会找三类人做交叉验证第一类是系统里待得最久的老员工他可能会告诉你“有个模块实际已经没人维护了”或者“某张表早就没有写入方了可以并入另一个模块”第二类是最近接手的新人他能告诉你文档哪里读不懂、哪里解释不清楚第三类是下游依赖方因为真正的边界问题往往出现在模块与模块的夹缝里依赖方对“交付物长什么样”最有发言权。在一次拆解订单域的项目中我本来按代码目录把“订单管理后台”划分成了一个模块但老员工说这里面有一部分数据来自人工导入而且导入逻辑和订单主链路没有任何关系。后来我把这部分数据单独拆成“人工辅助数据维护”模块文档才算是真正贴合实际。5.3 拆解过程中的“待确认”清单不要丢很多拆解文档最后会留一堆没法确认的问题但最怕的就是这些问题在文档里消失。我自己有一个“待确认清单”的固定格式记录问题描述、涉及模块、影响判断、找谁确认。最终可能有一部分问题短期无解但下一个接手的人至少知道这些坑存在不用重新踩一遍。在拆解文档的“风险与待确认项”里每一个条目我会同时写清楚它影响的是拆分粒度还是数据归属。比如“订单超时关闭任务目前部署在订单服务内部”这一条会影响后续是否把定时任务独立拆成一个 worker 的判断。这种信息看起来零碎其实价值极高。6. 从拆解到实践拆解结果必须能拿来用6.1 拆解结果直接用于排障一次实战中的验证有一次线上反馈“订单支付成功但页面还是待支付”排障的同事第一时间打开我整理的拆解文档按依赖视图锁定“支付回调 → 订单状态更新”这条链路再按模块说明表里的对外接口找到对应的回调接口十个字节如上的检查点逐步排查不到二十分钟就定位到是回调里幂等校验逻辑的问题。如果换作没有拆解文档的时候大概率是两三个后端围着一堆日志猜来猜去。这件事让我更深地意识到拆解文档不是坐在那儿写给别人看的摆设它是真的拿来救命的。所以现在我做拆解一定会要求排障相关的内容落得足够细接口名、错误码、日志关键字、配置项位置都要尽量写清楚。6.2 拆解结果驱动重构优先级拆解文档还能用来当重构的作战地图。因为你通过依赖视图和数据归属视图能一眼看出哪些地方是“改一处要动一片”的重灾区这些就是重构的首选对象。比如我在拆解一个商品系统时发现商品主数据的写入方竟然有五个模块其中两个模块只是在商品创建后顺手更新了某个字段。这种情况在拆解前很难被发现拆解后清清楚楚。我据此重新梳理了数据写入权限把字段更新收敛到拥有方另外四个模块改成消息订阅或接口调用改动风险大幅下降。没有模块拆解这轮重构基本就是盲人摸象。6.3 把拆解文档变成持续更新的“活文档”拆解最怕的是做完就完了代码一迭代文档迅速腐烂。我现在的习惯是把拆解文档放进团队的代码仓库和项目代码一同评审、一同发布。模块说明表有变化必须在同一个 merge request 里更新对应文档。交付标准就是“文档不更新代码不让合”。另外模块拆解不是一次性的。“第二阶段”之后还会有第三阶段、第四阶段随着系统演进原来看似合理的边界可能变得不合理当初清晰的模块可能开始腐化。我建议每半年做一次模块拆解的审视拉一下模块清单看有没有变了职责但名字没改的有没有已经空转但没人敢动的有没有数据归属已经漂移的。把拆解当成一件持续投入的事系统才能一直保持健康。7. 写在最后一点个人的实操体会模块拆解这件事看起来是个技术活实际上更考验细致和耐心。做得好的拆解能让团队在讨论问题时有了共同语言能让新人在几天内就理解跑了几年的系统能在故障发生时少烧几根头发。做得不好的拆解文档写完就进回收站下次重构依然抓瞎所谓系统梳理最后只是自我安慰。我个人这几年做下来最大的体会就是拆解的价值不在文档有多厚而在边界是否真的清楚、依赖是否真的画对、数据归属是否真的明确。与其把所有细节都堆在文档里不如先保证核心链路闭环再慢慢补充边角信息。一个读起来顺畅、能经得起追问的模块拆解文档远比一个看起来全面但注水的文档有用得多。最后再分享一个小技巧做完拆解把文档发给团队里一位完全没参与的系统同事请他只看文档回答“某某功能从哪里改起”。如果他答得出来这份拆解就过关了如果他反应半天说明你还欠着技术债需要继续打磨。用这个方式验证拆解结果比你自己反复读十遍都管用。
RELATED READING

延伸阅读

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