恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
从单体到微服务的架构演进:拆分之痛与治理之道
首页
资讯中心
/
从单体到微服务的架构演进:拆分之痛与治理之道
从单体到微服务的架构演进:拆分之痛与治理之道
发布时间:2026/9/18 3:10:55
2018年秋天我负责的一个电商单体应用迎来了它最焦头烂额的一次发布。订单组改了表结构支付组改了接口两个人同时动了同一个服务代码合并冲突没完没了晚上十点终于合并提交启动又报“超过最大连接数”因为一条慢SQL把数据库连接池打满了。凌晨两点切流量的时候群里没人敢睡觉。那个晚上之后团队内部正式把“拆分微服务”提上了日程。这就是我写这篇“编年史”的起点从单体到微服务看起来是一次技术选型实际是一部与复杂性斗争的编年史。这篇文章想把我自己踩过的泥坑、跟同行讨论过的架构决策、以及后来辅导团队做迁移时的经验教训串成一条完整的脉络。我会先讲清楚“架构范式”到底在解决什么问题再逐层拆解微服务落地的核心细节然后是迁移的实操路径最后给出一份常见问题的排障实录。无论你是第一次接触微服务的后端开发还是团队里正在推动架构演进的技术负责人这篇内容应该能帮你少走几条弯路。需要先说一句我并不是微服务的狂热信徒。恰恰相反在不少场景下我会劝人谨慎。但你必须理解它诞生的原因才能真正用好它。这个话题没有银弹只有权衡。1. 内容整体设计与思路拆解1.1 架构范式是一份“复杂度的对账表”架构范式本质上是对系统复杂度的应对策略或者说是一张对账表。单体架构、微服务架构、甚至更复杂的分布式架构都是不同复杂度阶段的不同记账方式。当系统复杂度处于“三个模块、两个开发”的层级单体架构是最高效的记账方式——一个仓库、一个进程、一次部署CRUD全搞定。但是复杂度会涨涨到一定程度单体的记账方式开始失真你无法判断改一行代码会影响到哪张表、哪个服务、哪个定时任务你也无法判断一次发布能不能在半小时内完成。我用一个生活类比来解释一个家庭从两口之家变成五口之家还是只有一个厨房那饭点的时候矛盾一定爆发。单体不是不好而是它承载人数的能力有限。你去看那些从初创一路走过来的业务系统早期用单体架构绝对是正确选择因为它聚焦的是业务探索而不是技术规模。只有当业务稳定扩张、团队膨胀、发布频率上升单体厨房的边界感才会逐步失效。我见过太多团队把“微服务”当成一股潮流老板说要微服务就拆分微服务。最后拆出来的不是微服务而是三十个吵架的单体。这就是没搞明白范式背后的对账逻辑。很多人拿“秒杀商城微服务系统”写简历写得很漂亮但你问他服务拆分怎么做的、数据怎么分片的、分布式事务怎么保证的回答不上来。这类项目写进简历不是加分项面试官一问就能拆穿。我建议把经历彻底捋清楚再写不然还不如不加。1.2 单体架构的黄金时代为何必然终结单体的优势值得认真复盘因为如果你不知道它好在哪你也不知道拆分会失去什么。单体最典型的三个好处事务不需要跨服务业务逻辑可以直接在一个事务里写入多张表调试非常方便断点一打调用栈从Controller到Mapper一路清晰部署简单一个war包或者一个镜像起来滚动发布改个端口就能做。这些优势在中小规模下是决定性的。类似若依这种优秀的开源管理脚手架能受到大量中小团队欢迎核心原因就是它把权限、字典、日志等通用能力打包好了一个人也能撑起一个后台。但等团队到三十人以上业务线扩展成订单、支付、库存、营销多条线时单体的“简单”就会转化成“混乱”。数据库连接池就那么几个连接一条营销活动的慢查询就会拖垮支付链路一个服务一发布所有人被迫陪跑。这种“全局耦合”的问题是单体架构无法回避的天花板。当耦合引发的交付速度、故障半径、团队协作等问题积累到一定阈值微服务就成了一个自然的选择。注意我说的是自然选择不是灵光一闪。从组织角度看单体架构还有一个隐性成本技术债会在一个仓库里无限累积。老员工写的代码没人敢动新员工看不懂过往决策核心模块变成“鬼城代码”。这些都不是微服务直接解决的但微服务强制用进程边界去切分代码归属等于给每个模块划清了产权至少在管理上是把复杂度拆开治理了。1.3 微服务把复杂问题拆成多个简单问题微服务的基本思想听起来特别朴素把单一应用拆分成一组小服务每个服务围绕业务能力构建拥有独立的数据库通过API通信独立部署、独立扩展。但它真正的价值不在技术细节而在组织协作当订单组只维护订单服务时订单服务的变更就不需要惊动支付组当营销大促来了也可以单独给商品服务扩容而不是把整个应用放大一倍。这里面的关键转变是“控制反转”单体是把所有逻辑揉在一起微服务是用进程边界强制要求团队把逻辑理清楚。用我的话说微服务是一种“组织架构的镜像”也就是康威定律——系统设计实质上是复制组织内部的沟通结构。如果一个团队分成订单组、支付组、库存组那系统大概率也会成长出对应的服务边界。如果不是那日子大概率过不好。当然微服务也把很多原本由中间件、框架帮你隐藏的问题暴露了出来比如服务发现、配置管理、链路追踪、分布式事务。这就是为什么说微服务没有省掉复杂性只是把复杂性搬到了新的位置。想真正用好微服务就必须从底层理解这些被“搬走”的复杂度在哪里以及怎么去治理它们。2. 核心细节解析与实操要点2.1 拆分边界技术视角还是业务视角服务拆分第一个要回答的问题是按什么拆很多团队会犯一个错误按技术层次拆——把一个应用拆成controller服务、service服务、dao服务这是我在接盘过的团队里见过最灾难的拆分方式。按技术层拆意味着每改一个业务需求要同时改三个服务甚至在三个服务之间远程调用这种“分布式单体”比单体还难维护。正确起点是业务边界要尽量从一个独立的业务能力出发。比如订单、支付、库存、积分、营销这些是可独立演化的业务域。想把这个边界定义清楚可以借助领域驱动设计DDD里的“限界上下文”概念看得懂DDD最好看不懂也没关系。我提供一个非常朴素的标准如果某个实体或功能的变化不会引起另一个模块的变化那它们就可以被拆开。我实际操作用过的方法是把需求文档里经常同时出现的名词聚成一类把经常一起变更的用例聚成一类再看它们的读写数据结构是否相对独立。如果订单和支付总是在同一个页面出现、同一个流程里变更那它们适合先留在同一个服务等稳定后再拆。反之营销活动和订单主流程的节奏完全不一样就可以优先拆出去。先合后拆比一开始就拆得稀碎要安全得多。2.2 服务间通信与 API 治理服务拆完之后服务与服务之间的通信方式就成了核心问题。同步调用最常见REST就够用但性能要求高的内部调用可以考虑gRPC。这里要说的是很多团队把服务间接口直接暴露成公网接口这是不对的。服务间的接口应该走内部网络最好统一通过服务注册中心发现避免IP直连。IP直连的问题在故障转移时尤其明显对方服务扩了两个新节点你的代码还在往旧IP上打等于白扩容。另一个高频问题是接口变更的治理。我踩过一个特别经典的坑B服务为了优化查询把一个字段类型从Integer改成了LongA服务还按Integer解析上线后A服务那部分功能直接异常。问题在于A、B两边分属不同团队没人通知。所以我现在要求所有服务间API都带版本号不兼容变更必须提前发通知并保留旧版本灰度过渡。接口规范不是写文档而是一条生产事故换来的红线。对于异步通信消息队列是标配。RocketMQ、Kafka、RabbitMQ各有应用场景但无论选哪种都要面对“消息丢失、重复消费、顺序性”三个问题。生产环境下我见过最多的坑是“重复消费没有幂等处理”比如扣库存的服务同样一条消息消费了两次库存被扣了两次。这个问题必须一开始就设计好幂等策略比如消费端保存消息ID和业务主键的对应关系重复消息直接返回成功。幂等设计看起来简单但它决定你的消息系统到底能不能上线。2.3 分布式事务的现实取舍分布式事务是微服务化过程中最刺痛人的环节。单体架构里一个事务里先扣库存再创建订单一气呵成。微服务化之后订单服务和库存服务各自拥有数据库原来的本地事务不存在了你需要想办法让两个库的数据保持一致。市面上的方案很多2PC事务、TCC柔性事务、Saga、本地消息表、最终一致性。我的实际经验是除非业务流程特别短、对一致性要求特别高否则不要轻易上2PC或TCC。原因很简单同步阻塞、补偿逻辑复杂实现和维护的成本都非常高。绝大多数业务场景最终一致性是足够的。我实际负责过的一个订单流程是用户下单 - 调用库存服务扣库存 - 创建订单 - 发送消息通知积分服务加积分。库存和订单用“本地消息表消息队列”来保证最终一致积分则允许一定时间后到账用户界面显示“积分即将到账”就完事。这里的关键不是技术多花哨而是业务上能不能接受短暂的“中间状态”。如果产品经理说不能接受你再回来谈2PC这才是正确的顺序——业务决定技术而不是反过来。3. 落地微服务的关键技术选型3.1 服务发现与配置中心的踩坑记录服务发现和配置中心是微服务的基石。没有服务发现服务地址就只能写死一旦扩容或故障转移所有客户端都要改配置。现在主流的注册中心有Nacos、Eureka、Consul我多数时候更推荐Nacos因为它除了服务注册发现还自带配置中心而且支持主动推送变更某种程度上可以少维护一套组件。Eureka 2.x那边停更得早新项目不太建议再碰。配置中心的坑也不少。我见过团队把配置中心当摆设所有配置还是放在本地application.yml里上线后再去服务器上改完全失去了微服务动态配置的意义。建议从一开始就把容易变化的配置——开关、阈值、限流参数、对接方地址——迁移到配置中心并且在代码里禁止写死IP。这一点虽然听起来基础但能极大降低运维成本。不过还是要提醒一句任何组件都是复杂度。如果你的系统规模不超过两台服务器或者团队对注册中心、配置中心并不熟悉硬上一套Nacos集群可能是自讨苦吃。服务发现的价值在于规模化而不是为了“听起来专业”。技术选型和买鞋一样合脚最重要你在小项目里上了全套微服务组件只会发现大部分时间都在处理基础设施的告警。3.2 网关、认证与流量治理网关是微服务的门面。所有外部请求先进网关由网关做路由转发、鉴权、限流、灰度等公共能力。常见的选型有Spring Cloud Gateway、Kong、APISIX等。我比较推荐Spring Cloud Gateway因为和Spring生态集成度最高学习成本低适合大多数Java技术栈团队。如果团队异构语言多可以考虑APISIX或Kong它们的功能边界更偏向于流量网关能够统一纳管多语言服务。认证部分从单体到微服务有一个明显的分水岭。单体里Session机制很好用微服务化后需要把用户态从“服务内部会话”搬到“请求者侧边状态”JWT或者OAuth2就变得常见。但JWT不是银弹尤其要注意刷新令牌和注销问题——一个签发出去永不过期的JWT在用户改密码后还能继续使用这会带来安全风险。所以需要引入refresh token的刷新机制或者维护token黑名单。很多团队做微服务只关注拆分忽略了认证体系改造结果上线后才发现退出登录变成摆设。流量治理是微服务上生产环境的必备能力包括限流、熔断、降级。限流常见用Sentinel或网关层限流熔断降级可以用Resilience4j或Sentinel。这里需要强调熔断、降级不是只有大厂才需要的。哪怕只是一个聚合商品详情页底层商品服务超时你不做熔断很可能所有线程都卡在等待响应上整个服务跟着雪崩。我的习惯是凡是依赖外部服务的调用都要设置超时时间、重试次数上限并为它设计降级返回值这是微服务从业者的基本素养。3.3 可观测性没有监控不要拆微服务可观测性决定你能否在微服务环境里活下来。单体时代排查问题非常简单一条调用链一路走到底。拆分后一个请求穿过网关、用户服务、订单服务、支付服务任何一个环节出错都要靠日志和链路追踪来定位。没有监控的微服务等于在夜里关灯走路出了问题只能靠猜。可观测性的三根支柱是日志、指标、链路追踪。日志必须结构化推荐JSON格式并且统一包含traceId方便关联同一个请求。指标采集一般用PrometheusGrafana重点监控请求量、错误率、响应时间、GC、连接池等指标。链路追踪可以选SkyWalking或Zipkin在UI上能直观看到一次请求在哪个服务耗时最长。前段时间有同事用Nest.js写微服务一直在问监控与可观测性怎么落地。其实思路完全一样日志结构化、暴露/metrics端点、接入Prometheus然后加一个链路追踪中间件。语言和框架不是关键关键是这套基建能不能让你在流量高峰期五分钟内定位到“到底是网关慢了还是下游慢了”。把可观测性放在服务注册之后讲是因为很多中小团队在搭建微服务时往往先做网关再做认证最后才想到监控。而我的经验恰恰相反监控应该先于业务上线否则微服务一拆线上出问题你可能连从哪儿查都不知道。4. 从单体到微服务的迁移实战4.1 绞杀者模式与增量演进单体存量系统迁移是一个重大工程做不好就是“拆一个挂一个挂一个回滚一个”所以策略极其重要。我强烈推荐的模式是“绞杀者模式”Strangler Pattern不重新推翻重写而是在单体旁边长出一个新的微服务把新需求或者部分能力逐步迁移到新服务上老单体慢慢被“绞杀”掉。为什么要这样因为彻底重写的风险极高业务逻辑在单体里纠缠了多年靠重构很难100%还原往往重构到一半才发现老系统的隐藏逻辑数不胜数。我记得一次重构所有开发都认为库存扣减很简单结果翻历史代码才发现有十几个判断分支比如预售、赠品、秒杀、黑名单都是从需求文档里消失的老功能。所以保留老系统、增量替换反而是最稳的路线。绞杀者的实施步骤大概是选定一个容易隔离的模块例如积分模块先在单体前加一层网关把积分相关请求转发到新积分服务新服务用来承接新的积分逻辑线上稳定后再把旧代码从单体中移除。每一步都只做能力迁移不做一次推翻。这样每一轮都有交付而不是憋一个大版本然后祈祷发布成功。这个模式几乎适用于所有Java、PHP、Python等语言的老系统改造前提是你愿意接受“慢慢来才是最快”的节奏。4.2 数据库拆分与不停服迁移方案数据库拆分是迁移中最危险的操作比代码拆分危险得多。很多团队把代码拆得很干净数据库还是一个库结果服务之间还在共享表事务边界名存实亡。所以要真正微服务化数据库必须跟着业务边界拆开。但拆库的难处在于表之间外键、关联查询、事务都很常见拆开之后这些全都失效了业务代码必须先重构为“服务间查询/异步同步数据”。迁移时如果允许停机那简单一点凌晨发布写停止写入数据迁移校验然后切换新服务。但如果要求不停服、不丢数据就需要更精细的设计。我最近在处理的一个项目就是这样要把整套环境迁移上云还要准不停服、不丢数据。思路是先用binlog监听把数据库增量变更同步到目标库开双写做数据校验最后切换流量。听起来不复杂但每一步都有很多细节比如binlog监听要有位点记录、目标库要在切换前保证追平、切换瞬间要短暂停写几秒避免丢数据。另一个常见问题是历史数据的迁移。我建议不要在切换当天做全量迁移而是在发布窗口之前先把历史数据全部同步完留下增量在切换窗口处理。切换窗口尽可能短最后再通过比对工具校验两边数据一致即使不一致也要有回滚预案。如果实在没有完善的校验工具那就做好逻辑回归测试用例并给业务方一个明确的数据确认时间窗口。数据库迁移不是技术的胜负手而是流程管理的胜负手。4.3 容器化、编排与压测验证服务拆完之后部署方式也要跟着升级。单体时代一台服务器跑一个应用微服务时代每个服务可能还要多副本靠手工部署完全不可能所以容器化和编排基本是必选项。Docker打包镜像Kubernetes做编排调度这是目前的主流。但Kubernetes本身是个复杂的系统。我见过单节点K8s上搭建整套微服务环境的案例比如把一整套脚手架项目放到单节点K8s里跑起来注册中心、配置中心、网关、各个业务服务都做成Pod这用来学习和演示确实很快。不过如果用在生产环境单节点K8s会有单点风险而且运维成本不可小觑至少要三节点起步还要规划存储、网络、日志采集。所以对中小团队我的建议是先搞清楚自己为什么要上K8s有时候用Docker Compose或轻量容器平台也能解决部署问题。环境部署完成后压测验证不能少。最近就有个朋友在云上搭完微服务环境后让压测人员直接上JMeter脚本做高并发压测看单个服务、全链路、数据库池在各个压力级别的表现。这里我想提醒压测不只是看峰值QPS更要看90分位响应时间、错误率、上下游链路的资源瓶颈。如果压测时发现服务还没报错但数据库连接池已经满了那就说明服务层配置需要调整而不是只加副本数量。这也是为什么我不建议把“秒杀商城微服务”这种项目写进简历时掉以轻心因为秒杀场景的流量模型和普通业务完全不同。如果你没有真正跑过压测、没有分析过瓶颈点就只是按教程搭了一堆服务面试官一问细节就会露馅。踏踏实实做一次全链路压测比堆十个框架更值钱。5. 常见问题与排查技巧实录5.1 拆了反而更慢先查这三件事很多团队微服务化之后发现很多接口比单体时代还慢响应耗时翻倍。遇到这种情况先别怀疑微服务本身按下面顺序排查第一调用链路上是不是多了很多不必要的远程调用。我在代码评审里常常见到这种代码订单服务为了展示用户名先调用用户服务为了展示优惠券又调用营销服务三个服务串行下来一次HTTP调用少说增加几十毫秒。解决办法是用“批量接口”或“聚合服务层”或者把高频率的基础数据做成服务端缓存/本地缓存。第二是不是网络开销没控制好。服务间走HTTP/1.1序列化用JSON在大流量下性能和连接管理都不理想可以换成gRPC或者HTTP/2并开启连接池复用。很多服务框架默认连接池配置很保守QPS一上来就会出现大量连接等待这里需要单独调优。第三是不是数据库连接和缓存配置没有跟着拆。服务变多了但每个服务依然用同一个数据库连接数分配自然紧张。这又回到我前面说的数据库如果不跟着服务边界走微服务化对性能只会有负面影响。查完这三个方向大部分“拆了更慢”的问题都能找到根因。剩下真正的瓶颈往往出在某个下游依赖比如第三方接口慢、Redis热点Key过多那就需要更细的压测和监控了。5.2 分布式事务的经典Bug复盘我记得一次生产事故是库存服务扣减成功但订单服务创建订单的消息因为消费失败被无限重试然后又因为重试导致重复创建了多笔订单。那一次的根因是消息重试机制和业务幂等没有配合好。后来我们在消费端增加了唯一键约束用业务订单号作为唯一键重复消费时插入失败直接返回成功这样就把重复问题解决掉了。还有一个经典Bug是Saga模式的补偿顺序错了。用户下单先减库存再创建订单如果创建订单失败需要把库存加回。但加库存时用了当前的库存数量去更新中间如果其他订单也扣了库存就可能把别人扣掉的库存覆盖掉。正确做法是更新时使用“库存本次要加的数值”而不是先查出来再加。像这种问题不经过线上事故是记不牢的。分布式事务的坑本质上是并发和状态控制的坑。方案选型只是第一步细节和边界情况才是最消耗时间的部分。建议所有涉及消息消费和补偿逻辑的代码都要写单元测试和故障演练。没有演练过的补偿逻辑等于默认它是错的。5.3 组织协作引起的架构事故微服务架构的成败一半在技术一半在组织。康威定律我前面提过这里再展开讲一个真实故事。有一次团队把业务拆成了A、B两个服务A归前端组维护B归后端组维护。结果前后端组之间没有一套清晰的接口变更流程后端把接口改了也不在群里通知前端调用直接404。这个事故的根本原因不是代码而是组织沟通。为了应对这类问题我总结了几条硬性要求服务间所有接口变更必须走“接口变更评审”至少提前一个迭代在团队群里周知每个服务必须有明确的负责人和ON-CALL统一的日志规范、错误码规范、上线流程必须有文档沉淀。没有这些基础制度再好的架构也会在人的沟通成本下落败。另一个跟组织相关的现象是微服务的“过度拆分”。公司人不多却拆出几十个服务每个服务只有一两个人懂出了问题没人敢动。这种组织和系统都不稳定的局面是我最不愿意看到的。我常说服务拆分的第一原则不是“拆”而是“合得拢”。一个两三个人的团队宁可先保留一个模块化单体也不要去撑起十几套微服务。架构演进的节奏必须匹配团队规模这是硬道理。关于单体到微服务的这段“编年史”我个人的体会是架构范式没有先进与落后只有复杂度匹配不匹配。微服务提供了一种用边界对抗复杂性的思路但它同时把复杂性从编码问题转移到了运维、治理和人的协作问题上。你只有在足够大的复杂度压力下才能体会到微服务的好处否则模块化单体可能是更诚实的选择。最后再分享一个小技巧如果你正站在“要不要微服务化”的岔路口先做一个“变更爆炸半径”的试验。记录一个需求从提出到上线最常受影响的模块和团队有多少。如果一个需求常常要同时修改五个以上模块、牵动两个以上团队的排期微服务化就值得认真考虑如果大多数需求两三个模块就能搞定那就先用模块化设计守住边界。这个试验虽然简单但比看十篇架构文章都有用。我不希望你再走一遍“先拆了再说然后后悔”的老路。