ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

分布式与微服务到底有何区别?订单雪崩复盘与治理实践

分布式与微服务到底有何区别?订单雪崩复盘与治理实践 凌晨02:13我盯着监控大屏上的订单失败率曲线看它从0.01%一路冲到60%。下单接口P99从80ms涨到7.3s支付回调超时积分服务熔断库存扣减的锁等待飙升二十分钟后整个下单链路雪崩——用户点结算直接报“系统繁忙”后台日志里全是连接超时和线程池拒绝异常。复盘会上团队为故障定性吵了一晚这是分布式系统出了问题还是微服务架构有问题有人说是“分布式事务没做好”有人说是“微服务调用链路太深”还有人觉得“上了微服务之后系统本来就更脆了”。说实话那次复盘最有价值的产出不是修掉了Bug而是逼我们把两个长期混着用的词掰扯清楚分布式和微服务。很多人把这两个词当成同义词写简历时混着写架构评审时混着聊真到定位故障、设计容灾方案的时候就抓瞎了。这篇文章就当是那次故障复盘的系统化记录把概念差异、订单超时雪崩的真实成因、以及后续治理方案一次性讲透。1. 订单超时雪崩复盘从毫秒级延迟到全链路宕机1.1 故障现象与关键时间线那套系统是典型的Spring Cloud微服务架构下单链路串了商品、库存、订单、积分、支付、物流六个核心服务外加几个配置中心和网关组件。平时的流量峰值大概每秒1200单大促预热冲到2500单问题也不大。出事那晚没有流量尖峰压测都没打满订单量只比平时高了不到30%。故障演进大致分四个阶段时间核心指标现象02:13订单失败率 0.01% → 8%支付回调偶发超时部分订单状态不一致02:18失败率 8% → 37%库存服务可用性下降数据库连接池占满02:25失败率 37% → 60%网关层重试风暴Redis分布式锁超时定时任务重复执行02:38下单链路雪崩所有依赖服务线程池打满Pod健康检查失败被连续重启最典型的两个人间惨剧现场一是同一个订单被扣了两次库存因为订单服务和库存服务之间的事务补偿逻辑本身就有缺陷失败重试时没有做幂等校验二是支付成功但订单状态还是待支付支付回调处理线程全部阻塞在数据库连接获取上回调消息被跳过之后没有重新入队。1.2 排查链路从调用链到数据库连接池凌晨三点打开链路追踪系统下单流程的全貌把我看懵了——一次创建订单的请求内部竟然串了12次RPC调用而且大部分是串行调用。从订单服务进去先调商品服务查SKU信息再调库存服务做预占然后调会员服务拿用户等级接着调优惠券服务计算价格再调积分服务做冻结……每一步都在等网络I/O只要有一个依赖慢整条链路的耗时就线性叠加。更离谱的是排查中发现订单服务调用会员服务和积分服务时HTTP客户端根本就没配超时时间。默认连接超时是30秒读超时也是30秒。数据库连接池最大连接数50其中一个慢SQL把连接池占满之后所有需要查库的请求全部排队线程池WorkQueue塞满新的请求直接被Tomcat拒绝。排查结果汇总成三个根因的时候团队气氛很凝重服务拆分粒度太细调用链长且串行一个明显的长尾依赖就能拖垮全链路。微服务之间调用普遍缺少超时、重试上限、熔断降级配置失败自动重试还都用的默认参数失败越多重试越多重试越多压力越大。基础组件容量评估缺失数据库连接池、线程池、Redis连接池都是“估的”没有经过大促压测摸上限。这三个问题没一个新鲜全是微服务落地时最常犯的错。它们叠加在一起就产生了一个雪崩放大器。1.3 雪崩的放大机制线程池、健康检查和重试风暴很多文章讲雪崩会从“服务A挂了拖垮服务B”这个宏观角度讲但故障现场的真实传导路径是这样的库存服务因为慢SQL导致数据库连接池打满库存服务接口的P99从50ms涨到5秒。订单服务调用库存服务的线程本来应该在200ms内释放现在全堵在等待响应上Tomcat的业务线程被一个上游依赖慢慢吃光。线程被吃光之后后面来的请求全部在队列里排队订单服务自己的接口也开始超时。关键点来了订单服务作为下游服务它长期没有在健康检查接口上及时响应K8s里配置的存活探针连续三次失败直接把Pod给重启了。重启过程需要从注册中心下线再上线期间网关还在把新请求负载均衡过来一部分请求打到了正在终止的Pod上瞬间失败于是网关触发重试把请求再打到别的Pod。重启这台机器把单点故障扩散成了全链路故障。重试风暴是最容易被低估的放大器。网关层默认失败重试2次每个重试请求进来之后服务内部又因为数据库连接池满而失败然后服务内部的RPC客户端又重试2次……一个原本只需要几毫秒的查询请求最终打到底层数据库的次数可能是6到8次。2. 分布式和微服务两个层次的概念不是小名和大名2.1 分布式讲的是“运行形态”把那次故障定性清楚之后我们才开始认真复盘“分布式和微服务到底差在哪”。分布式是一个运行形态概念。只要系统由多个节点组成节点通过网络通信协作完成任务那它就是分布式系统。节点可以是一台物理机上的多个进程也可以是跨地域的多个数据中心。MapReduce、HDFS、GFS、MinIO分布式存储、Redis Cluster、Kafka、ZooKeeper这些都是典型的分布式系统。它们不一定拆出了“用户服务”“订单服务”这种业务服务但它们在网络分区、数据一致性、节点故障容错、时钟同步这些问题上花了大量精力。Hadoop伪分布式就是最好的入门例子——一台机器上跑多个Java进程模拟NameNode、DataNode、ResourceManager、NodeManager各自独立部署节点之间的通信走本地网络栈。它规模再小、再“伪”本质上也已经是一个分布式系统了。这就引出一个关键判断分布式不关心你拆成几个业务微服务它关心的是多节点协同工作时怎么保证正确性和可用性。2.2 微服务讲的是“架构组织方式”微服务解决的是另一个层面的事怎么把一个复杂的单体应用按照业务能力拆分成多个可以独立开发、独立部署、独立扩展的小服务。但它不是一个纯逻辑层面的概念。微服务跑起来之后服务之间要通信要注册发现要配置管理要有网关路由要处理分布式事务要保证数据一致性——这些全都是分布式系统的经典问题。所以更准确地说微服务是一种架构风格这种架构风格落地后系统必然以分布式系统的形态运行。一个可以粗暴理解的公式微服务 围绕业务能力做服务化拆分 分布式系统落地 组织架构匹配。如果用一张表来区分它们的关注点大概是这样的对比维度分布式微服务核心问题多节点协作、数据一致性、容错、性能业务拆分粒度、服务边界、独立交付、团队自治关注层次运行态、基础设施架构设计、组织协同典型技术RPC、消息队列、分布式存储、分布式锁、分布式事务Spring Cloud、Dubbo、Kubernetes、Service Mesh故障传导网络分区、节点宕机、时钟漂移服务依赖链、重试风暴、配置漂移可量化指标可用性、一致性、吞吐、延迟部署频率、服务数量、MTTR、故障爆炸半径2.3 常见误解把“上了微服务”等同于“做好了分布式”那次故障复盘里最扎心的一句结论是我们上了微服务但分布式该做的功课一样没做。微服务框架给了我们注册中心、配置中心、负载均衡、声明式HTTP客户端很多人就以为这些组件摆上去系统天然就具备分布式能力了。但分布式系统里最难的那几道题框架一个都帮不了你订单服务和库存服务之间的状态一致性怎么保证框架不管。多个节点同时执行定时任务怎么保证只有一个节点执行框架不管。缓存和数据库双写不一致怎么办框架不管。服务A调服务B失败重试多少次重试会不会放大流量框架给了默认值但没有结合业务评估。节点之间分布在哪里网络分区了怎么处理框架也不管。所以当你觉得“微服务化之后系统反而更不稳定”的时候大概率不是微服务模式错而是你只用了微服务的壳没做分布式的核。3. 订单链路里真正的分布式难题锁、事务、一致性3.1 分布式锁Redis的set nx ex为什么不够用订单服务在微服务化之后原来的单机定时任务直接原地部署成了多副本。任务调度框架虽然做了分片但总有一些任务因为分片粒度不够细多个节点会同时执行同一批订单。经典场景就是支付超时关单凌晨跑批节点A和节点B同时扫到同一批超时订单然后同时去调用支付服务关单、同时去扣减库存。当时排查日志发现同一个订单号在日志里被两个不同IP的节点各处理了一次。幸运的是关单接口做了幂等没有给用户重复退款但优惠券返还逻辑没做幂等导致一个订单被返还了两次券。分布式锁最基础的方案就是Redis的原子操作SET lock:order:123456 818e17de-3c42-4f9a-8f0a-3c3f1c6e9f5f NX PX 30000NX保证只有key不存在的时候才能设置成功PX设置自动过期时间value放一个唯一ID用于释放锁的时候校验所有权。释放锁的时候用Lua脚本保证“判断删除”的原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这套方案看起来简单坑却一个都不少锁过期时间设短了业务还没执行完锁就自动释放另一个节点马上拿到锁并发执行。锁过期时间设长了持有锁的节点宕机其他节点要等很久才能抢到锁。释放锁之前要先GET判断value是否是自己写的ID否则有可能误删别的节点刚抢到的锁。实际项目中真正降低风险的做法有三个缺一不可一是把锁的value设为全局唯一的请求ID释放时严格比对二是给锁续期Redisson的看门狗就是干这个的每10秒续期一次直到业务执行完主动释放三是尽量压短临界区代码不要把RPC调用和慢SQL放进锁里。3.2 分布式事务扣库存和订单状态的一致性方案订单超时雪崩那晚最要命的数据问题还不是锁失效而是订单状态和库存数量出现了不一致。用户提交订单之后订单服务要插入一条订单记录库存服务要预扣一件库存。这两个操作跨了两个服务、两个数据库不可能靠本地事务保证原子性。我们最终落地的是可靠消息最终一致性方案核心思路是本地事务和消息发送在一个事务里完成。订单服务启动一个本地事务插入订单记录同时向消息表里写入“已扣库存”事件事务提交后通过消息队列把事件发给库存服务库存服务消费消息后幂等地扣减库存。这套方案有个关键点——消息发送和业务操作的顺序。用RocketMQ的事务消息实现才是“先执行本地事务、事务提交后再发送消息”的正确姿势如果先发消息再执行本地事务消息发出去之后业务失败库存服务提前扣了库存就产生了不一致。如果只插入本地消息表但没有可靠的投递机制消费者没收到消息同样会漏扣。另外要特别强调幂等不是可选优化而是硬性要求。消息可能被重复投递消费端必须以业务主键做唯一约束确保重复消息不会导致重复扣减。如果场景对实时一致性要求更高比如金额敏感的核心交易链路可以考虑Seata的AT模式或TCC模式。AT模式通过全局锁和undo_log实现自动补偿TCC模式则需要业务方显式实现Try、Confirm、Cancel三个方法。它们各有代价AT模式侵入小但全局锁容易成为性能瓶颈TCC性能好但实现成本高、对业务侵入大。3.3 幂等设计、分布式ID和缓存一致性聊完锁和事务其实已经进入分布式系统的深水区了。订单链路里还有三个看起来不起眼、但稍微没做好就是线上事故的问题。幂等设计。支付回调、定时关单、消息消费这些重复触发的场景必须有幂等兜底。最简单有效的方案是给每笔业务分配一个全局唯一的业务键比如“orderId 业务类型”在这张业务表上建唯一索引数据库层面直接拒绝重复操作。比先查再判断的Check-Then-Act方式可靠得多因为查询和插入之间永远存在竞态窗口。分布式ID。订单号如果用数据库自增ID分库分表之后就会冲突。雪花算法Snowflake是主流方案64位里分时间戳、机器ID、序号三段单机一毫秒能生成上千个ID。但注意它依赖机器时钟时钟回拨就可能产生重复ID实际部署时要么开启NTP的步进修正要么在算法里留一个时钟回拨的等待或备用ID段。缓存一致性。订单列表、库存数量、商品详情这些数据在Redis里都有缓存。最接近正确性的方案是“Cache Aside 延迟双删”先更新数据库再删缓存隔几百毫秒再删一次用来处理并发读请求把旧数据写回缓存的问题。但延迟双删也只是降低概率要彻底解决得靠版本号或订阅Binlog异步刷新缓存。这些内容列出来不是为了凑篇幅而是想说明一件事上面任何一个问题你在“分布式系统”的教科书里都能找到对应的章节。它们不是微服务框架帮你解决的问题是无论你用不用微服务只要系统拆成了多个节点就必须直面并且亲手解决的难题。4. 微服务没背锅雪崩治理和架构边界4.1 微服务治理清单超时、重试、熔断、隔离那次故障之后稳定性和架构组联合定了一套治理基线业内叫微服务三件套加两件套。我把它们当成排查微服务故障的第一反应清单这里直接分享出来。超时。所有RPC调用必须显式设置连接超时和读超时。HTTP客户端里connectTimeout一般设800msreadTimeout一般设3000ms具体按业务接口的P99.9来调整。原则是读超时不能小于这个接口正常情况下的最长耗时否则误杀也不能太大否则线程被拖死。重试。重试必须设上限超时重试最多2次且每次重试间隔要递增退避。重试只适合幂等接口订单创建这种非幂等操作不能随便重试。最关键的一条是下游已经雪崩的时候重试只会加速雪崩。所以重试要有快速失败机制比如连续重试失败几次之后熔断器打开直接不再发起请求。熔断。熔断器基于滑动窗口统计错误率和慢调用比例比如最近10秒内错误率超过50%直接熔断5秒。这个参数需要根据业务容忍度调整库存服务对一致性要求高阈值可以设严格一点积分服务属于非核心链路熔断阈值可以更敏感尽快切断依赖。隔离。线程池隔离是最有效的防雪崩手段。每个下游依赖分配独立的线程池普通查询线程池大小设在核心数的一半到一倍之间队列容量按实际吞吐计算不要用无界队列。Semaphore隔离适合耗时短、对并发数要求高的场景但没法隔离网络等待。限流降级。网关层按服务维度做QPS限流超过阈值直接返回兜底结果或排队等待。核心链路里库存服务挂了可以降级为“显示有货但下单时校验”积分服务挂了可以暂时不累积积分保留最基本的交易链路。4.2 可观测性没有监控的微服务等于盲飞雪崩复盘会上我们问了一个很致命的问题12次RPC调用串起来的下单链路故障发生前谁能说出当前系统的真实吞吐上限没人能回答。因为我们没有一套完整的可观测性体系。微服务场景下可观测性分三个支柱缺一不可链路追踪。一次请求跨了6个服务全靠Trace ID把所有节点串起来。完整的调用链数据可以定位每个服务实际耗时、哪个环节最慢、哪个节点在抛错。Zipkin、SkyWalking、Jaeger都行关键是全链路必须打通网关、业务服务、消息队列、数据库访问全都要埋点。Metrics指标。每个服务的QPS、错误率、P99延迟、线程池活跃数、数据库连接池使用率、GC停顿时间这些指标必须按服务维度、实例维度、接口维度落库。Prometheus Grafana是标配。日志聚合。ELK或Loki统一收集各节点日志用Trace ID关联查询能在一个界面里串出完整日志链。没有这三板斧在复杂依赖链里排查一个超时问题等同于蒙着眼睛在迷宫里找出口。有了之后大部分问题的定位时间能压缩到分钟级。4.3 高并发验证迁移到云上之后的压测经验复盘会议之后团队把基础设施从自建机房迁到了云上压测人员用JMeter配套脚本对新环境做高并发验证。这个过程有几个经验非常值得分享。我们在单节点K8s环境上跑整套微服务类似若依微服务的整套环境时感觉很顺但一旦上云、上多节点集群很多隐性瓶颈立刻暴露出来。JMeter脚本跑第一阶段阶梯加压500并发的时候一切正常1000并发时订单服务的P99直接翻了4倍。查看监控发现瓶颈根本不在业务代码而是云上数据库实例的连接数上限比原来自建库小连接池打满后请求就在等待获取连接。压测还有一个常见教训脚本里的用户数并不等于真实并发。JMeter线程组里1000个用户可能需要配合适当的Ramp-Up周期才能模拟出平滑的真实流量。另外压测一定要从网关层打流量不要直接打到服务A的接口上否则就绕过了负载均衡、网关限流、链路追踪埋点结果不具备参考价值。5. 上不上微服务决策依据和个人经验5.1 不要为了“分布式”而上微服务那场故障过去几个月后隔壁项目组新起一个业务系统。技术同学方案评审时直接拿出了微服务架构图注册中心、网关、订单服务、用户服务、商品服务拆了一整套。我问了他们三个问题这个系统目前的预估日活是多少团队规模几个人哪些模块有独立的扩展需求用户量和订单量的增长曲线是否明显不同团队有没有人负责运维基础设施负载比如K8s、注册中心、链路追踪、日志平台沉默了很久。最后把微服务架构改成了分布式单体——在一个应用里按模块拆分包结构模块之间走本地方法调用数据库单独部署缓存和消息队列用云上的托管服务。这个系统到现在运行得很稳。单体不是贬义词。单体应用纵向扩展Scale Up一台大机器配合读写分离、缓存、消息异步化完全可以承载绝大部分中小业务的流量。只有当你确认了下面这些条件之后才需要认真考虑微服务服务之间存在明显的资源隔离或独立扩缩容需求比如查询服务需要比写入服务更多的副本。团队规模超过两个小队需要按业务域划分开发、独立发布权限。组织中存在多语言技术栈混用的强诉求比如算法团队用Python写推荐服务业务团队用Java写交易服务。时间充裕有专人补齐分布式基础设施和可观测性能力。5.2 分布式能力按需购买不必一步到微服务很多刚接触分布式架构的同学会有一种错觉分布式 微服务 必须要学一套完整的Spring Cloud全家桶。其实不是的。分布式能力可以按需增量引入。单体应用加一个Redis做分布式缓存它就具备了分布式系统的部分能力加一个消息队列削峰填谷它又往前走了一步主库从库做读写分离它已经是一个非常典型的分布式存储架构了。HDFS、GFS这些分布式文件系统Hadoop伪分布式和单节点K8s上的微服务环境本质上都是在用有限资源模拟多节点协作目的就是让你用最小的代价理解分布式系统的通信、容错和一致性难题。反过来微服务也没有必要追求一步到位。方案评审时把服务拆分粒度定粗一点比如按交易域、用户域、商品域拆3个服务先跑通一整套链路追踪、日志聚合、限流熔断体系后续再根据实际痛点细化比一次性拆20个服务要稳妥得多。5.3 故障复盘后的长期感受那次订单超时雪崩事件之后我很长时间里都会对新同学反复强调一句话微服务不是把单体应用变成多个小单体而是把单体应用里的复杂度拆分成了分布式系统的复杂度。如果只是把原来的Service类加一层HTTP包装拆成多个服务部署那你得到的不是微服务架构而是一个故障率更高的分布式系统。真正的微服务改造拆服务只是第一步同时要搭建注册中心、配置中心、监控告警、链路追踪、日志平台、熔断限流还要约定幂等规范、分布式事务方案和容灾演练机制。前两天看到团队架构评审群里有人提问“单节点K8s上跑微服务整环境怎么不停服不丢数据地迁移到云上”我不由想起年初那次熬夜复盘。其实迁移的技术难点背后还是同样的问题你拆出来的服务之间依赖关系和数据一致性有没有梳理清楚基础设施的容量边界有没有压测验证过这些问题不解决就算迁移到再大的机器上雪崩一样会发生。这就是我为什么坚持把分布式和微服务分开看待。它们不是一件事情的两个名字而是两个必须同时做对的维度。架构图上的微服务方案可以画得很漂亮但跑得稳不稳、挂得快不快取决于你有没有真正解决分布式系统抛给你的那些硬问题。
返回列表