☰
从单体到微服务:架构演进的实践与避坑指南
2026/10/16 3:31:51 网站建设 项目流程

好端端一个单体应用,为什么要拆成微服务?这个问题我在各种技术讨论里听过无数次,自己也完整经历过一轮微服务架构演进历程。我们当时那个项目是一个典型的电商后台系统,早期就是一个大单体:几千个方法、几十张核心表、一个MySQL实例,日常开发、发布、扩容全都围着同一个仓库打转。后来业务量上来了,痛点一个一个爆出来,我们才被逼着走上服务化这条路。这篇文章把我踩过的坑、走过的弯路、总结出来的经验,原原本本写出来。不管你是还在单体阶段纠结要不要拆,还是已经在服务化过程中被各种分布式问题折磨,这篇应该都能给你一些参考。

1. 单体架构的痛点:好端端为什么要拆

1.1 单体架构的隐性成本

单体应用从0到1阶段是最务实的方案,快速上线、调试方便、事务天然一致,团队几个人写起来效率很高。但规模上来之后,问题就陆续出现了。

最明显的就是发布成本。我们当时迭代节奏是每周发布一次,每次发布前测试都要做全量回归。为什么?因为没人能拍胸脯说改了一个模块不影响另一个模块。代码层面各种包互相调用,经常出现改订单状态的时候不小心影响到了用户模块的情况。影响面分析全靠人肉,改代码的人自己都说不清这次改动到底会波及到哪里。

还有个更隐蔽的问题:技术债务被不断放贷。早期的单体代码没有严格的边界约束,大家都往公共类里塞方法,各种Utils、Helper包越来越膨胀。等你想拆的时候才发现,这些公共代码早就被所有模块引用了,牵一发动全身。我那时候最怕听到的一句话就是:这个需求风险评估一下,看看影响哪些人。影响面分析全靠人肉。

数据库层面也好不到哪去。单库撑不住业务增长,连接池被大量慢SQL占满,经常出现某个报表查询把数据库连接池耗尽,导致线上订单服务跟着变慢甚至超时。你想单独给订单模块扩容?不行,因为它是单体,要扩就是整个应用一起扩,连带着所有无关模块全部重启。

1.2 什么时候该拆:三个典型信号

不是所有项目都要微服务,我在很多场合表达过这个观点。一个写内部工具的小系统,硬拆成微服务就是给自己找麻烦。但如果你遇到了下面这三个信号,就要认真考虑服务化改造了。

第一个信号是团队规模。当研发团队超过20人,大家都在同一个仓库里提交代码,合并冲突频繁,发布排期互相牵制,这时候单体就成了协作的瓶颈。我见过一个团队,十多个开发同时维护一条主干分支,每天光解决冲突就要花掉几个小时。

第二个信号是模块之间没有清晰边界。改一个功能需要同时动三个模块的代码,而且这些模块还互相调用、互相依赖。这说明业务边界已经模糊了,你在代码层面已经很难理清谁该对什么负责。

第三个信号是有独立扩容的需求。比如某个模块的流量是其他模块的十倍,你想针对性地给这个模块加机器,但如果它们是同一个应用,你就只能整个应用一起扩,白白浪费资源。数据库也同理,某个模块的表量特别大,整个单库都被它拖累。

我整理过一个简单的判断表格,分享给团队评估用:

评估维度适合单体适合微服务
团队规模10人以内20人以上
业务复杂度低,模块少高,边界清晰度要求高
流量特征均衡,增长平缓模块间差异大,有突发流量
发布频率低,每周甚至更低高,多个团队独立迭代
运维能力一般有独立运维或基础设施团队

注意,即使满足微服务的条件,也建议大家先做模块化单体,把代码层面边界理清楚再谈拆服务。直接对一个大泥球做外科手术,很容易拆出一个分布式大泥球。

1.3 数据库拆分:最痛的一刀

很多团队拆分服务时,服务代码拆得挺顺利,但一碰到数据库就卡住了。因为服务拆了,数据库还是那个库,所有服务都连同一个数据库,连接数、表级锁、慢SQL的问题根本没有解决,只是把问题从应用层挪到了数据库层。

我们当时分库的策略是:先拆代码、后拆库;先只读、后写入;先异步、后同步。一共走了三步。第一步先把服务之间的数据库访问改成接口调用,这个阶段数据库还保持原样,但代码层面已经不再跨服务直接查表了。第二步把读多写少的表做只读副本,比如商品信息、营销活动配置,这些表被多个服务读取,先把它复制到各自的服务库中。第三步才动核心的写库,比如把订单表从主库迁到独立的订单库,这一步最危险,因为大量SQL语句里有跨表join,拆完之后这些SQL都得改造。

举个具体例子:原来一个联表查询,能从订单表和用户表里一次取回订单信息和用户昵称,拆库后订单服务先查本地库得到user_id,再通过HTTP调用用户服务批量获取用户信息。第一次改造完性能惨不忍睹,循环调用用户服务产生了大量RTT。后来加了批量查询接口,把一百个user_id一次传过去,才把性能拉回来。这种改造的细节非常多,建议拆库前先把SQL清单梳理出来,逐个评估改造方案。

2. 微服务化的完整落地:从拆分到技术选型

2.1 拆分原则:按业务域拆,不按技术层拆

拆分服务最忌讳的是按技术层拆。我见过有人把一个应用拆成Controller服务、Service服务、DAO服务三个项目,美其名曰分层微服务,实际效果就是多了一堆网络开销,还把一个好好的事务边界切得稀碎。

正确做法是按业务域拆。我当时跟团队讲:你要拆的是业务边界,不是让你的Controller、Service、DAO各住一个房间。我们最终按照电商的核心领域,拆成了用户、订单、商品、支付、库存、营销六个业务服务,另加一个网关层。为什么不把用户和订单合并?因为在我们的业务模型里,用户域关心的是账号、身份、积分,订单域关心的是订单状态流转、支付结果核对,两者的生命周期完全不同,合并在一起只会互相干扰。

拆分时还要注意一件事:识别出那些被多个服务依赖的东西。我们当时有个公共的用户鉴权逻辑,被几乎所有服务引用。这个逻辑我们没有让它变成一个服务被反复调用,而是做成了网关层的统一鉴权,把Token解析和用户基本信息校验下沉到网关,业务服务自己只做业务鉴权。这样既保证了跨服务的用户信息可用,又不会因为鉴权服务挂了导致全网瘫痪。

2.2 注册中心和配置中心选型

服务拆出来之后,第一个要解决的就是服务发现和配置管理问题。服务实例的IP地址是动态变化的,不可能在代码里写死,必须有一个注册中心让大家互相找到。我们当时对比了几个方案,每个都有坑。

方案CAP模型维护成本配置管理我们的结论
EurekaAP低,但已停更维护需要额外接配置中心首先排除
ConsulCP中等自带KV,但使用体验一般社区资料少,放弃
ZookeeperCP高,运维复杂需要额外接配置中心不适合做注册中心
NacosAP/CP可切换低自带,与注册中心一体最终选择

这里有一个核心的架构决策值得展开说说。注册中心这个角色,应该选AP还是CP?我的理解是:注册中心追求的是高可用,服务提供方短暂不健康可以接受,但不能因为注册中心自己挂了导致整个集群不可用。Zookeeper是强一致模型,在极端情况下为了选主会短暂不可用,这对服务发现来说是致命的。而Nacos默认AP模式,注册列表短暂不一致不会影响服务间调用,同时还能切换CP模式处理配置这种强一致场景。一个组件同时搞定注册和配置,我们最终选它还有一个现实原因:Nacos自带控制台,配置灰度发布、命名空间隔离这些功能开箱即用。

2.3 服务注册发现与网关配置改造

服务接入Nacos很简单,核心就是引入依赖、配置地址。以我们当时的Spring Cloud技术栈为例,每个服务只需要在自己的配置文件里加上:

spring: application: name: order-service cloud: nacos: discovery: server-addr: nacos-cluster:8848 namespace: prod config: server-addr: nacos-cluster:8848 file-extension: yaml

注意namespace这里,一定要按环境隔离。我们一开始没分namespace,测试环境和生产环境的服务注册在同一个集群里,出现过服务间调用串环境的问题,排查了半天才发现是注册中心命名空间没做隔离。生产、测试、开发各建一个namespace,服务注册到各自的namespace下,调用的时候只会发现同namespace的实例。

网关我们用的是Spring Cloud Gateway。选它而不是Zuul 1.x,核心原因是性能:Gateway基于WebFlux响应式编程,底层是Netty,高并发下比Zuul 1.x的阻塞IO模型好太多。路由配置类似这样:

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

这里有个容易踩的坑:路由条件如果配置得太宽泛,两个服务的路径前缀重叠,请求就会随机进错服务。我们线上出过这个故障,排查了半天才找到原因是一个路由的Path少了精确匹配。后来的规范是:每个服务的前缀必须全局唯一,所有业务服务路径必须以/api/{service-name}开头,网关路由表统一由架构组维护,不允许业务开发自行修改。

2.4 服务间调用方式的细节

服务间的同步调用,我们选择了OpenFeign + Ribbon负载均衡的组合方式。OpenFeign的好处是声明式HTTP客户端,接口定义直观,配合服务发现后自动做负载均衡。但用起来有几个细节必须注意。

第一个是超时设置。OpenFeign默认的超时时间非常短,如果被调用的服务处理超过1秒,直接超时报错。当时我们把订单服务的一些查询逻辑放到OpenFeign里调用商品服务,上线后频繁超时,就是因为默认连接超时和读取超时都设置得太小。后来在配置文件里统一调整为连接超时3秒,读取超时10秒,才算稳定下来。

@FeignClient(name = "product-service", path = "/api/product") public interface ProductClient { @PostMapping("/batch-query") Result<List<ProductDTO>> batchQuery(@RequestBody ProductQueryRequest request); }

第二个是超时重试的陷阱。我们最初配置了请求失败自动重试,但没考虑到下游接口的幂等性。某个生成订单的接口,因为网络抖动重试了一次,结果用户被创建了两笔订单。分布式环境下,网络抖动是常态,重试策略必须和幂等设计结合,这个我们在第三章详细讲。

3. 拆完之后的一地鸡毛:分布式经典问题

3.1 分布式事务:最让人头疼的硬骨头

服务拆了,原来单体里一个事务就能解决的问题,现在变成了跨服务的一致性问题。最典型的就是下单:扣库存、生成订单、扣优惠券、加积分,四个操作分布在四个服务里,任何一个失败都会导致数据不一致。

曾经有人试图用两阶段提交(XA)方案解决,但我们评估后放弃了。XA强一致事务性能开销太大,整个事务期间要锁住所有参与方的资源,高并发场景下根本撑不住,而且它要求所有参与的数据库都支持XA协议,很多NoSQL组件根本做不到。我们最终采用的是最终一致性方案——本地消息表 + 消息队列 + 定时任务补偿。

本地消息表的设计思路是这样的:业务操作和消息记录放在同一个本地事务里。比如订单服务创建订单的时候,同时往本地消息表插入一条状态为PENDING的消息记录,这两个操作在一个数据库事务里,要么一起成功,要么一起失败。消息表写入成功后,一个独立的任务把PENDING状态的消息定时扫描出来,推送到消息队列;下游服务消费消息后执行自己的业务,成功后回调一个确认接口,把消息状态改为SUCCESS。如果消费失败,消息还在消息表里,定时任务会不断重试,直到成功。

消息表的核心字段我列一下:id、biz_id(业务唯一ID)、biz_type(业务类型)、message_content(消息体)、status(PENDING/SENDING/SUCCESS)、retry_count(重试次数)、next_retry_time(下次重试时间)。设计这个表的时候一定要加biz_id的唯一索引,这是幂等消费的基础。

这里有一个很多人容易忽略的点:不是所有业务都需要分布式事务。能异步就异步,能降级就降级。比如加积分这个动作,完全可以先跳过,等订单完结后再异步补上。我们后期把大量同步的分布式事务改成了异步事件通知,系统稳定性提升非常明显。分布式事务是最后的手段,不是默认的选择。

3.2 链路追踪:从接到一起到一次排查

服务变多了以后,最痛苦的是排查问题。以前单体应用出问题,看日志定位代码位置就行;微服务架构下,一次请求要经过网关、订单服务、支付服务、消息队列等好几个环节,每个环节都可能出问题,日志散落在不同的机器上,排查起来非常费劲。

我们当时做了一个决定:可观测性建设从第一天就开始做。链路追踪选了SkyWalking,核心原因是接入成本低——它是Java Agent方式,通过-javaagent参数启动应用即可,不需要业务代码做任何埋点改造。对比过Zipkin和Jaeger,Zipkin需要业务代码里手动注入span,侵入性太强,团队推行起来抵触情绪很大;SkyWalking的自动探针模式大幅降低了接入门槛。

链路追踪解决的第一个问题是慢接口定位。以前接口慢,你只能从头到尾看日志猜测,现在直接看调用链路图,所有环节的耗时一目了然。有一次线上接口变慢,我们通过SkyWalking看到耗时主要集中在用户服务的某个查询上,进一步排查发现是用户表新增了一个索引导致查询计划改变,回退索引后问题解决。没有链路追踪,这类问题可能要排查几小时。

第二个问题是日志串联。我们把traceId打印在每一条日志里,日志组件配置里加一个Pattern:

<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>

这样一条业务请求从网关到最终落库的所有日志,都能通过traceId关联起来。然后再把日志接入统一的日志平台,排查问题时一个traceId搜到底,效率直线上升。这里有个细节:Gateway网关在入口生成traceId后,要用MDC的方式写入日志上下文,并且通过Header传递下去,下游服务在入口处读取该Header并重新放入MDC,整条链路才能贯穿。

3.3 幂等设计:分布式系统的安全网

微服务环境下,网络超时、重试、消息重复投递几乎无法避免,幂等设计不是可选项,是必选项。我们的原则是:所有写接口必须幂等,所有MQ消费逻辑必须幂等。

幂等设计的三步走:幂等键、状态判定、唯一约束。幂等键由调用方生成,比如用户ID+业务类型+业务流水号,在创建订单时随请求传入;服务端收到请求后先查幂等表,如果存在相同键且状态是已完成,直接返回已有结果,不再重复执行;同时在业务表上加上唯一索引做兜底,即使并发请求同时进来,数据库层面也会拒绝重复数据。

MQ消费的幂等处理,我们用的是Redis的SETNX命令。消费消息时先尝试在Redis里写入消息ID,写入成功说明是首次消费,执行业务逻辑;写入失败说明之前已经消费过,直接丢弃。这里需要注意的是,业务逻辑执行完毕后要设置一个过期时间,防止消息ID永久占内存。

我见过一个实际案例:某团队做积分服务,消费订单完成消息时没有做幂等,结果消息队列重试机制投递了三次,用户积分被加了三次。排查的时候他们还在纠结是哪个环节重复发消息,实际上问题就出在缺少消费幂等。这类问题在分布式系统里特别典型,大家一定要引以为戒。

3.4 熔断限流降级:防止雪崩的最后一道防线

全链路雪崩是微服务架构里最恐怖的场景。一个服务变慢,调用方线程被占满,跟着变慢,然后继续传染到上一级,最终整个系统崩溃。我们自己也遇到过:商品详情接口里同步调用了营销服务、库存服务、评价服务,营销服务一次大促活动上线后处理变慢,商品服务的线程池全部被它占满,整个服务被拖垮,最后所有依赖它的上游服务也跟着出问题。

解决雪崩问题,我们对比了Hystrix和Sentinel,最后选了Sentinel。Hystrix的问题在于官方已经停止维护,线程池隔离的开销过大,而且它的控制台功能很弱,动态规则配置体验也不好。Sentinel使用滑动窗口算法做流量统计,内存开销比Hystrix的信号量模式还小,规则可以通过控制台动态推送。

Sentinel接入后,我们给核心接口配置了限流规则,熔断规则和降级规则。给下游一个合理的超时时间,超时直接走降级逻辑,不让线程在这里傻等。同时设置了一个关键参数:如果接口调用失败率超过20%,熔断器打开,后续请求快速失败不再实际调用。就这么一条规则,就把商品服务从雪崩边缘拉了回来。

Sentinel的规则配置可以通过控制台动态推送,不需要重启服务。比如我们给下单接口配的限流规则就是这样:

[ { "resource": "order-create", "count": 1000, "grade": 1, "limitApp": "default" } ]

resource是接口路径,count是QPS阈值,grade=1表示按QPS限流。规则热更新后几秒钟即可生效。

4. 从微服务到云原生:架构继续演进

4.1 容器化与编排:微服务的最后一块拼图

微服务如果没有容器化编排,运营成本会高到让人崩溃。服务拆到五六个以后,我们手工维护这些服务的发布、扩缩容、健康检查已经非常吃力。当时每个服务一套脚本,环境变量靠手工维护,发布效率低,回滚更是噩梦。直到引入Docker和Kubernetes,一切才发生质变。

容器化带来的第一个好处是环境一致性。镜像打包好了,测试环境和生产环境跑的是同一个东西,什么环境差异问题都消失了。第二个好处是资源隔离和弹性伸缩,每个服务的内存、CPU可以独立配置,某个服务流量上来了,Kubernetes的HPA可以自动加副本,流量降了自动缩容,不需要人工干预。

有一个观点我觉得值得分享:没有Kubernetes的微服务是伪微服务。你拆了服务,却没有编排能力,就只能手工管理进程,服务发现、负载均衡、健康检查这些能力全部要自己用代码来实现,效果还不如不拆。

我们在Kubernetes上跑了一个服务到底的最小部署单元:每个服务一个Deployment负责副本管理,一个Service负责集群内负载均衡,一个ConfigMap统一管理环境配置。发布流程通过Git提交触发流水线,构建镜像、推镜像库、滚动更新一气呵成。发布频率从原来的每周一次提高到每天多次,而且有自动回滚机制,发布失败自动回退到上一个版本。

4.2 服务网格:有机会试一试但没全面上

服务网格这几年很火,特别是Istio。它的思路是把服务间通信能力下沉到基础设施层,通过Sidecar代理拦截所有网络流量,把熔断、限流、负载均衡这些能力统一管理起来。听起来很美好,但我们在试用过程中遇到了不少现实问题。

控制面复杂度非常高。Istio的组件很多,部署、升级、调优都需要专门的知识,团队如果不能配备专职的云原生运维人员,很容易陷进去。我们当时试运行了两个月,光Istio自身的故障排查就花了不少时间,Sidecar的CPU和内存开销也让部分高吞吐接口的性能出现了下降。

我们的结论很务实:服务网格适合服务规模庞大、基础设施团队成熟的架构;对于中小团队,直接用Kubernetes原生能力加轻量级框架反而更稳。最终Istio只在部分非核心链路上做了试点,没有全面铺开。技术选型不能只看概念热度,更要看团队能不能接得住。

4.3 中台化:一次来自现实的反思

微服务演进到一定阶段,很多团队会想往中台方向走,把公共能力沉淀出来,形成用户中台、支付中台、数据中台等。这个方向有它的道理,但也存在着一个常见陷阱:中台很容易做成一堆被所有业务方共享的大库,重新变成一个庞大的单体。

我见过一些中台项目,边没有切开,最后做成一个被所有业务方依赖的集权中心。改一个字段要通知几十个业务方,跨团队的会议开个不停,服务之间的调用链路复杂到没人能说清。中台化如果处理不当,比单体架构还难维护。

我们的经验是:中台的边界要来自领域分析而不是组织架构。做中台之前先厘清哪些能力真正具有通用性,哪些是不同业务方的特殊逻辑。通用性强的能力如用户身份、支付通道可以中台化,但中台本身也要讲究服务自治,每个中台能力应该是一组高内聚低耦合的服务,而不是一个巨大的共享数据库。用我当时跟团队讲的一句话:中台是微服务的规划方式,不是回归单体的借口。

4.4 Serverless时代的微服务形态

微服务架构走到今天,已经不再是唯一的高级形态。Serverless的出现让一部分场景可以采用更极致的模式:函数计算、按调用计费、自动弹性,业务代码完全不用关心服务器。

我们的实践是把一些计算密集型的离线任务拿出来做了函数化改造。比如每天凌晨的报表统计,原先跑在常驻服务的定时任务里,占用内存资源;改造后放到函数计算平台,按次计费,跑完即释放,成本降了不少。还有一个突发流量的营销活动,入口用函数计算做流量整形,峰值来了自动扩容,峰值过了自动缩容。

我这里想说的是:微服务并没有被Serverless取代,而是下沉了。以前你要自己管理服务实例、配置弹性策略,现在这些被云平台承接了。微服务是一种架构思想,Serverless是一种部署形态,两者可以结合。我们现在的策略是:核心业务链路用Kubernetes跑微服务,边缘逻辑用Serverless函数处理,各取所长。

5. 给准备做架构演进的团队几条实在建议

5.1 先治理后拆分

很多团队兴冲冲地开始拆服务,结果拆出自己的问题。我见过最惨的例子是:一个电商平台,半年内把单体拆成了20多个微服务,结果服务间调用关系比原来的模块还乱,线上故障频率比拆分前翻了倍。原因很简单,拆分前没有先把模块边界理清楚,拆的时候只追求服务数量多,把关联逻辑强行断开。

我的建议是:拆分前至少要做一轮依赖分析,把核心业务域划分清楚,再画一张目标架构图。你会发现很多模块其实没必要拆成独立服务,它在代码层面做好模块隔离就够了。先把单体的包结构、目录结构优化好,再逐步演进,这个过程也叫模块化单体,是很多团队最终选择的折中方案。

5.2 可观测性必须先行

链路追踪、指标监控、日志系统这三件事,必须在拆分服务之前就建好。没有可观测性,服务一拆,出现问题你连从哪里查起都不知道。

我们当时的教训是:拆服务拆到第三个的时候,线上出了一个偶发超时问题,结果日志都散落在不同服务,没有一个全链路视角工具,整整排查了一天。最后发现是某个服务的一个线程阻塞了,但因为日志无法串联,光定位就花了大量时间。后来装了链路追踪,这类问题的排查效率提升了数倍。

5.3 微服务基础设施清单

如果团队决定要微服务化,我建议对照这个清单评估自己还缺什么:

功能域必备组件说明
服务发现与配置注册中心、配置中心建议一体化选型
流量治理网关、熔断限流组件网关统一入口,限流防雪崩
可观测性链路追踪、日志平台、指标监控三项缺一不可
自动化交付CI/CD流水线、容器镜像仓库发布自动化是生命线
数据一致性分布式事务方案、幂等框架消息表、重试补偿机制

基础设施不齐,不建议贸然拆服务。很多人问我说微服务应该从哪个团队开始,我的答案永远是:先建基础设施团队或者至少明确一位架构师专门负责这些事情。

5.4 别把微服务当银弹

写了这么多,我最想强调的还是那句老话:微服务不是银弹。如果你的业务逻辑本身复杂、领域边界不清晰,拆成微服务只会把复杂度从代码层面转移到网络层面,反而更难处理。

对一些中小团队来说,单体加模块化,配合CQRS读写分离,可能已经能解决大部分问题。微服务最大的价值不在于技术炫技,而在于它逼着你思考架构治理:服务边界在哪里,依赖关系是什么,如何保证数据一致性。就算你最后没有真的拆成很多个服务,过程中建立的架构意识也会让团队受益匪浅。

最后分享一个我的真实体验。这些年走下来,我觉得微服务架构演进最打动我的不是那些技术组件,而是它让整个团队不得不把以前靠默契、靠口头约定、靠代码规约维持的事情,变成了显式的架构治理。服务之间不允许随便互相调用,数据不允许随便跨境查询,配置变更必须走审批,接口必须有版本兼容。这些规则在单体时代几乎没人执行,但在微服务架构下,不执行就是灾难。不管你最后选择什么架构风格,这种边界意识和治理意识,我认为才是真正值钱的东西。如果你也正在评估要不要动架构,我的建议是:先想清楚你现在遇到的问题是什么,是模块耦合、是团队协作、还是扩容瓶颈,再根据问题去匹配架构方案。适合你的,就是最好的。

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

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

立即咨询