微服务是被逼出来的:从单体架构到服务拆分的演进之路
2026/9/2 19:09:35 网站建设 项目流程

这几年聊微服务,聊得越多会越发现一个现象:很多团队还没被业务问题逼到那个份上,就已经在规划微服务架构了;而真正把系统规模做到一定量级的团队,往往会有另一个体会——微服务通常不是“设计”出来的,而是被增长一步一步“逼”出来的。

Uber 的工程演进常被拿出来当作微服务架构的经典案例。早期为了快速验证市场、快速迭代,Uber 采用的是单体架构;后来业务覆盖的城市越来越多,司机、乘客、运营、支付、地图等模块之间互相挤占资源,发布一次要牵动全链路,数据库连接数也濒临极限。于是,架构被“增长”推着走,服务被一次次拆开。这个过程中踩过的坑比最终看到的架构图要多得多。

本文不打算重复那种“单体架构不好,微服务很好”的结论式文章,而是把视角放在几个更实际的问题上:微服务为什么会被“逼”出来?拆分的最佳时机和方法是什么?拆完之后又会遇到哪些新问题?中小团队是否要盲目跟进?如果你正在做架构规划,或者正处于单体改造微服务的途中,这篇内容会比较适合你。

1. 先理解一个核心观点:微服务不是目的,而是结果

1.1 单体架构并不是落后,而是起点的最优解

很多初学者容易把“单体架构”和“落后”画等号,这是一个很大的误解。

在业务刚开始、团队只有几个人的阶段,单体架构其实是性价比最高的选择。它只有一个应用、一个数据库、一套部署流程,代码之间可以直接调用,调试简单,事务也容易保证,发布链路也不复杂。此时如果强行拆成微服务,带来的只有负担:网络通信变复杂、数据一致性变难、部署环境变多、排查问题变慢。

所以,业界经常说的“微服务是被逼出来的”,并不是一句调侃,而是描述了一个真实规律:架构始终在跟随业务规模与组织规模一起演化。单体架构在早期承担了“快速试错”的重任,只是当业务扩张到一定程度后,它的某些优势会转化为瓶颈。

1.2 “被逼”二字的技术含义

所谓“被逼”,通常在技术层面表现为以下几类压力:

  • 数据层压力:数据库连接数达到上限,一个高流量模块会拖垮所有模块。
  • 团队协作压力:多个团队在一个代码仓库里开发,合并冲突频繁,版本发布互相踩线。
  • 发布压力:任何一个模块哪怕是改了一行配置,也要触发整个应用重新测试和发布。
  • 故障隔离压力:某个功能出现内存溢出时,可能导致整个系统雪崩。
  • 扩容压力:CPU 密集型和 IO 密集型模块混在同一个服务里,扩容时只能“一刀切”,资源浪费严重。

当这五类压力同时出现时,架构演进就不再是“选择题”,而是“生存题”。这也是理解微服务架构最重要的切入点:微服务不是设计者坐在白板前想出来的完美蓝图,而是团队在业务增长过程中不断妥协、不断权衡、不断修复问题后的产物。

2. 单体架构的优势与增长瓶颈

2.1 单体架构为什么能撑起早期业务

先看一个典型的单体应用结构:

my-project/ ├── src/ │ ├── main/ │ │ ├── java/com/company/demo/ │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── dao/ │ │ │ ├── model/ │ │ │ └── DemoApplication.java │ │ └── resources/ │ │ └── application.yml │ └── test/ ├── pom.xml └── README.md

在项目早期,这种结构是非常清晰的。controller 负责接口暴露,service 负责业务逻辑,dao 负责数据访问,所有模块共享一套数据库连接池和一套配置。开发者只需要在 IDE 里启动一个应用,就可以完成整个功能的开发、联调和测试。

这个阶段,单体架构的优势非常明显:

  • 业务逻辑调整成本低,方法之间直接调用,不需要关心网络。
  • 事务处理简单,一个方法内可以包裹多次数据库操作,ACID 特性天然存在。
  • 本地调试方便,断点可以打在任何位置。
  • 部署简单,一个 jar 包或 war 包就能完成发布。

2.2 增长之后,瓶颈开始显现

当业务量涨到一定程度,单体的“简单”会逐渐变成“脆弱”。

第一个问题是数据库连接数。假设应用最大连接池是 50,订单模块、用户模块、支付模块共用同一组连接。订单模块碰到秒杀类流量,瞬间占满全部连接,支付模块的所有请求都要排队等待。即使你扩容了服务实例,数据库侧的连接数限制仍然是一个绕不过去的墙。

第二个问题是团队协作。多个团队修改同一个仓库,今天你改了订单模块的服务,明天他改了支付模块的公共类,代码合并时冲突不断。为了保证上线安全,整个应用必须在发版窗口内一起回归,任何一个模块阻塞,其他团队都要等待。

第三个问题是故障影响范围。单体架构中,一个进程内的某个线程池被打满,或者某个地方出现死循环,影响的是整个应用。一个并不核心的统计任务出现内存问题,可能导致用户登录、下单全部不可用。

第四个问题是技术栈演进困难。在单体中,想对某个模块引入新技术,例如把某段 Python 写的算法模块化并用 Go 重写,几乎不可能与 Java 主应用无缝共存。技术选型会被绑死在最初的决定上。

这些问题并不是某一个“错误”导致的,而是架构在规模面前的自然磨损。当磨损足够严重时,拆分就变成了必然选择。

3. 微服务被“逼”出来的触发信号

3.1 什么时候应该考虑拆分

在实际项目中,并不存在一个精确的“单机 QPS 超过 1000 就拆分”的标准。系统的拆分信号往往出现在组织协作和资源瓶颈上,可以从下面几个维度来判断:

维度说明
团队规模代码仓库超过一个团队可以稳定维护的范围,通常接近 10 人以上时,维护成本急剧上升
发布频率一次发版需要多个团队排期对齐,发布窗口被大量挤占
数据库压力数据库连接数告警频繁,CPU 和慢 SQL 相互影响
故障隔离局部故障频繁演变成全局故障,业务连续性受损严重
技术演进某个模块需要引入新的技术栈,但被主技术栈制约

如果这些信号出现了多个,说明业务增长已经给架构提出了新要求。这时去做微服务拆分,是“顺势而为”;如果这些信号一个都还没有,团队只有三五个人,业务也处于验证阶段,就值得慎重考虑:是否真的需要立刻上微服务。

3.2 拆分的启动方式

拆分的启动通常有两种形式。

一种是“自上而下”的规划型拆分。架构师或技术委员会基于业务规划,先识别出未来半年到一年的流量增长点,然后主动划分服务边界。这种方式的优点是可控性强,缺点是容易过度设计。

另一种是“自下而上”的被动式拆分。某个模块出现线上事故,或者某个团队发现自己的迭代节奏被严重拖慢,于是先把这个模块抽出去独立部署。Uber 的演进历程中,这种被动驱动是很常见的。某个业务模块出了问题,工程师花几周时间把它拆成独立服务,解决眼下的痛点,然后再继续处理下一个痛点。

在实际操作中,这两种方式往往是交替进行的。完全靠“自上而下”容易把架构推到过度复杂;完全靠“自下而上”又容易拆出大量缺乏统一规范的服务。比较稳妥的做法是:先有整体规划,识别出服务域边界,再从痛点最集中的模块开始动手拆分。

4. 服务拆分方法论:怎么拆才不被反噬

“拆”这个字看起来容易,但拆得不好反而会加重问题。这里有一套比较通用的拆分思路,也是很多团队验证过的路径。

4.1 按业务域拆分,而不是按技术层拆分

拆分服务的第一个原则:按业务能力划分,而不是按 controller、service、dao 这样的技术层划分。

反例是常见的一种错误做法:把所有 controller 拆成一个服务,所有 service 拆成一个服务,所有 dao 拆成一个服务。这种拆分本质上只改变了代码的物理位置,并没有解决业务耦合问题。controller 服务调用 service 服务时,依然会产生大量远程调用,性能反而比单体更差。

正确的思路是按照业务域划分,例如订单域、用户域、支付域、库存域、配送域。每个域拥有自己独立的数据、独立的服务、独立的技术决策权。服务之间通过 API 通信,不直接访问对方的数据库。

4.2 先解决数据归属问题

拆分微服务时,一个非常核心的问题就是数据库。如果一个服务拆出去了,但所有表仍然留在同一个数据库里,那么服务边界实际上没有建立起来。别的服务依旧可以通过一条 SQL 直接查询你的表,事务也仍然纠缠在同一个数据库中。

因此,拆分顺序一般是:

  1. 先梳理每个业务域的数据归属。
  2. 将数据表按照归属划分到不同数据库。
  3. 服务之间只能通过接口访问数据。

这个步骤通常是最痛苦的,因为历史业务中往往存在大量交叉查询。处理交叉查询时,一般有两种选择:一种是把数据冗余到目标服务中,由源服务提供同步;另一种是保留一个服务作为数据主归属方,其他服务通过 API 获取数据。这两种方案没有孰优孰劣,但都需要业务团队对“数据所有权”达成共识。

4.3 先模块化,再微服务化

很多团队在拆分时容易走向另一个极端:计划一次把单体拆成 20 个微服务,结果发布上线时发现联调根本跑不通。避免这种风险的方法是“先模块化,再微服务化”。

模块化单体(Modular Monolith)是一种折中方案。代码仍然打包在一个应用中,但内部强制划分模块边界,模块之间只通过明确定义的接口交互。当模块边界稳定后,再逐步将模块独立部署成微服务。

下面是一个 Maven 多模块项目的简化结构,这种结构可以作为单体向微服务过渡的中间状态:

e-commerce/ ├── pom.xml ├── order-module/ │ ├── pom.xml │ └── src/main/java/ │ ├── OrderApplication.java │ ├── controller/ │ ├── service/ │ └── repository/ ├── user-module/ │ ├── pom.xml │ └── src/main/java/ │ ├── UserApplication.java │ ├── controller/ │ ├── service/ │ └── repository/ └── common-module/ └── pom.xml

在这个结构中,order-module 和 user-module 虽然是两个 Maven 模块,但它们仍然可以在同一个进程里运行。模块之间的调用可以通过接口完成。当业务量增长到需要单独扩容订单服务时,只需要把 order-module 独立打包部署,并把它对 user-module 的内部调用改成远程调用即可。

这种过渡方式的优势是:拆分过程中任何一步出问题,都能快速回退,不会造成一次性大重构。

4.4 拆分顺序:从依赖简单、边界清晰的服务开始

拆服务也不是随机的。推荐的策略是优先拆分边界清晰、被依赖方少、数据独立性强的服务。例如用户服务、基础数据服务,这类服务通常是“被所有人都依赖”的基础服务,但它们本身的业务逻辑相对独立,拆出来收益很大。

相反,像订单这种处于整个交易流程核心、依赖大量其他服务的业务域,应该放到后期拆分。因为它的依赖关系复杂,拆分时如果其他服务还没拆好,可能既要依赖用户服务,又要依赖支付服务,还要保留对单体内部模块的调用,这会让过渡期变得异常漫长。

4.5 拆分时的粒度控制

微服务粒度没有标准答案。常见的经验是:一个服务应该由一个不超过 8 到 10 人的小团队独立维护。如果一个服务需要两个以上团队频繁协调才能上线,拆分就是失败的。如果一个服务内部已经包含多个不相关的业务能力,那它还可能继续拆。

粒度控制的核心原则是“高内聚、低耦合”。同一个服务内部的模块应该有强相关性,服务与服务之间的依赖应当尽可能少并且清晰。

5. 拆完之后,真正的挑战才开始

很多团队以为拆分完成就等于微服务改造结束,实际上拆分只是把问题从“代码层”转移到了“系统层”。服务拆开后,大量新问题会浮出水面,如果没有治理能力,迁移后的系统反而可能比单体更不稳定。

5.1 调用链与性能问题

单体架构中一次下单操作,可能只是方法之间的多次调用;而在微服务架构中,这些方法调用变成了服务之间的 RPC 调用。一次请求可能要经过网关、用户服务、订单服务、支付服务,链路变长后,任何一环的超时、重试、网络抖动都可能影响整体响应时间。

因此,拆分之后首先要治理的是超时和重试策略。某个下游服务出现性能波动时,不能让上游无限制地等待。常见的做法是为每次远程调用设置合理的超时时间,并限制重试次数。重试本身也是一把双刃剑:在下游服务已经处于高负载状态时,大量的重试反而可能把服务压垮,导致雪崩。

5.2 分布式事务与数据一致性

单体架构中,一个事务方法里可以同时更新订单表、扣减库存表、写支付流水表,数据库的本地事务保证了这些操作的原子性。服务拆分后,这些表可能分属不同的服务,本地事务不再覆盖所有操作,于是“分布式事务”成为必须面对的问题。

分布式事务的解决方案有很多,例如两阶段提交(XA)、TCC(Try-Confirm-Cancel)、SAGA、本地消息表等。它们的共同点都是:用复杂的工程手段,去弥补分布式环境下事务原子性的缺失。

在实际业务中,并不是所有操作都需要强一致性。对于支付、库存这类对一致性要求极高的场景,通常采用 TCC 或可靠消息事务;对于订单状态更新、物流状态更新这类场景,最终一致性往往已经足够。设计时要避免一个误区:不要试图让所有微服务调用都达到本地事务般的强一致性,那样会引入极高的复杂度。

5.3 配置管理与环境治理

单体架构只有一套配置,而微服务拆出来后,每个服务都有自己的一套配置,并且还分为开发环境、测试环境、预发布环境、生产环境。如果配置仍然通过修改配置文件的方式来管理,配置的修改和发布将变成灾难。

配置中心是微服务治理的重要基础组件。以 Nacos 为例,一个典型的配置管理接入方式如下。首先是引入依赖:

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>

然后在 bootstrap.yml 中配置 Nacos 地址:

spring: application: name: order-service cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml namespace: dev

服务启动后,会从 Nacos 配置中心拉取以order-service.yaml命名的配置。当配置中心中的配置变更后,通过@RefreshScope注解可以动态刷新部分 Bean,而不需要重启服务。

配置中心的核心价值不只是集中管理配置,还包括:配置版本管理、灰度发布、权限控制、变更审计。生产环境的配置变更,尤其是涉及数据库连接、线程池参数这类高影响配置时,一定要支持回滚,并且尽量先灰度到少量实例验证。

5.4 接口版本管理与兼容性

服务拆分后,不同服务由不同团队维护,服务接口的演化步调不可能完全一致。一个服务升级了接口,另一个服务如果还在调用旧接口,就可能出现兼容性问题。

接口版本管理有几个常用手段:在 URL 路径中带版本号,例如/api/v1/order/api/v2/order;在请求头中标记版本;或者在扩展字段中以增量方式做兼容。对于大多数内部服务,比较推荐的方法是“向后兼容式扩展”:新增字段时给默认值,不删除旧字段,不改动旧字段含义。这样下游服务无需同步升级也能继续运行。

5.5 可观测性建设

单体架构排查问题时,直接看一个应用的日志就够了。微服务排查问题时,一个请求可能横跨多个服务,如果只靠每个服务单独的日志,根本排不出来。

可观测性通常包括三块:日志、指标、链路追踪。日志可以按 traceId 串联一次请求的完整日志,指标监控服务的 QPS、响应时间、错误率、CPU、内存等,链路追踪则用来分析一次请求在各服务之间的耗时分布。这三者是微服务环境下排查问题的基本支撑,缺少任一块,线上故障的恢复时间都会明显拉长。

6. 微服务治理的关键组件一览

微服务不是“把服务拆开部署”这么简单,拆完后的服务治理才是核心工作量。下面是一组常见的治理组件及其作用。

组件核心作用常见技术方案
服务注册与发现服务实例动态注册、动态感知上下线Nacos、Consul、Eureka
配置中心配置集中管理、动态刷新、灰度发布Nacos、Apollo
API 网关统一入口、路由转发、鉴权、限流Spring Cloud Gateway、Kong
负载均衡与容错服务调用负载均衡、熔断、降级、限流Spring Cloud LoadBalancer、Resilience4j、Sentinel
链路追踪调用链分析、耗时定位、故障排查SkyWalking、Zipkin、Micrometer Tracing
容器化与编排环境一致性、弹性伸缩、部署编排Docker、Kubernetes
消息队列异步解耦、削峰填谷、最终一致性RocketMQ、RabbitMQ、Kafka

以一个简单的订单服务为例,服务启动后需要注册到注册中心。下面是接入 Nacos 注册中心的配置示例:

spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848

服务之间调用时,使用 OpenFeign 声明式 HTTP 客户端可以简化调用代码:

// 文件路径:src/main/java/com/example/order/client/UserClient.java @FeignClient(name = "user-service") public interface UserClient { @GetMapping("/user/info/{userId}") UserInfo getUserInfo(@PathVariable("userId") Long userId); }

调用方引入依赖:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>

在网关层,通过 Spring Cloud Gateway 做路由转发,将/api/order/**的请求转发到 order-service:

spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path=/api/order/**

这些组件共同构成了微服务的基础设施。需要说明的是,组件并不是越多越好,每种组件都有学习和运维成本,应根据团队规模和实际需求选择。

7. 中小团队该怎么办:避免为了微服务而微服务

7.1 什么时候不要做微服务

这是一个非常值得写在前面的建议:如果你的团队人数不多、业务复杂度不高、单机数据库连接没有告警,那么不建议为了“技术先进”或者“简历上更好看”而强行拆微服务。

微服务带来的成本是明确的:基础设施成本、网络通信成本、部署运维成本、排查问题成本。如果单体系统的性能瓶颈已经可以通过缓存、索引优化、应用拆分、读写分离来解决,那优先做这些优化会远比拆微服务更划算。

7.2 拆分前必须想清楚的三个问题

第一个问题:拆分的目的是什么?是为了提升团队迭代效率,还是为了解决数据库连接瓶颈,还是为了独立伸缩某个模块?目的不同,拆分的方案也会完全不同。

第二个问题:数据如何处理?如果业务表的归属无法明确划分,服务拆分之后依然共享数据库,那真正的服务边界始终没有建立起来。

第三个问题:是否有配套的可观测性和自动化发布能力?没有链路追踪、没有配置中心、没有 CI/CD,拆出来的服务越多,运维负担越大。微服务不是一个单纯的组织结构问题,它需要一整套工程基础设施来支撑。

7.3 小团队落地的推荐路径

如果评估后确实有微服务化的必要,建议按下面的路径逐步演进:

  1. 先把单体内部做模块化改造,明确模块边界,强制模块间通过接口通信。
  2. 引入网关,作为统一入口,先实现认证、限流、日志等横切能力。
  3. 将最独立、最痛点的模块拆成独立服务,单独部署,通过网关路由。
  4. 等独立服务稳定运行后,再按业务优先级逐步拆分其他模块。
  5. 在拆分过程中同步完善配置中心、注册中心、链路追踪和 CI/CD 流水线。

这套路径的核心思想是:每一步都解决一个具体问题,每拆一个服务都能衡量收益。而不是一次性把整套微服务架构从零搭好再迁移。国内很多项目使用 Spring Cloud Alibaba 这套技术栈,它把注册中心、配置中心、网关、熔断降级等都整合得比较完善。还有像若依微服务 plus 这类开源脚手架,也适合团队在二次开发时参考,但要注意,开源脚手架往往带有自己的设计约定,接入前需要评估与业务的匹配度,不能为了“跑起来”而盲目引入。

8. 微服务改造的高频问题与最佳实践

8.1 高频问题排查表

问题现象常见原因解决思路
服务调用超时下游服务过载、慢 SQL、线程池阻塞先检查下游日志和指标,定位瓶颈;再调整超时时间与重试策略
服务间数据不一致分布式事务方案缺失或使用不当梳理事务边界,对关键链路引入 TCC、可靠消息或本地消息表
配置修改不生效未引入配置中心或未加动态刷新注解确认服务正确接入配置中心,检查@RefreshScope是否生效
接口变更导致其他服务报错接口版本管理缺失,下游未升级接口设计时预留版本号,变更遵循向后兼容原则
链路追踪无数据未注入 traceId 或未接入追踪组件检查日志、埋点与链路追踪组件是否贯穿服务之间
重复代码泛滥公共库边界不清晰将通用能力沉淀为独立公共库,明确版本管理责任
服务拆后反而变慢过度拆分导致远程调用过多评估服务间调用频率,必要时合并服务或引入缓存

8.2 工程最佳实践

在微服务架构的工程实践中,有几个特别值得注意的细节。

第一,接口规范要提前定。服务间通信的协议、错误码规范、请求追踪 ID 的传递方式,最好在拆分前就约定好。如果每个服务各自定义一套返回格式和错误码,联调成本会呈指数级上升。

第二,配置变更要走审批与灰度流程。尤其是生产环境,任何配置变更都可能影响线上实例。配置中心本身提供了权限控制能力,应当启用。无论是连接池参数、开关配置还是限流阈值,修改前都要先在灰度环境或少量实例验证。

第三,数据库变更要谨慎。微服务环境下,每个服务的数据库由各自团队维护,跨库查询非常困难。修改表结构、新增索引、清理数据前,建议先在测试环境验证,并保留备份。任何涉及生产库的变更,都要遵循“备份优先、灰度执行、失败回滚”的原则。

第四,故障演练要常态化。微服务越多,故障模式也越多。网络抖动、下游宕机、慢接口、磁盘写满、配置错误,这些情况都应该通过故障演练提前验证。只有演练过熔断降级,线上真的发生故障时才不会手忙脚乱。

第五,关注幂等与重试。服务间调用失败后自动重试,但重试会带来重复请求的问题。订单创建、支付回调这类操作,必须保证接口幂等。常见的做法是引入全局唯一请求号,服务端基于请求号做去重处理。

9. 下一步学习路线

微服务架构这个话题非常庞大,不可能在一篇文章里讲完所有的深度细节。如果读到这里,你希望继续深入,可以按下面的顺序展开学习。

第一步,先把单体架构的模块化设计学好。理解高内聚、低耦合,理解依赖倒置和接口隔离,这是服务拆分的基本功。一个在单体里都分不清模块边界的人,直接做微服务只会把混乱扩散到多个进程。

第二步,学习分布式系统的基础概念。包括 CAP 定理、BASE 理论、分布式事务、幂等设计、分布式 ID、分布式锁。这些概念是微服务架构的理论基础,遇到实际问题时,需要依靠这些原理来判断方案是否合适。

第三步,选择一套微服务技术栈深入实践。从 Spring Cloud Alibaba 入手是比较顺畅的路径,因为它包含注册中心、配置中心、网关、熔断等完整组件,且文档相对完善。学习时不要只跟着教程跑通 Demo,要理解每个组件解决什么问题、有哪些替代方案、在什么场景下会失效。

第四步,把可观测性和自动化发布能力补齐。没有监控的微服务系统就像盲人开车,根本不知道故障在哪里。弄清楚日志聚合、指标采集、链路追踪三者的配合,学会通过 traceId 定位一次跨服务请求的完整路径,这才是微服务排障的基本功。

如果你现在正站在“要不要拆分”的十字路口,有一点最想强调:微服务是一个由业务增长推动的演进过程,而不是一个靠架构师在项目启动前画出来的蓝图。先找到当前系统最痛的点,再让架构向解决痛点方向演进。当你的系统规模还不需要微服务时,好好维护单体同样是很优秀的技术决策。

希望这篇文章能帮你理清微服务架构演进背后的逻辑,也能为你在实际项目中的架构决策提供一些参考。如果你正在经历微服务改造,欢迎在评论区聊聊你在这个过程中踩过哪些坑。

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

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

立即咨询