架构设计从画图到决策:约束、选型与落地方法
去年帮一个团队做架构评审对方甩过来一张画得相当漂亮的部署图五颜六色的方框、箭头纵横交错看着就很有技术含量。我问了一句这套东西为什么要拆成这么多块对方沉默了十几秒回答说参考了行业里比较主流的做法。这个场景我见过太多次了。项目产品技术具体架构设计这件事最难的从来不是画出那张图而是图上的每一个方框、每一条箭头背后都能说清楚为什么是它而不是别的。图是结果决策才是内容。一份没有决策记录的架构图本质上和一张装修效果图没区别——好看但经不起追问。我写这篇东西是想把架构设计从画图这个动作里拽出来还原成它本来该有的样子一串有先后顺序的判断题。适合谁看准备从零搭一个产品的技术负责人、需要接手别人架构的中层开发、以及那些画完图被老板追问为什么这么设计的同学。下面这些内容不是标准答案是我在实际项目里反复验证过的一套推进顺序你可以按需取用。1. 架构设计的真正起点把模糊需求翻译成可计算的约束绝大多数架构设计的失败根源不在技术选型而在起点就跑偏了——拿到一份需求文档直接开始想用什么框架、拆几个服务。这条路走不通因为需求文档写的是用户要什么而架构要回答的是系统在什么条件下必须成立。1.1 需求清单为什么不能直接变成架构图需求文档里的每一句话几乎都是形容词和动词的组合系统要快支持多人协作数据要安全以后可能扩展。这些描述对产品经理是有意义的对架构师几乎没有信息量。因为快没有单位多人没有量级以后没有时间点。我习惯做的第一件事是把需求逐条改写成带数字的约束。举个例子系统要快这句话在架构层面至少要拆成三问多快算快接口响应时间的目标值是多少、在什么负载下要快并发用户数、峰值QPS、慢到什么程度算不可接受超时阈值和降级边界。这三问答完快才变成一个可以指导设计的输入。这个改写过程有个副作用特别好它会把需求里那些被含糊带过的部分全部暴露出来。某项目组曾经在需求评审时写支持未来业务增长我追问了一句增长到多少结果发现产品和研发对增长的理解差了整整两个数量级——产品想的是用户翻三倍研发以为要按十倍预留。这种错位如果不在架构阶段解决后面所有的容量规划、数据库选型全是空中楼阁。1.2 一张约束采集表该怎么填我一般会用一张表把所有约束摊开四个维度每个维度填不满就先别往下走。维度需要采集的信息填不出来的后果业务约束核心业务流程、业务峰值的时间规律、不能中断的功能清单分不清主次把边缘功能做成核心链路规模约束用户量级、请求量级、数据增长速率、单条数据的大小容量估算失真存储和带宽选型全错质量约束响应时间目标、可用性目标、数据一致性要求无法判断该用同步还是异步、要不要分布式事务团队与资源约束团队人数与技能栈、交付时间节点、可用的基础设施条件选了一堆团队驾驭不了的技术上线即负债这张表里最容易被跳过的是最后一行。很多人觉得架构是纯技术问题团队情况不该掺和进来。这是书生气。一个五人团队维护八个微服务技术再先进也是灾难因为运维成本、排障成本、沟通成本会成倍吃掉技术进步带来的收益。我在实际项目里见过太多教科书式架构死在团队规模上。1.3 从约束推导架构风格的三条判断线约束采集完之后怎么推出架构风格我一般看三条线。第一条线是变更频率的差异度。如果产品里不同模块的变更频率差距很大比如订单规则一周改三次用户基础信息一年不动那按变更频率做拆分是合理的模块边界大致能对上团队边界。如果所有模块都是一起改、一起发硬拆成多个独立部署单元只会增加发布协调成本。第二条线是负载特征的差异度。有的功能是 IO 密集型大量查询、文件读写有的是 CPU 密集型报表计算、图像处理。这两类混在一个进程里会互相拖累——计算任务把线程池占满查询请求全部排队。这时候按负载特征分离部署是有价值的。第三条线是故障影响范围的容忍度。核心交易链路和非核心的运营后台对可用性的要求完全不同。如果非核心模块出问题会拖垮核心模块共享数据库连接池、共享线程池那就必须做隔离。这三条线的判断结果直接决定了你是走单体、模块化单体还是服务化。注意模块化单体在绝大多数中小规模项目里是被严重低估的选项——它在代码层面已经有清晰边界但部署仍是一体省掉了分布式系统那一整套复杂度。先能拆而不拆等真需要拆的时候再拆比一上来就拆散了拼不回去要安全得多。2. 分层与边界的划法让每一层只对一件事负责分层这个词谁都会说但真正做到层与层之间只通过约定的方式通信十个项目里能有两三个就不错了。大部分所谓的分层最后都退化成了文件夹分层——代码放在不同目录里调用关系依旧是网状穿透。2.1 四层划分的实际职责红线我用的分层一般是四层但重点不在层数而在每层的红线什么东西绝对不能出现在这一层。接入层负责协议转换、参数校验、鉴权、限流。红线是不做业务判断。我见过接入层里写如果用户等级是VIP且订单金额大于500则走特殊逻辑的代码这种代码一旦业务规则变了改起来要在网关里翻半天而且完全无法单测。应用层负责编排——把一次业务请求拆成若干个领域动作控制事务边界。红线是不写业务规则本身。应用层应该是薄的它的价值在于顺序和边界不在于逻辑。领域层负责业务规则和状态流转。红线是不依赖任何框架和外部资源。这一层应该是纯逻辑输入是数据输出是数据不碰数据库、不发消息、不调接口。做到这一点业务规则才能真正被单元测试覆盖。基础设施层负责持久化、消息、外部服务调用。红线是不含业务分支。这一层只做怎么存取不做存什么、什么时候存。这套红线的核心价值在于当业务规则变化时改动应该主要落在领域层当存储方案变化时改动应该主要落在基础设施层。如果改一个业务规则要动五个文件分属四个层说明边界已经失效了。2.2 边界划错之后会出现的四种典型症状判断边界有没有划对不用看设计文档看代码症状就行。症状一一个改动牵连多个模块同时发布。你想改个折扣计算结果发现用户模块、订单模块、结算模块都得跟着改。这说明领域边界切在了业务流程的中间而不是切在业务概念的分界上。症状二循环依赖。A 模块调 BB 又反过来调 A。这在编译期可能被语言特性掩盖了但运行时的初始化顺序、测试时的 mock 成本会立刻暴露出来。循环依赖的本质是两个模块共享了同一个业务概念正确的做法是把这个概念单独提出来变成一个双方都依赖的第三块。症状三大量公用工具类。一开始是日期格式化字符串处理后来变成订单状态判断权限校验也放进去了。工具包一旦开始装业务逻辑就说明有业务概念找不到归属这是边界划分不清的典型信号。症状四跨模块的直接数据库访问。订单模块直接查用户表结算模块直接改订单表。这种情况短期看省事长期看等于把两个模块焊死在一起任何一方的表结构变更都会引发连锁反应。2.3 模块依赖方向的可视化校验方法依赖方向对不对光靠人眼看代码是看不准的。我一般用两个土办法做校验。第一个办法是画依赖矩阵。把所有模块列成行和列行依赖列就在格子里打勾。画完之后看两件事对角线右上角是不是大部分为空说明依赖方向单一有没有对称的勾说明存在双向依赖。这个表用表格软件十分钟能画完比看几千行代码直观得多。第二个办法更硬核一点在构建脚本里加依赖检查。大多数语言生态都有现成的依赖分析工具能在编译期阻断非法依赖。比如规定领域层不能引用基础设施层的包一旦有人在领域层 import 了数据库驱动构建直接失败。这种做法一开始会让团队很不适应但坚持两周之后边界就变成肌肉记忆了。注意依赖检查规则不要一次加太多。我建议第一版只加三条最要命的规则跑顺了再逐步扩展。一次性加二十条团队的第一反应是找办法绕过而不是遵守。3. 技术选型的取舍账本不选最强的选最扛得住的技术选型是架构设计里争议最大的环节因为每个人都有自己的偏好而且偏好往往来自上一份工作的经验。我见过一个团队为了技术先进性选了某个刚发布半年的大数据组件结果上线三个月遇到两次内存泄漏社区里连相关 issue 都没几条最后只能自己啃源码打补丁。3.1 选型评估的五个维度与权重我的做法是给每个候选技术打五个维度的分权重按项目实际情况调。这套东西不新鲜但真正填一遍结论往往和直觉不一样。维度评估要点中小项目建议权重大型项目建议权重功能匹配度能否覆盖当前需求缺口需要多少自研补30%25%成熟度与稳定性版本迭代历史、已知严重缺陷、社区活跃度25%30%团队熟悉度团队里有几个人能独立排障25%20%运维成本部署复杂度、监控接入难度、故障恢复手段15%15%退出成本用错了之后换掉的代价有多大5%10%这张表里最关键的是最后一行——退出成本。很多选型讨论只谈用起来爽不爽不谈用错了怎么办。一个技术再优秀如果数据格式是私有的、迁移工具不成熟那你实际上是在和一个供应商绑定终身。我一般会问一句假设三个月后我们要换掉它需要做什么如果答案是数据要重写、业务代码要改三百处那这个选择的权重就要往上调。3.2 中间件与存储选型的对照表具体到组件层面我把常见的取舍整理成下面这张表。这里说的不是绝对结论是决策依据。场景倾向选择判断理由什么时候该反过来选事务型业务数据关系型数据库需要多表关联、强一致性、复杂查询单表数据量过亿且查询模式极简时考虑分片或列式存储高频读取的配置/字典数据进程内缓存 定期刷新数据量小、变更不频繁省掉一次网络往返多实例之间要求秒级一致时改用集中式缓存异步任务与解耦消息队列削峰、解耦、失败重试有天然支持任务量很小且无峰值时直接同步调用更简单全文检索独立检索引擎关系库的模糊查询在数据量上来后性能断崖数据量在十万级以内且查询简单时数据库索引足够了大文件存储对象存储与业务库分离避免备份和扩容互相牵连文件数量极少且需要强事务关联时可考虑库内存储用这张表的时候有个原则每一项倾向选择都要能对应到第 1 章采集的某条约束。如果对应不上说明这个选择是凭感觉做的要么补上依据要么就选更简单的那个。架构设计里简单本身就是一个被低估的优势——少一个组件就少一处可能出故障的地方少一套需要学习的运维知识。3.3 被低估的团队熟悉度成本这一条我要单独拎出来说因为它太容易被忽略。假设有两个方案A 方案在性能上比 B 方案好 30%但团队里只有一个人用过。B 方案性能一般但团队一半人熟悉。选哪个我的答案几乎总是 B。原因很实际性能差距是可以用机器弥补的加节点、加缓存、加索引但人的知识缺口没法用钱快速填平。线上出故障的时候是凌晨两点那个唯一懂 A 方案的人如果联系不上整个团队就只能干等。而且知识集中在一个人身上本身就是组织风险。我在一个项目里吃过这个亏。当时为了追求更优的写入性能选了一个相对小众的存储方案团队里没人有生产环境经验。上线后遇到一个写入放大的问题排查了整整四天才定位到是某个配置项的默认值导致的。四天里业务侧不停催最后是靠翻官方文档的一个角落才找到线索。后来复盘如果当初选团队熟悉的那套方案性能大概低 20%但多花的机器成本折下来还不到那四天人力成本的三分之一。选型的本质不是找最优解是找在你这个团队、这个时间窗口内综合代价最小的解。4. 数据流与接口契约架构里最容易含糊过去的部分架构图画完之后最容易糊弄的就是数据怎么流、接口怎么定。图上一个箭头实际落到代码里可能是同步调用、异步消息、批量同步三种完全不同的东西而这三种的错误处理、超时策略、重试语义全都不一样。箭头不标清楚实现阶段必然各写各的。4.1 端到端数据流的画法与检查点我画数据流不看模块看数据本身。挑一条最核心的业务链路比如用户下单到订单生效然后追踪这份数据从进入到落地经过的每一个环节每个环节标注三件事数据形态、处理动作、失败后的处置。举个具体的一次下单请求的流转大概是接入层校验参数形态原始请求体失败直接返回错误码→ 应用层编排形态业务对象失败回滚已执行的步骤→ 领域层计算价格与库存形态领域对象失败抛出业务异常→ 基础设施层写库形态持久化记录失败重试或进入补偿队列→ 发消息通知下游形态消息体失败本地消息表 定时重投。这条链路上有三个检查点必须明确数据在哪一步从不可信变成可信通常是有一次完整校验的地方、数据在哪一步产生副作用写库、扣库存、发消息、副作用之间的顺序能不能调换。最后这一问特别重要——如果扣库存和发消息的顺序调换会导致下游拿到一个不存在的订单那就说明这个顺序是强约束实现时不能随便优化。4.2 接口契约的七个必填字段接口定义我一般要求写满七项缺一项就不算定完。入参的完整结构包括字段类型、是否必填、长度范围、枚举取值。出参的完整结构包括成功和失败两种形态。错误码清单每个错误码对应什么业务含义调用方看到之后该做什么。超时时间以及超时后调用方的行为重试还是放弃。幂等键以及重复请求的返回语义返回首次结果还是报错。数据量上限单次请求最大条目数、最大报文体积。版本策略字段新增和废弃怎么处理老版本调用方什么时候必须升级。第三项和第五项是最常被跳过的。错误码不定义清楚调用方只能靠 HTTP 状态码猜最后所有错误都变成系统异常请稍后重试用户看到这句话完全不知道该怎么办。幂等键不定义重试就会变成重复下单、重复扣款这类问题在真实业务里造成的事故一点都不少。4.3 幂等、重试与补偿契约里必须提前写死的三件事这三件事必须在架构阶段就定好规则不能留到编码时各自发挥。幂等的核心是找到一个业务上唯一的标识。订单号、支付流水号、外部请求 ID 都可以关键是要保证同一个业务动作重复提交时这个标识不变。有了标识服务端做先查后写或者唯一索引兜底都行。我倾向于两者都做——查询拦截挡住 99% 的重复唯一索引兜住剩下的 1%。只靠查询拦截在并发场景下是挡不住的。重试的规则要写清楚三件事重试几次、间隔多久、什么错误可以重试。我的默认设置是网络超时和 5xx 类错误可以重试业务参数错误不重试重试不超过三次间隔采用指数退避比如 200ms、600ms、1.8s。重试次数不能多因为重试次数乘以调用量就是故障时的放大倍数。下游已经扛不住了你还猛重试等于雪上加霜。补偿是针对那些做了一半的场景。典型的如扣了库存但订单创建失败这时候需要一个反向操作把库存加回去。补偿的设计要点是补偿动作本身也要幂等而且要能重复执行——因为补偿也可能失败失败了还得再补偿一次。注意补偿不是万能的。如果补偿逻辑本身有 bug会造成数据越补越乱。所以我在项目里的原则是能用一次本地事务解决的事情绝不引入补偿只有在跨系统、跨存储的情况下才用补偿并且必须配一个对账机制定期核对。5. 非功能性需求的量化把要快、要稳变成可核算的数字非功能性需求是架构设计里最像玄学的部分因为它不像功能那样有明确的验收标准。但实际上它比功能更该被量化——因为架构方案的主要差异几乎全部体现在性能、可用性、扩展性这些维度上。5.1 容量估算的一个可复算的例子容量估算不需要多精确但必须能算。我给一个我自己常用的推演过程。假设一个协作类产品目标用户 20 万日活按 10% 算是 2 万人均每天打开 8 次产生约 30 次接口请求。那么日请求量约 60 万次。业务高峰集中在上午十点和晚上八点这两个两小时窗口假设 60% 的请求落在这 4 小时里那高峰期的平均 QPS 大约是 36 万 / 14400 秒 25。再乘一个 3 倍的峰值系数真实场景里瞬时峰值往往是均值的数倍设计目标按 75 QPS 考虑。75 QPS 是什么概念单台普通配置的应用服务器只要没有慢查询和外部调用阻塞承载这个量级毫无压力。这个结论很重要因为它直接告诉你不需要做复杂的分布式设计瓶颈根本不在应用层。那瓶颈在哪继续算。每次请求平均产生 3 条数据库操作高峰就是 225 QPS 的数据库请求。如果单条查询平均耗时 5ms那数据库的负载率大约是 225 × 0.005 1.125也就是已经过载了。这时候优化方向就很明确要么减少查询次数合并查询、加缓存要么优化单次查询加索引、减少回表要么读写分离。你看同一个业务算完之后会发现真正的瓶颈和直觉常常不一致。不做估算直接上架构方案等于闭着眼睛开药方。5.2 性能预算的分配方法总量算出来之后要往下分配。我的习惯是给一次端到端请求设一个总预算然后按环节切分。假设接口的响应时间目标是 300ms那可以这么分网络传输 30ms接入层处理 10ms应用层编排 20ms领域逻辑 50ms数据库访问 150ms序列化与返回 40ms。留出的余量不算多但够用。这个分配表的价值在于当某个环节超支时你能立刻知道是谁的问题。比如实测发现数据库访问用了 280ms那不用怀疑去看慢查询日志就行而不是在应用层加一堆缓存来掩盖问题。没有预算分配的性能优化最后都会变成到处加缓存而缓存带来的数据一致性问题往往比原来的性能问题更难处理。5.3 可用性目标与降级策略的对应关系可用性目标不是喊口号它直接决定了你要准备几套方案。经验上的对应关系大致是这样可用性目标年停机时间量级需要的能力99%约 3.6 天基础监控、快速重启、定期备份99.9%约 8.7 小时多实例部署、健康检查、自动摘除99.95%约 4.4 小时跨可用区部署、数据实时同步、故障自动切换99.99%约 52 分钟多活架构、流量调度、故障演练常态化看清这张表很重要因为每往上走一档成本和复杂度都是非线性上升的。一个内部使用的运营后台非要做到 99.99%这是在浪费资源而一个承载核心收入的交易链路只做到 99.9%那就是在赌运气。降级策略要和可用性目标配套设计。比如目标是 99.95%那就必须明确当依赖的某个外部服务不可用时主流程是阻塞等待、返回兜底数据还是直接跳过这个环节这三种选择对用户体验和数据正确性的影响完全不同必须提前定不能等故障发生了临时拍脑袋。6. 从图纸到代码架构落地那段最容易被忽略的灰色地带架构文档写完、评审通过很多人就觉得工作完成了。但从文档到代码之间还有一段很长的路这段路上如果没人管架构会在三个月内自然退化成一锅粥。6.1 架构决策记录的写法我的做法是给每个重要决策写一条记录格式很短但必须写全五项决策内容我们决定做什么。背景约束当时的哪些条件促成了这个决定。备选方案还考虑过什么为什么没选。预期后果这个决定会带来什么好处以及接受什么代价。失效条件什么情况下这个决定需要重新评估。最后一项是最有价值的也是最容易被漏掉的。比如选择单体部署这条决策失效条件可以写当团队规模超过 15 人且发布冲突每周超过 3 次时重新评估。写清楚失效条件这个决策就有了生命周期而不是变成一个谁也不敢动的祖传约定。我见过一个项目三年前因为团队只有三个人选了单体三年后团队三十人了还在单体里挣扎问为什么不分拆回答是当初架构评审定的就是单体。如果当初写了失效条件这个决策早就该被拿出来重新讨论了。6.2 防腐层与适配层的实际落地架构里说要隔离外部依赖落到代码上就是适配层。但很多项目的适配层形同虚设——名字叫 Adapter里面直接调第三方 SDK返回的还是第三方的对象。这种隔离等于没做因为第三方一升级业务代码还是得跟着改。真正的隔离要做到两点入参出参是自己的数据结构第三方对象在适配层内部就转换掉异常也转换第三方的错误码统一映射成自己的业务异常。这两点做到之后换供应商的改动范围就被限制在适配层的几个类里不会外溢。防腐层则用在新旧系统并存的场景。老系统的数据模型往往很混乱直接让新代码去读等于把历史包袱继承过来。做法是在中间加一层转换把老模型翻译成新模型让新系统看到的是干净的数据。这层转换代码写起来有点枯燥但它是新旧系统能够长期共存的前提。6.3 灰度与开关让架构变更可控地生效架构层面的改动风险远大于功能改动因为它影响的是全局。所以必须有能力只让一部分流量走新路径。最基础的手段是功能开关。在配置中心放一个开关控制某段逻辑走新实现还是旧实现。开关的粒度要足够细能精确到某个租户某个渠道某个百分比。但开关不能无限加我一般要求每个开关在文档里登记谁加的、为什么加、计划什么时候删。没有删除计划的开关最后会变成永久的技术债代码里到处是 if-else谁也不敢删。再往上一层是流量灰度。按用户 ID 哈希或者按请求头标记把 1%、5%、20%、50%、100% 的流量逐步切到新路径每一档观察足够长的时间。这里的关键是观察指标要提前定好不能切完了才想我该看什么。至少要盯错误率、关键接口耗时、核心业务转化指标。前两个是技术指标第三个是业务指标——有时候技术指标全绿但业务指标掉了说明新逻辑在语义上有问题。7. 几个反复出现的坑以及我现在的处理手法架构设计这门手艺进步主要靠踩坑。下面这几个是我在不同项目里反复遇到的写出来供参考。7.1 过早抽象三层变五层代码量翻倍有个项目一开始就设计了接入层—应用层—领域层—仓储层—基础设施层五层理由是以后可能要拆微服务先分好。结果一个简单的查询功能要写五个类、跨五个包。三个月后团队开始抱怨改一行代码要翻八个文件然后有人开始抄近路直接在接入层写业务逻辑。半年后五层变成了名义五层实际两层而且比一开始就写两层更乱。我现在的做法是默认从最少的层开始只在真正需要的地方分层。什么时候加层当一段逻辑被三个以上地方复用时或者当它需要被独立测试时。抽象是对重复的回应不是对未知的预防。7.2 把缓存当数据库用缓存能解决性能问题这个说法没错但有个前提缓存里丢了数据业务还能从别处恢复。一旦这个前提不成立缓存就从加速器变成了唯一数据源风险指数级上升。我遇到过的典型场景是把会话状态、临时计算结果、甚至部分业务计数放在缓存里并设置较长的过期时间但没有持久化。结果缓存节点重启数据全丢用户登录态全部失效业务计数归零。更麻烦的是这些数据没有任何地方可以还原。现在的原则很简单缓存只放丢了能重建的数据。凡是不满足这条的要么进数据库要么做持久化没有第三条路。7.3 分布式事务用得太随意微服务拆开之后跨服务的数据一致性成了绕不过去的问题。很多人的第一反应是上分布式事务框架觉得专业的事交给专业的组件。但分布式事务的代价被严重低估了——它带来的性能损耗、实现复杂度、故障排查难度往往超过它解决的问题。我现在的判断顺序是先问这个一致性要求是不是真的强一致。绝大多数业务场景其实是最终一致就够了比如下单后通知库存扣减中间差个几百毫秒完全无感用本地消息表 重试就能解决简单可靠。只有当两个操作必须同时成功或同时失败且不允许任何中间状态被外部观察到时才值得考虑分布式事务。而这种情况在真实业务里其实很少——大部分所谓的强一致需求追根究底是业务方没想清楚中间状态该怎么处理。7.4 架构文档和代码脱节这个问题几乎每个项目都有只是程度不同。文档写完就没人更新三个月后代码已经改了五轮文档还停留在最初的样子。新人来了看文档按文档理解代码然后彻底懵掉。我的处理办法是把文档贴在代码旁边而不是放在一个独立的文档系统里。架构说明、决策记录、关键约束全部以注释或者 Markdown 文件的形式放在代码库里跟代码一起提交、一起评审。这样至少能保证改了代码的人看得到文档评审的人会发现文档没同步。放在外部系统里的文档改代码的人根本不会想起来去更新它。另外一个技巧是把能自动检查的约束写成脚本。比如领域层不能依赖基础设施层这条写成构建时的检查规则比写在文档里有效一百倍。文档靠自觉脚本靠机器后者的可靠性高得多。这四条坑有一个共同点都是在当时看起来合理的情况下发生的。过早抽象是为了未来扩展缓存当库是为了性能分布式事务是为了正确性文档独立存放是为了统一管理。每一个决策在当时都有正当理由问题出在没有配套的约束条件和退出机制。我个人在实际操作中的体会是架构设计的功夫一半在设计另一半在给自己留后路——每做一个决定都想清楚它什么时候该被推翻推翻的代价有多大。想清楚这两点架构就从一个静态的图纸变成了一个能跟着业务一起长的东西。