☰
从单体到智能架构:软件架构演进的约束、取舍与落地
2026/11/2 2:55:22 网站建设 项目流程

软件架构这二十多年走过的路,说穿了就一句话:把复杂度从机器身上挪到人身上,再从人身上挪回机器。早年间一台服务器跑一个包,改一行代码全站重启,那叫单体架构;后来业务跑不动了,开始拆服务、上容器、铺编排,于是有了云原生;到现在,模型成了系统里的一个正式组件,架构图里多出了推理网关、向量库、编排器,这又被叫做智能架构。我给不少团队做过架构复盘,发现一个规律:真正决定成败的,从来不是用了多新的技术,而是架构形态和团队当下的能力、业务的节奏是否对得上。

这篇文章想干的事很朴素——把这条演进线从单体的合理性讲到云原生的落地细节,再讲到模型接入之后架构发生的变化,顺带把每一步"为什么这么选""参数怎么算""坑在哪"讲透。适合三类人看:刚接触分布式、被微服务名词绕晕的同学;正在做架构演进、需要一份取舍依据的负责人;以及想把模型能力接进现有系统的工程同学。我不打算给你一套"标准答案",架构没有标准答案,只有约束条件下的最优解,但这个"约束条件"是可以被说清楚的。

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 TZ=Asia/Shanghai \ JAVA_OPTS="-XX:MaxRAMPercentage=60 -XX:+UseG1GC" WORKDIR /app COPY --from=build /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 负责,最后还是退回了大单体。

给非确定性组件留出确定性的外壳。模型接入这块,我的原则是:模型可以不确定,但它外面的接口、超时、重试、成本上限、审计日志必须是确定的。把不确定性关在笼子里,系统整体才可控。

最后一条:架构文档要写"为什么",而不只是"是什么"。三年后接手的人,看到架构图知道有什么组件,但不知道为什么这么分、为什么不用另一个方案,那这份文档的价值就少了一大半。我现在写设计文档,一定会留一节叫"被否掉的方案及原因",这一节往往是最有价值的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询