
软件架构这二十多年走过的路说穿了就一句话把复杂度从机器身上挪到人身上再从人身上挪回机器。早年间一台服务器跑一个包改一行代码全站重启那叫单体架构后来业务跑不动了开始拆服务、上容器、铺编排于是有了云原生到现在模型成了系统里的一个正式组件架构图里多出了推理网关、向量库、编排器这又被叫做智能架构。我给不少团队做过架构复盘发现一个规律真正决定成败的从来不是用了多新的技术而是架构形态和团队当下的能力、业务的节奏是否对得上。这篇文章想干的事很朴素——把这条演进线从单体的合理性讲到云原生的落地细节再讲到模型接入之后架构发生的变化顺带把每一步为什么这么选参数怎么算坑在哪讲透。适合三类人看刚接触分布式、被微服务名词绕晕的同学正在做架构演进、需要一份取舍依据的负责人以及想把模型能力接进现有系统的工程同学。我不打算给你一套标准答案架构没有标准答案只有约束条件下的最优解但这个约束条件是可以被说清楚的。1. 先搞明白架构演进到底在演进什么很多讲架构史的文章喜欢按年份排流水账我看完记不住因为缺了主线。我更愿意把架构演进看成一场持续的搬运动作一段时间里复杂度集中在代码里机器很轻松另一段时间里复杂度被搬到基础设施上代码变简单了但运维变重了。每次搬家都有代价也都有触发条件。1.1 三个约束条件决定了你该长成什么样我判断一个团队适合什么架构基本只看三件事。第一是团队规模五个人的团队搞二十个微服务一个人管四个服务光发布就会把人拖死反过来三百人的团队守着一个单体主干合并冲突能让你怀疑人生。第二是业务变化速度如果需求半年才动一次单体完全够用拆微服务纯属给自己找活干如果一周发三次版还得几个组并行发那拆分的收益立刻显现。第三是故障成本一个模块挂掉是影响一个边缘功能还是影响全站下单这决定了你要不要做隔离、要不要做多副本、要不要做异地。这三条之外还有一条隐性约束经常被忽略技术债存量。一个跑了八年、没有任何测试覆盖的单体你直接上微服务等于把一堆看不见的耦合直接变成跨网络的分布式事务问题只会更严重。我见过不止一次这样的迁移最后的结果是新的没跑顺旧的也不敢下线两套系统并行维护团队疲惫不堪。所以真要做演进第一步通常不是拆而是先让旧系统可观测、可测试把地基垫平。这三条约束我一般会打分团队规模 1-5 分、变化速度 1-5 分、故障成本 1-5 分总分低于 7 分的时候我几乎不会建议上微服务先把模块化和自动化做扎实更划算。1.2 每一代架构都是对上一代痛点的过度矫正如果把这条线摆开来看会发现一个很有意思的现象每一代架构其实都在解决上一代的核心痛点而且往往会矫枉过正然后下一代再来往回拉一点。阶段主流形态当年最大的痛点主要解法矫正过头之后带来的新问题早期单体应用部署麻烦、环境不一致一个包、一次部署耦合严重、一处改动全站回归分层期分层 模块化代码职责混乱分层约束、模块边界层数越堆越多调用链变长服务化SOA 总线异构系统互联统一总线做协议转换总线成瓶颈配置改动排期漫长微服务独立部署的小服务单体改不动、扩不动按业务拆分、独立发布分布式复杂度爆炸、运维成本高云原生容器 编排 网格环境差异、弹性不足不可变基础设施、声明式编排学习曲线陡、排障链路变长智能架构模型作为组件接入非结构化数据处理推理服务、检索增强、编排器延迟高、成本高、结果不确定这张表我经常拿来做复盘用。当你发现团队正在抱怨服务太多管不过来其实不是微服务错了而是你可能把拆分当成了目的忘了拆分只该服务于独立变更和独立扩容这两个目标。同理当你抱怨K8s 太复杂大概率是你把本来不该容器化的东西比如有状态数据库的早期形态也塞进去了。2. 单体时代一台机器扛起所有也埋下所有坑现在讲单体很多人第一反应是落后。这个判断不太公平。单体架构在它诞生的场景里是极其聪明的选择甚至在今天很多业务用单体跑得比微服务还稳、还快。2.1 单体的真实形态以及它当年为什么是对的典型的单体长这样一个进程、一份代码、一个数据库页面请求进来经过控制器、业务逻辑、数据访问全部在同一块内存里完成。业务层调用数据层就是一次方法调用纳秒级事务用数据库本地事务就能保证BEGIN到COMMIT之间所有操作要么全成要么全败你根本不需要考虑什么最终一致性、补偿、对账。部署也简单得可爱打一个包传到服务器重启进程完事。出了问题看一份日志、连一个数据库就能定位不需要跨三个系统查链路 ID。我至今认为在业务模型还没稳定下来的阶段单体是最高效的形态因为你可以用最快的速度改代码、改表结构、改接口不需要跟任何人协调。真正让单体变难受的不是单体这个形态本身而是它承载的东西超出了它能承受的量级。2.2 出现这几个信号说明单体真的到瓶颈了我一般看几个可量化的指标而不是凭感觉。第一单次全量构建时间超过 10 分钟说明代码量已经大到影响反馈速度了你改一行等十分钟一天能试错几次第二发布只能放在凌晨且回滚率超过 15%说明一次发布影响的面积太大团队不敢在白天发版。第三主干合并冲突每周超过 5 次而且是不同业务组之间冲突说明代码的所有权边界已经模糊了。第四一个非核心模块的内存泄漏把整个进程打挂这就是隔离缺失的直接体现。这四个信号里最致命的是第二个和第四个。前者摧毁的是迭代节奏后者摧毁的是可用性。到了这一步你要做的其实不是立刻拆微服务而是先做两件事一是把可独立的部分模块化让代码层面的边界先清晰起来二是把可分离的进程拆出去比如把定时任务、消息消费、报表导出这些明显异步的东西从主进程里挪走。这两步做完很多团队会发现压力已经缓解了大半根本不需要上完整微服务。3. 分层与模块化在单体内部先做一次软切割单体的第一个进化方向不是拆进程而是拆代码结构。这一步做得好后面拆服务会顺很多做得不好你会把一坨意大利面拆成二十份意大利面。3.1 分层架构的边界约定比分层本身更重要经典三层是控制器层、业务层、数据访问层。但我在很多代码库里看到的分层只是目录名分了实际调用是乱的控制器直接调数据访问层、业务层互相循环依赖、工具类被所有人引用。这种分层等于没分。我自己的约定是三条硬规则。第一依赖方向单一控制器只能调业务层业务层只能调数据访问层反向调用一律不允许。第二跨层数据不传递实体对象数据访问层返回的是数据对象业务层转成领域对象控制器再转成对外传输对象三层三套模型虽然写起来啰嗦但好处是数据库字段改动不会穿透到接口层。第三工具类必须无状态任何带状态的共享工具类都是定时炸弹。我见过一个很典型的案例某团队把当前登录用户存在一个静态变量里单体跑着没问题因为一个请求一个线程后来一上异步任务用户信息串得一塌糊涂排查了整整两天。这类问题在单体时代不显眼一到并发或者分布式就集中爆发。3.2 包结构应该按业务分而不是按技术分这一点争议挺大但我态度很明确中大型项目包结构按业务领域划分领域内部再按技术分层。按技术分所有控制器一个包、所有服务一个包在小项目里看着整齐一旦项目变大你要改一个功能得在四个包里来回跳心智负担极高。一个我常用的结构大概是这样com.example.order ├── api // 对外接口与传输对象 ├── application // 用例编排 ├── domain // 领域模型与领域服务 ├── infra // 仓储实现、外部调用 └── config // 该模块的配置 com.example.payment ├── api ├── application ├── domain └── infra这样组织的好处是模块边界在目录上就可见了。你想知道订单模块依赖了支付模块的什么看一眼导入语句就知道你想单独把订单模块抽成服务直接把这个目录搬走改一下依赖注入就行。这就是为什么我一直说模块化是微服务的前置作业作业没做就跳级后面要补的课更多。4. 面向服务的过渡总线、契约与它的历史包袱单体的边界画清楚了下一步自然就想到能不能让不同系统互相调用。这一段历史里最值得复盘的是面向服务的架构以及它配套的那套企业服务总线。4.1 初衷很好落地时容易被做成新的中心最初的设想很合理企业内部有十几套异构系统语言不同、协议不同与其两两对接形成网状结构不如大家都接到一根总线上由总线负责协议转换、路由、数据格式映射。理论上新增一个系统只要接一次总线连接数从 N 平方降到 N这个思路非常漂亮。问题出在总线后来什么都想管。路由、转换之外还加了编排、加了业务规则、加了监控告警、加了安全策略最后总线变成了一个庞大到没人敢动的中心节点。所有业务变更都要排总线的队总线的配置改一次要停机窗口总线的性能成了全公司接口的上限。这时候它已经不是解耦工具而是新的耦合中心。我总结这个阶段的教训只有一句话任何被设计成所有流量都必经的组件都必须按最高可用性标准来建设而且要预留水平扩展的能力。否则它一定会成为下一个瓶颈。这条教训在后来的网关、注册中心、消息中间件上反复应验。4.2 从重总线转向轻量调用契约先行后来的方向是去中心化不再依赖一个中心节点做转换而是让服务之间用统一的协议直接通信注册中心只负责谁在哪负载均衡放到调用方。这套模式的核心变化是接口契约从运行时的总线配置变成了代码里的接口定义。契约先行这件事的价值我在实际项目里体会很深。当接口定义入参、出参、错误码、超时语义被写进代码仓库并且随版本发布一起管理上下游的沟通成本会显著下降。反过来如果接口只存在于文档里文档三天就过期了。我一般要求接口定义里必须明确这几项超时时间、是否幂等、失败重试策略、错误码语义。尤其是幂等这是后面做重试和补偿的前提。一个非幂等的写接口你在调用方加一次自动重试就可能产生两笔订单这种事故我见过太多次。// 契约里把关键语义写清楚比写一堆文档有用 public interface InventoryService { /** * 扣减库存幂等同一 requestId 重复调用只生效一次 * 超时建议800ms下游 DB 操作 P99 约 120ms * 失败语义抛 InventoryShortageException 表示库存不足可重试的只有 TimeoutException */ DeductResult deduct(DeductRequest request); }5. 微服务拆得开容易管得住才是本事到了微服务这一段网上资料最多但误区也最多。我见过太多团队把拆当成目标拆完之后发现发布更慢了、故障更多了、排查更难了然后又想着要不要合回去。5.1 拆分粒度别按技术层拆按变更频率和数据所有权拆先说一个反例。有些团队按技术层拆所有数据访问逻辑一个服务、所有业务逻辑一个服务、所有对外接口一个服务。这种拆法看着分层清晰实际上是最糟的因为每次业务变更都要同时改三个服务、发三次版、做三次联调拆分的收益一点没拿到成本全付了。我的判断依据是两条。第一变更频率是否一致经常一起改的东西应该放在一起一年不动的东西不该和天天改的东西捆在一块。第二数据所有权是否清晰一个服务应该是它那部分数据的唯一写入方别的服务只能通过接口读不能直接连库。这两条比什么领域驱动设计的方法论都实在先按这两条划划完再用领域模型去校验命名和边界。至于拆多少个我给个粗糙的估算服务数量 ≈ 能独立负责服务的团队数 × 1.5。一个五到八人的小组维护一到两个服务是比较舒服的超过三个就开始顾此失彼。二十人的团队拆出三十个服务那不是微服务那是微灾难。5.2 拆完之后必然面对的六件事拆进程这件事本质上是把方法调用换成了网络调用而网络调用有六个躲不开的特性会超时、会失败、会重复、会有延迟抖动、会有部分成功、会不可达。这六件事在单体里统统不存在所以很多人第一次拆服务会在这些问题上栽跟头。对应的工程手段是超时控制、重试加退避、幂等设计、熔断降级、分布式追踪、配置与注册中心。这里面最容易做错的是重试。我见过一个链路A 调 B 超时 1 秒A 重试 3 次B 调 C 超时 1 秒B 也重试 3 次结果一次用户请求在最下游触发了 9 次调用直接把数据库打满。这就是典型的重试放大。我的做法是只在最外层做重试中间层一律不重试重试必须配指数退避加随机抖动只对明确的可重试错误超时、连接失败、限流拒绝重试业务错误一律不重试。5.3 超时怎么设一个能算出来的漏斗超时不是拍脑袋定的。我一般用漏斗原则下游所有串行调用的超时之和必须小于上游给它的超时而且每一层都要留出 20% 左右的缓冲。举个例子一个下单接口内部串行调用库存服务、优惠服务、订单服务。假设三者的 P99 分别是 120ms、200ms、150ms。那么下单接口的内部耗时上限大约是 470ms加上序列化、网络、GC 等开销我会给它设 800ms 超时对外网关设 1000ms。这样任何一层变慢都会在它自己的超时点被截断不会把整个链路拖死。线程池大小也是同理可以用最基础的排队论估算并发数 QPS × 平均响应时间。假设单实例 QPS 200平均响应 50ms那理论并发是 10考虑到突发流量核心线程数设 20 到 30 比较稳。这个公式简单但能避免很多线程数凭感觉拍个 200的悲剧。# 一个典型的服务治理配置示例数值需按实际压测调整 resilience: timeout: connect: 200ms read: 800ms retry: max-attempts: 2 # 加上首次共 2 次不要多 backoff: 100ms jitter: true retry-on: [TIMEOUT, CONNECT_FAILED, RATE_LIMITED] circuit-breaker: failure-rate-threshold: 50 # 窗口内失败率过半即熔断 sliding-window-size: 20 wait-duration-in-open-state: 10s bulkhead: max-concurrent-calls: 50 # 舱壁隔离防止单一下游拖垮整个线程池6. 云原生把运维负担从人身上挪到平台微服务解决了拆的问题但没解决怎么跑的问题。服务一多机器怎么分配、环境怎么一致、扩容怎么及时这些都变成了新的瓶颈。云原生的本质是把这些重复劳动沉淀到平台层。6.1 容器化把环境一致这件事彻底解决掉容器带来最大的改变不是轻量而是不可变基础设施。镜像构建出来之后就是只读的任何环境跑的都是同一个镜像配置通过外部注入。这直接消灭了我本地是好的这类经典争论。我写镜像有几个习惯。一是用多阶段构建编译环境和运行环境分开最终镜像只留运行需要的东西体积能小一大截拉取也快。二是固定基础镜像版本不要用最新标签否则某天平台更新了基础镜像你的构建结果突然变了排查起来极其痛苦。三是非 root 运行并且把时区、字符集这类容易出问题的东西在镜像里就定好。# 第一阶段构建 FROM eclipse-temurin:17-jdk AS build WORKDIR /src COPY pom.xml . RUN mvn -B dependency:go-offline COPY src ./src RUN mvn -B clean package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre ENV TZAsia/Shanghai \ JAVA_OPTS-XX:MaxRAMPercentage60 -XX:UseG1GC WORKDIR /app COPY --frombuild /src/target/app.jar ./app.jar USER 10001 ENTRYPOINT [sh, -c, exec java $JAVA_OPTS -jar app.jar]6.2 编排平台的关键参数资源申请与限制怎么算编排平台里最容易出事的两个字段是资源申请和资源限制。申请值决定调度到哪台机器限制值决定超了会怎样。申请设太小节点会被压垮限制设太紧进程直接被干掉。我的经验是内存的申请和限制设成相等避免节点内存超卖导致的连锁驱逐CPU 的申请可以小于限制允许一定的弹性借用。JVM 应用要特别注意堆内存不能按限制值满打满算因为还有元空间、线程栈、直接内存、GC 自身开销一般把堆设在限制值的 60% 左右比较安全。apiVersion: apps/v1 kind: Deployment metadata: name: order-service spec: replicas: 4 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 # 滚动发布时不允许可用副本减少保证容量 template: spec: containers: - name: app image: registry.example/order-service:1.4.2 resources: requests: cpu: 500m memory: 2Gi limits: cpu: 1500m memory: 2Gi # 与 requests 保持一致避免被驱逐 readinessProbe: httpGet: { path: /health/ready, port: 8080 } initialDelaySeconds: 20 periodSeconds: 5 livenessProbe: httpGet: { path: /health/live, port: 8080 } initialDelaySeconds: 60 periodSeconds: 10 failureThreshold: 3副本数也别拍脑袋。算法是峰值 QPS ÷ 单副本安全承载 QPS再乘以 1.3 的冗余系数。单副本安全承载量要通过压测拿到一般是压到 CPU 70% 时的 QPS。假设峰值 3000 QPS单副本压测在 70% CPU 时能扛 900 QPS那 3000 ÷ 900 × 1.3 ≈ 4.3取 5 个副本。这个算法比先上 2 个看看要靠谱得多。6.3 服务网格与函数计算两种不同的减负思路服务网格的思路是把服务治理能力重试、熔断、加密、追踪从业务代码里抽出来放到一个边车代理里。好处是业务代码干净多语言支持好代价是每个 Pod 多一个容器多一跳网络延迟增加个位数毫秒资源开销增加。我的判断是服务数量超过三十个、且多语言并存的时候网格的收益才开始覆盖成本小规模团队上网格往往是给自己加了一层不熟悉的排障环节。函数计算的思路更激进连服务常驻都不要了来一个请求启动一次执行环境跑完就销毁。它的优势是极致的弹性和按量计费劣势是冷启动。一个带运行时初始化的函数冷启动几百毫秒到几秒都有可能这对延迟敏感的主链路是致命的。所以我一般把函数计算用在异步任务、定时任务、低频事件处理上主链路还是常驻服务更稳。7. 智能架构当模型变成系统里的一个正式组件这两年架构图变化最大的地方是多了一类不太好伺候的组件——模型推理服务。它跟传统服务的差别很大延迟高且波动大、成本高、输出不确定、还常常是流式的。这几点决定了它不能按普通微服务的方式来对待。7.1 推理服务的接入形态和普通接口完全不是一回事普通接口的 P99 可能几十毫秒推理服务动辄几秒普通接口的耗时相对稳定推理服务的耗时跟输入长度强相关同样的接口输入五百字和五千字可能差十倍。所以超时设置必须按输入长度动态调整或者在网关层做输入长度校验超长的直接拒绝而不是让它慢慢跑然后超时。第二个差别是流式输出。绝大多数场景下用户不可能等五秒才看到第一个字所以要用流式返回边走边显示。这对网关和前端都是新要求网关不能做响应缓冲负载均衡要支持长连接前端的错误处理也要重新设计因为响应一旦开始返回状态码就已经发出去了中途出错只能在流里传错误事件。第三个差别是成本结构。传统服务的成本主要是 CPU 和内存推理服务的成本高度集中在加速卡上而且跟批处理效率强相关。单条请求跑一次和攒十条一起跑单条成本可能差好几倍。所以推理服务通常要做请求排队和动态批处理这又引入了排队延迟。这个取舍没有标准答案得看你的场景更在意首字延迟还是单位成本。我的做法是把推理服务单独放到一个资源池里跟业务服务做物理隔离走独立的队列和限流策略。业务侧看到的只是一个普通的异步任务接口提交任务拿 ID然后轮询或者通过流式通道拿结果。这样业务服务不会被推理的长尾延迟拖住推理侧扩容也不影响业务侧。7.2 检索增强向量库在架构里的位置模型只知道训练时见过的东西企业自有知识得靠检索来补。这就是检索增强的基本思路用户提问先从知识库里召回相关片段拼进上下文再交给模型生成。架构上它分成两条链路。离线链路负责把文档切片、转向量、写入向量库在线链路负责把问题转向量、召回若干片段、重排、拼接、调用模型。这里有几个参数我认为必须显式配置不能全靠默认值。切片大小我一般从 300 到 800 个 token 之间试重叠部分设 10% 到 20%。切太小语义不完整模型拿到碎片理解不了切太大噪音太多还会挤占上下文预算。召回数量先粗排拿 20 条再用重排模型选前 5 条这个粗召回 精排的组合比直接召回 5 条效果稳定得多。上下文预算要算清楚假设模型上下文窗口 8000 token留 2000 给回答那检索内容加提示词不能超过 6000按每片 500 token 算最多放 10 片左右。这里有个很容易被忽略的点向量库的索引是需要重建的。文档更新之后旧向量不会自动消失你得有一套文档版本—切片映射的机制删除或者重建时要能精确清理否则会出现文档已经改了模型还在按老内容回答的诡异现象。7.3 编排器带来的新工程问题不确定性怎么兜住再往上走一层就是让模型去调用工具、多步推理、自主决策也就是所谓的智能体编排。这一层的架构挑战跟前几层完全不同它是非确定性的。同一个输入可能走三步就结束也可能走十五步还可能陷入循环出不来。所以工程上必须加几道护栏。最大步数限制超过就强制终止并返回兜底答案。单次会话成本上限按 token 数或者工具调用次数设阈值超了就降级成简单模式。工具调用的幂等与审计每个工具调用都要记日志写操作要有唯一请求号。沙箱隔离模型生成的代码或者命令必须在受限环境里执行不能直接碰生产环境。还有一类风险来自输入本身。用户输入里可能夹带忽略之前的指令这类内容诱导模型做出非预期行为工程上要在入口做过滤在关键决策点做二次校验重要操作要人工确认。这不是模型能力问题是系统设计问题靠提示词是兜不住的。8. 常见问题与排查实录架构演进这条路上踩过的坑我挑几个最有代表性的整理成速查表都是可以直接对号入座的。8.1 典型故障速查表现象最可能的原因排查路径处理方式单个下游变慢导致全站雪崩缺少舱壁隔离与熔断看线程池是否被单一依赖占满加舱壁隔离按依赖维度限并发服务实例正常但流量打不进来健康检查探针配置错误看就绪探针是否一直失败区分存活与就绪探针启动期给足延迟滚动发布期间大量 502可用副本允许减少检查发布策略的不可用副本数设为 0先起新再停旧容器频繁被重启退出码 137内存超限被杀对比容器内存曲线与限制值堆内存下调到限制值 60% 左右配置修改后不生效配置缓存未刷新或未推送看配置中心的推送记录加版本号与灰度推送避免全量刷新链路追踪断在某一跳异步线程或消息队列未透传上下文检查线程池与消费者是否传递追踪标识在异步边界手动注入上下文检索结果答非所问切片粒度不当或召回太少抽样看召回的原始片段调整切片大小加粗召回再重排模型输出时好时坏温度参数过高或提示词不稳定固定输入重复跑多次对比降低随机性输出做结构化约束与校验8.2 几条我用真金白银换来的经验第一别在没有可观测性的情况下做拆分。我经历过一次服务拆完了日志分散在十几个地方没有统一追踪一次线上问题排查花了六个小时。后来我们的规矩是先上追踪和指标再拆服务顺序不能反。指标体系至少要有请求量、错误率、耗时分布这三个维度按服务、按接口、按依赖分别看。第二数据库别急着拆。很多人一上微服务第一件事就是分库结果跨库 Join 全废了只能靠应用层拼装性能掉一大截。我的建议是先拆服务、后拆库服务边界稳定运行三个月以上再考虑把数据真正切开。中间阶段可以用同一个库的不同表来过渡配合只允许本服务写自己的表这条纪律。第三网关不要长成新的总线。网关只做三件事路由转发、认证鉴权、限流。一旦有人想在网关里加业务编排、加数据聚合、加协议转换我就会拦住。历史已经演过一遍了中心化的东西一旦承担业务逻辑就会变成谁都不敢动的黑盒。第四异步化是解决长延迟的通用解但要先解决幂等。把同步调用改成消息队列能立刻缓解超时问题但如果消费端不幂等重投就会产生重复数据。我的顺序永远是先做幂等键再上异步反过来一定会出事。第五新技术的引入必须有明确的退出条件。引入网格、引入编排、引入向量库都要提前想好什么情况下我们会认为它不合适怎么退回去。没有退出方案的引入最后往往变成骑虎难下成本一直付收益一直不来。9. 我在架构选型上守着的几条硬规则最后分享几条我自己的规矩不是什么金科玉律但确实帮我避开了不少坑。能用简单方案解决的绝不上复杂方案。一个定时任务加一张表能解决的对账问题不要上流式计算一个单体加读写分离能撑住的量不要拆微服务。复杂度是有利息的你今天借的每一分后面都要连本带利还。架构演进要能半途而废。好的演进路径是分阶段的每一阶段结束都是一个可用的稳态。就算后面不继续走了系统也能正常运转。反过来那种必须全部改完才能跑的大重构风险极高我基本不碰。任何跨团队的技术决策先谈边界再谈技术。服务是谁的、数据是谁的、故障谁背、发布谁批这些谈不清楚技术选型再漂亮也落不了地。我在实际项目里见过太多次架构图很完美但因为没人愿意为自己的服务 SLA 负责最后还是退回了大单体。给非确定性组件留出确定性的外壳。模型接入这块我的原则是模型可以不确定但它外面的接口、超时、重试、成本上限、审计日志必须是确定的。把不确定性关在笼子里系统整体才可控。最后一条架构文档要写为什么而不只是是什么。三年后接手的人看到架构图知道有什么组件但不知道为什么这么分、为什么不用另一个方案那这份文档的价值就少了一大半。我现在写设计文档一定会留一节叫被否掉的方案及原因这一节往往是最有价值的。