拓冰建站拓冰建站
首页 / 资讯中心 / 正文

分布式与微服务的区别:从一次订单超时雪崩复盘架构难点

周五晚上九点半我刚洗完澡手机连续弹了七八条告警。订单服务超时率飙到30%支付服务、库存服务、用户服务整条链路全是红色监控面板上的错误曲线像瀑布一样往下冲。我们当时只有三十几个微服务规模不算大但就是这几个服务在一个没有任何大促流量预兆的晚上把整条交易链路给拖死了。复盘完那次故障我突然意识到一件事分布式和微服务这两个词很多开发者其实是混着用的。面试时被问到“你们是分布式还是微服务”有人脱口而出“都有”但真被追问“两者到底差在哪”又说不清楚。那一次订单超时引起的雪崩恰好把这两个概念的差异血淋淋地展示了一遍。这篇文章我就从这次故障出发把分布式与微服务讲透再把微服务架构里最容易踩的雪崩、分布式锁、分布式事务、分布式缓存这几个坑一并聊完。适合正在做微服务改造、或者准备跳槽时被问“分布式和微服务区别”的Java后端同学看完能直接拿去用。1. 从一次订单超时雪崩的完整复盘说起1.1 故障现场接口P99从200ms飙升到10s先说故障面貌。当时线上有个秒杀类商品在做常规补货流量确实比平时高但远没到打垮系统的程度。网关QPS大概涨了3倍任何一个做过高并发的人都知道这个量级不应该出事。真正异常的是订单服务的响应指标P99从平时的200ms左右一路飙升到10秒以上错误率超过30%紧接着支付回调、库存扣减、用户积分三个下游服务开始跟着报错。我当时的排查顺序是这样先看全局监控确认是不是入口流量爆了再打开链路追踪发现订单服务95%的耗时都花在调用库存服务上然后抓线程栈订单服务的大批Tomcat线程全部卡在等待库存服务返回的socket读上最后去看库存服务的依赖数据库连接池打满了连接获取超时。整个过程不超过十分钟但问题已经很明显了这是典型的服务间慢调用放大导致的雪崩不是流量真的把系统打垮而是一个下游服务的性能劣化把整个调用链上的线程资源全部耗尽。1.2 根因分析慢请求、重试与线程池耗尽的连锁反应继续往下挖根因比现象更有意思。库存服务扣减库存时核心逻辑里先查了一次商品信息然后尝试获取一把Redis分布式锁来做库存扣减的互斥控制再更新数据库。那天的问题出在两点第一热门商品的库存行存在严重的锁竞争同时还有大量慢查询拖着数据库连接不释放第二订单服务调用库存服务时设置了超时时间是5秒但库存服务在连接池耗尽后大量请求在等待获取连接的阶段就已经超过5秒于是订单服务不断抛出超时异常。这里最要命的是重试机制。订单服务里有一个“失败重试两次”的配置本意是解决网络抖动结果在库存服务已经半死不活的时候重试请求像补刀一样继续涌向下游。库存服务的数据库连接池被占满新请求排队排队的请求又占着订单服务的线程不放订单服务的线程池也满了新的订单请求直接排队等待线程最后网关层开始超时前端不断重试雪崩就这么形成了。1.3 这次故障暴露的两个核心问题复盘时我们整理了两条教训这个结论我后来在很多团队都验证过很有普适性。第一分布式系统里慢请求比错误请求更危险。错误请求快速返回顶多占用瞬时资源慢请求会一直占着线程、连接、内存不释放在流量稍微高一点的情况下很快就能把资源池耗尽。所以分布式系统里所有远程调用必须有超时控制而且超时时间要分场景设置不能一个5秒走天下。第二微服务架构里故障是沿着调用链传播的。订单服务依赖库存服务库存服务依赖数据库数据库性能一劣化所有依赖它的服务全部遭殃。这就是微服务和单体应用在运维上一个很大的不同单体应用挂了就是整个应用挂了原因相对好找微服务挂了可能只是一个底层依赖慢了但表现在入口层面却是多个服务同时报错排查复杂度翻了好几倍。那次故障让我得出一个结论分布式解决的是“多台机器如何协作”的问题微服务解决的是“一个系统如何拆分成多个可独立演进的部分”的问题。两者大多数时候同时出现但它们的关注点、应对策略、技术选型全都不是一回事。2. 分布式与微服务到底差在哪2.1 一句话区分物理维度和逻辑维度我给团队讲这个概念时最常用的一句话是分布式是把一堆机器变成一台电脑微服务是把一个大系统拆成很多个小系统。分布式系统关注的是物理资源层面的协同。机器多了就要解决网络通信、时钟同步、数据复制、状态一致性问题。你用一个集群对外提供服务用户感知不到背后有多少台机器这就是“一台电脑”的感觉。Hadoop伪分布式就是在单机上模拟多节点分布式环境用多个进程扮演多台机器让开发者学习分布式原理这算是分布式领域最基础的实验形态。微服务关注的是业务逻辑层面的拆分。把用户、订单、库存、支付这些模块拆成独立部署、独立扩展的服务每个服务有自己独立的代码仓库、数据库、发布节奏。用户下单时订单服务去调库存服务、支付服务这就是一次典型的服务间调用。用个更生活的类比分布式像是一个公司有多个分部每个分部都有自己的服务器和数据库总部和分部通过网络协同工作微服务则像是一个公司按职能拆成销售部、技术部、财务部每个部门独立核算、独立汇报但都在同一个办公楼里。一个公司可以既是“分布式的”又有“微服务式的部门结构”但这两个词描述的根本不是同一维度的事。2.2 定义层面和技术层面的对比概念层面分布式系统是早期计算机学者就定义清楚的一组通过网络通信、通过消息传递协作、共同完成任务的计算机节点集合。微服务是2014年左右由Martin Fowler总结为架构风格的概念将单个应用开发为一组小型服务每个服务围绕业务能力构建可独立部署、独立扩展。这里有个关键点容易忽略微服务天然是一个分布式系统但分布式系统不一定是微服务。HDFS分布式文件系统、GFS、Redis Cluster、分布式爬虫这些都是分布式系统但它们不是微服务。微服务是面向业务架构的分布式文件系统、分布式缓存这些都是基础设施组件服务可以调用它们它们本身却没拆成“用户服务”“订单服务”。我用一个表格把关键差异放出来面试时这样答非常加分对比维度分布式系统微服务架构关注核心多节点协作的一致性、容错、网络通信业务能力的拆分、独立部署、独立演进解决问题单机性能与存储上限、高可用单体代码臃肿、发布耦合、团队协作效率适用范围所有多机协作场景含Hadoop、Redis Cluster、GFS偏业务后端系统的架构风格典型技术RPC、消息队列、分布式事务、分布式锁、共识算法Spring Cloud、Dubbo、Nacos、K8s失败场景节点宕机、网络分区、数据不一致服务间慢调用、依赖故障传播、版本兼容阶段关系底层基础设施上层业务架构风格2.3 为什么总有人把这两个词混着说混着说的人大多是从Spring Cloud入门微服务的。用Nacos做服务注册与发现用OpenFeign做远程调用用Sentinel做限流熔断用Seata做分布式事务——这一套搞下来给人的直观感觉就是“我们系统是分布式的”。但这里实际上是两套东西叠加在一起Nacos、Sentinel这些组件解决的是分布式环境下的协作问题订单、库存、支付这些服务拆分开解决的是业务工程的维护问题。所以准确的说法是我们用了微服务架构所以在分布式环境下必须额外解决分布式事务、分布式锁、分布式缓存、分布式调度、可观测性这一系列问题。这也是面试官问“分布式和微服务区别”时真正想听的答案你不仅知道微服务是拆服务还知道拆分后带来的分布式复杂度意味着什么。另外一个容易踩的误区是把“分布式事务”当成微服务专属问题。其实只要数据分散在多个数据库不管是不是微服务都会遇到分布式事务问题。我以前维护过一个“伪分布式”的单体系统一个应用连了四个库照样需要分布式事务方案。微服务只是把这种问题放大了它不是分布式问题的根源。3. 为什么微服务架构更容易雪崩3.1 雪崩形成的三个条件从那次订单超时故障里我总结出雪崩形成的三个必要条件有依赖链、资源有上限、缺少保护机制。单体应用基本不会雪崩是因为它内部方法调用不走网络即使某个方法变慢影响的也只是当前线程JVM还在其他接口还能继续服务。微服务不同一次用户请求要经过网关、订单服务、库存服务、支付服务四处网络调用每个服务都有独立的线程池和连接池任何一环变慢调用方线程就会被占住。当大量请求堆积在慢调用上线程池耗尽新请求直接排队或拒绝。这个阶段如果还有服务配置了重试或者前端自动刷新重试流量会被放大好几倍最终把整条链路拖垮。分布式系统的经典理论里有一个概念叫“故障放大效应”。一个接口耗时从200ms变成5s单个请求占用的线程时间放大了25倍如果超时后还有重试又会放大3倍。有时候下游只是数据库一条SQL没走索引上游却因此发生大规模雪崩这就是放大效应的可怕之处。3.2 分布式锁从防超卖到定时任务重复执行那次故障里库存服务用Redis分布式锁控制扣减库存的并发锁竞争是引起数据库连接池占用的诱因之一。这里把分布式锁单独拿出来讲因为很多微服务项目用到它的场景远比你想象的多。最常见的是防止库存超卖。用户下单时先尝试获取锁拿到锁的人才可以去扣减库存避免两个请求同时读到库存为1然后都执行扣减变0。这个场景用SET NX EX可以实现但我见过太多人在这个简单实现上栽跟头。第一种坑是锁误删。线程A拿到锁因为业务执行太久锁自动过期线程B拿到锁开始执行A执行完成后直接DEL把B持有的锁删了。解决方法是删除前先比较Value也就是在Value里存一个唯一标识删除前用Lua脚本做“先比对再删除”的原子操作。第二种坑是锁过期但业务没跑完。扣减库存这种短操作还好但如果是生成报表、批量结算这种可能执行几十秒的操作固定的过期时间很难设置。Redis官方推荐的Redisson库用“看门狗”机制给锁自动续期默认每10秒检查一次业务没结束锁就不会过期。我在项目里但凡业务可能超过默认租约时间的都会直接用Redisson不再手写SET NX。第三种坑是锁粒度过大。有人为了防止整个订单流程并发问题从创建订单到扣库存再到发消息全程持锁这把分布式锁用成了分布式串行化性能被拉到极低。锁粒度应该只覆盖临界资源操作比如扣库存只锁库存行不锁整个下单流程。分布式锁还有一种高频场景在定时任务里多个微服务实例同时部署了一份定时任务代码需要保证同一时间只有一个实例执行。用Redis分布式锁给任务名加锁抢到锁的节点执行执行完释放比引入一套分布式调度框架轻量很多。3.3 分布式事务订单与库存怎么保持一致订单超时雪崩故障里库存扣减看似只是性能问题深层其实是分布式事务问题。用户下单要创建订单、扣减库存、冻结优惠券、记录积分这些操作分散在多个服务里每个服务可能有独立数据库单靠本地事务已经管不住了。分布式事务的常见方案有三类我按实际场景说说怎么选。第一类是强一致方案典型是2PC两阶段提交或Seata AT模式。适合对一致性要求极高、并发量不高的内部系统比如账户余额变更、财务对账。代价是性能损耗大事务锁定资源的时间长秒杀这类高并发场景几乎不用。第二类是TCCTry-Confirm-Cancel补偿方案服务提供方自己实现三个操作Try阶段冻结资源Confirm阶段确认Cancel阶段回滚。相比2PC更灵活但开发量大每个业务方法都要写三段逻辑。第三类是最终一致性方案也是我用的最多的。核心是“本地消息表”或“事务消息”订单服务在本地事务里写入订单数据和一条消息然后通过消息队列异步通知库存服务扣减库存服务消费消息执行扣减成功后回执如果失败消息队列重试或者由定时对账任务扫描补偿。这个方案牺牲了强一致性但换来了高可用和性能是绝大多数电商场景的正确姿势。订单创建先返回“已受理”真正扣库存成功后再异步告知用户对用户来说体验也不差。这里提醒一点很多人一上来就上Seata觉得全自动很爽但AT模式默认会占用全局锁秒杀场景下数据库连接池很快会被锁等待占满。分布式事务没有银弹先想清楚业务能接受多大的不一致窗口再选方案。3.4 分布式缓存保护数据库的第一道防线微服务化之后数据库连接是最稀缺的资源。库存服务那次连接池被打满很重要的原因是缓存没扛住流量请求全部穿透到数据库。分布式缓存的三类常见问题我在这里一起说了。缓存穿透是指查询一个必然不存在的数据缓存和数据库都没有请求直接打到数据库。高并发下这种请求可以瞬间拖垮数据库。解决方法是缓存空值或者用布隆过滤器在缓存前拦截。缓存击穿是指某个热点key过期的一瞬间大量并发请求同时去数据库重新加载。解决方法是互斥重建也就是用分布式锁确保只有一个线程去查数据库其他线程等待后直接读缓存。缓存雪崩是指大量key在同一时间过期或者缓存节点宕机导致所有请求全部穿透到数据库。解决方法是给过期时间增加随机值避免批量key同时失效Redis Cluster做多节点高可用不让单点故障拖垮全局。分布式缓存的技术选型我在生产环境里基本就两种Redis Cluster做业务缓存和分布式锁数据量大、高并发场景下它能扛本地缓存Caffeine做热点数据的一级缓存放在服务进程内访问最快的才是性价比最高的。很多团队不管什么数据都往Redis里塞结果缓存本身成了瓶颈这就背离了缓存设计的初衷。4. 微服务防雪崩的实战组合拳4.1 超时、限流与熔断先给系统装上刹车防雪崩的第一件事是所有远程调用必须有明确的超时时间。OpenFeign配置超时要区分连接超时和读取超时连接超时通常1到2秒读取超时根据接口业务类型设置查询类接口3秒以内涉及批量计算的可以放到5秒到10秒。千万别一个timeout5000配在所有接口上慢接口和快接口混在一起故障时根本没法快速定位。第二件事是限流。限流可以放在网关层做全局QPS限流也可以在服务内部用Sentinel做单机维度限流。很多团队只配了网关限流忽略了服务内部接口的限流。网关限流只是入口服务间调用产生的流量它是看不到的。我习惯在每个服务的关键写接口上单独配置Sentinel限流规则阈值压测时给出比如扣减库存接口单机最高300 QPS超过直接返回“系统繁忙”。配置限流规则时还要同步配置一个降级兜底方法这样被限流的请求不是硬失败而是得到一个降级响应比如库存紧张提示。第三件事是熔断。Sentinel的熔断策略我按慢调用比例和异常比例两种来配比如接口在10秒内慢调用比例超过60%熔断10秒10秒后放行少量请求试探恢复情况。熔断期间调用的兜底逻辑是返回一个预设的降级结果。有个概念需要强调熔断不是把故障藏起来而是用快速失败保护线程资源给下游服务喘息恢复的时间。一次故障里如果所有调用方都快速失败而不是死死等待下游受到的流量压力会小很多恢复会更快。4.2 隔离与异步化让故障不扩散隔离是防雪崩最容易忽视的一环。线程池隔离的意思是为不同的下游依赖分配独立的线程池比如订单服务调用库存服务的请求走A线程池调用支付服务的请求走B线程池。如果库存服务变慢最多耗尽A线程池支付服务相关的请求不受影响。这个在Sentinel里可以通过信号量隔离或线程池隔离配置实现。有人觉得线程池隔离浪费线程资源但它在关键时刻能保住主干链路这笔账非常划算。异步化用来斩断同步调用链。那次订单超时故障里如果订单创建后库存扣减不是同步等待结果而是投递到消息队列由库存服务异步消费订单服务的主线程处理完订单创建逻辑后立刻返回库存服务就算慢也能靠削峰慢慢消化根本不会拖死订单服务。很多人担心异步后不一致窗口太大实际上配合本地消息表只要保证消息最终发送成功、消费幂等这个方案在绝大多数业务场景完全够用。4.3 上线前的容量评估与压测验证这一节说说压测可能很多人觉得和“分布式和微服务差在哪”这个话题关系不大但那次故障让我明白很多雪崩其实在上线前就能压出来。我们后来自建了一套压测流程核心思路是先摸清每个服务的容量水位再给限流熔断阈值定档。用JMeter做高并发测试时脚本设计有几个关键点。第一不要一上来就打最大并发而是由低到高逐步加压50并发跑3分钟、100并发跑3分钟、200并发跑5分钟每档都观察接口P99、错误率、CPU、内存、数据库连接池占用。第二要压链路而不是压单点推荐用JMeter的分布式测试模式从压测机向Nacos注册中心上加压让请求走真实的网关→订单→库存→数据库全链路。第三每个档位都要记录数据最后汇总成一张容量表比如“订单服务单实例极限500 QPS库存服务单实例极限300 QPS”这些数据就是配置Sentinel限流阈值、K8s HPA扩缩容策略的依据。当时我们把整套若依微服务环境迁移到一台单节点K8s上做过压测验证压测脚本就是配套的JMeter工程。那次的体会很深K8s单节点虽然方便资源争抢非常严重服务A的CPU飙升直接拉低服务B的响应所以压测一定要在目标环境做不要把开发环境测出来的数据当成容量基线。5. 分布式不是银弹聊聊代价与选型5.1 微服务的基础设施成本这些组件本身也是系统很多人以为把服务拆了就是微服务其实光有服务拆分远远不够。一个能稳定运行的微服务系统至少要配套注册中心、配置中心、网关、熔断限流组件、链路追踪、日志聚合、监控告警、CI/CD流水线、容器编排平台。我用Nacos做注册与配置Sentinel做限流熔断SkyWalking做链路追踪PrometheusGrafana做指标监控ELK做日志检索K8s做部署与弹性伸缩。这些东西加起来的运维成本比几个服务本身高得多。分布式文件系统也一样需要选型。我们内部小规模文件存储直接用了MinIO兼容S3协议部署简单性能足够但如果数据量到PB级别需要海量吞吐和容错能力HDFS更合适。数据量决定方案不能为了分布式而分布式。5.2 数据一致性的代价没有免费的午餐微服务拆分后一个很现实的痛苦是数据的一致性要求被“打散”到了业务代码里。以前单体应用里一条SQL带个事务就能搞定的事现在要引入MQ或者TCC代码复杂度成倍上升。我见过一个团队把订单和库存拆到了两个服务然后又为了强一致引进了Seata压测时性能惨不忍睹最后不得不把库存扣减改回订单服务本地事务。所以我在实际项目里定了两条经验。第一不是所有数据都要拆服务如果两个表的关系极其紧密、访问模式高度一致即使业务上分属不同模块也可以先留在同一个服务同一个库里用模块化代码做拆分等真正出现性能瓶颈或组织独立交付需求时再拆。第二分布式事务能不用强一致就不用优先考虑最终一致用异步消息、本地消息表、定时对账来兜底先把可用性保住。5.3 什么时候不适合上微服务这话可能有些反主流但我真的建议对大多数中小团队和业务一开始不要上微服务。微服务解决的核心问题是“多团队并行开发导致代码冲突严重、发布互相牵制、系统无法弹性扩展”如果你的团队只有两三个后端开发代码量也就几万行单体应用加模块化开发是效率最高的选择。硬拆微服务只会让每个人都要维护一套Nacos、一套Sentinel、一套SkyWalking光环境搭建和联调就耗掉大量开发时间。如果搞不懂分布式事务、分布式锁这些概念没经历过线上雪崩的代价更不要一上来就微服务。我后来和很多团队的负责人聊都会问三句话你们是否真的有独立扩容某个模块的需求团队是否大到必须按服务划分边界有没有人能专职维护基础设施三个都满足再考虑微服务。否则单体应用加缓存、加消息队列已经能扛住绝大多数量级。5.4 分布式组件选型的经验清单最后给一张我实践中沉淀的分布式组件选型表很多是踩坑之后才确定下来的场景推荐方案备选不推荐分布式缓存、分布式锁Redis Cluster RedissonCodis自己手写SET NX不加续期分布式事务本地消息表 / 事务消息 定时对账Seata AT低并发内部系统强一致方案用在秒杀链路分布式定时任务XXL-Job / PowerJobQuartz集群每个节点都跑同一份定时任务分布式文件存储MinIO中小规模HDFS/GFSPB级全部塞进数据库Blob字段服务发现与配置NacosConsul、Etcd自己维护ZooKeeper除非团队很熟链路追踪SkyWalking / Micrometer TracingZipkin出了问题全靠猜日志聚合ELK / Loki Grafana云厂商日志服务日志只留在宿主机里限流熔断SentinelResilience4j、网关限流全靠Nginx手动重启这个表不是标准答案是取舍之后的经验。比如ZooKeeper本身很成熟但维护成本和一致性模型的理解成本偏高中小团队用Nacos更省心。再比如Redisson虽然很强但如果团队只是做简单的短任务互斥手写SET NX配合Lua脚本也能用关键是意识到锁的续期和误删风险。那次订单超时雪崩复盘之后我给自己定了几条铁律没有超时控制的调用不发布没有降级方案的依赖不引入同步调用超过一跳就要问为什么不能异步。这些规矩听起来简单但真正坚持执行需要团队每个人对分布式系统的敬畏。我自己最大的感受是分布式和微服务从来不是“哪个更高级”的问题而是两个不同层面的技术决策。分布式是你在多台机器上做系统时的必然约束微服务是你在业务拆分解耦时的一种架构选择。把这两个概念分清楚再去看分布式事务、分布式锁、分布式缓存这些具体问题思路会清晰很多。也希望这篇文章能帮你少踩几个我踩过的坑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门