资讯动态

从单体到微服务:架构演进的实践与避坑指南

发布时间:2026/10/16 3:32:06 来源:尧图企业网站定制
好端端一个单体应用为什么要拆成微服务这个问题我在各种技术讨论里听过无数次自己也完整经历过一轮微服务架构演进历程。我们当时那个项目是一个典型的电商后台系统早期就是一个大单体几千个方法、几十张核心表、一个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: - StripPrefix1这里有个容易踩的坑路由条件如果配置得太宽泛两个服务的路径前缀重叠请求就会随机进错服务。我们线上出过这个故障排查了半天才找到原因是一个路由的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) ResultListProductDTO batchQuery(RequestBody ProductQueryRequest request); }第二个是超时重试的陷阱。我们最初配置了请求失败自动重试但没考虑到下游接口的幂等性。某个生成订单的接口因为网络抖动重试了一次结果用户被创建了两笔订单。分布式环境下网络抖动是常态重试策略必须和幂等设计结合这个我们在第三章详细讲。3. 拆完之后的一地鸡毛分布式经典问题3.1 分布式事务最让人头疼的硬骨头服务拆了原来单体里一个事务就能解决的问题现在变成了跨服务的一致性问题。最典型的就是下单扣库存、生成订单、扣优惠券、加积分四个操作分布在四个服务里任何一个失败都会导致数据不一致。曾经有人试图用两阶段提交XA方案解决但我们评估后放弃了。XA强一致事务性能开销太大整个事务期间要锁住所有参与方的资源高并发场景下根本撑不住而且它要求所有参与的数据库都支持XA协议很多NoSQL组件根本做不到。我们最终采用的是最终一致性方案——本地消息表 消息队列 定时任务补偿。本地消息表的设计思路是这样的业务操作和消息记录放在同一个本地事务里。比如订单服务创建订单的时候同时往本地消息表插入一条状态为PENDING的消息记录这两个操作在一个数据库事务里要么一起成功要么一起失败。消息表写入成功后一个独立的任务把PENDING状态的消息定时扫描出来推送到消息队列下游服务消费消息后执行自己的业务成功后回调一个确认接口把消息状态改为SUCCESS。如果消费失败消息还在消息表里定时任务会不断重试直到成功。消息表的核心字段我列一下id、biz_id业务唯一ID、biz_type业务类型、message_content消息体、statusPENDING/SENDING/SUCCESS、retry_count重试次数、next_retry_time下次重试时间。设计这个表的时候一定要加biz_id的唯一索引这是幂等消费的基础。这里有一个很多人容易忽略的点不是所有业务都需要分布式事务。能异步就异步能降级就降级。比如加积分这个动作完全可以先跳过等订单完结后再异步补上。我们后期把大量同步的分布式事务改成了异步事件通知系统稳定性提升非常明显。分布式事务是最后的手段不是默认的选择。3.2 链路追踪从接到一起到一次排查服务变多了以后最痛苦的是排查问题。以前单体应用出问题看日志定位代码位置就行微服务架构下一次请求要经过网关、订单服务、支付服务、消息队列等好几个环节每个环节都可能出问题日志散落在不同的机器上排查起来非常费劲。我们当时做了一个决定可观测性建设从第一天就开始做。链路追踪选了SkyWalking核心原因是接入成本低——它是Java Agent方式通过-javaagent参数启动应用即可不需要业务代码做任何埋点改造。对比过Zipkin和JaegerZipkin需要业务代码里手动注入span侵入性太强团队推行起来抵触情绪很大SkyWalking的自动探针模式大幅降低了接入门槛。链路追踪解决的第一个问题是慢接口定位。以前接口慢你只能从头到尾看日志猜测现在直接看调用链路图所有环节的耗时一目了然。有一次线上接口变慢我们通过SkyWalking看到耗时主要集中在用户服务的某个查询上进一步排查发现是用户表新增了一个索引导致查询计划改变回退索引后问题解决。没有链路追踪这类问题可能要排查几小时。第二个问题是日志串联。我们把traceId打印在每一条日志里日志组件配置里加一个Patternpattern%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阈值grade1表示按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读写分离可能已经能解决大部分问题。微服务最大的价值不在于技术炫技而在于它逼着你思考架构治理服务边界在哪里依赖关系是什么如何保证数据一致性。就算你最后没有真的拆成很多个服务过程中建立的架构意识也会让团队受益匪浅。最后分享一个我的真实体验。这些年走下来我觉得微服务架构演进最打动我的不是那些技术组件而是它让整个团队不得不把以前靠默契、靠口头约定、靠代码规约维持的事情变成了显式的架构治理。服务之间不允许随便互相调用数据不允许随便跨境查询配置变更必须走审批接口必须有版本兼容。这些规则在单体时代几乎没人执行但在微服务架构下不执行就是灾难。不管你最后选择什么架构风格这种边界意识和治理意识我认为才是真正值钱的东西。如果你也正在评估要不要动架构我的建议是先想清楚你现在遇到的问题是什么是模块耦合、是团队协作、还是扩容瓶颈再根据问题去匹配架构方案。适合你的就是最好的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑