资讯详情

资讯详情

电商系统分布式架构改造:从单机到微服务的实战演进

1. 从单机到多节点这次改造最初要解决什么把网站从单机搬到多节点在外人看来好像是“上规模”的标志可真正动手做这件事的人心里都清楚分布式架构从来不是目的而是被业务逼出来的必然选择。我这次负责改造的“某在线商城系统”最初就部署在一台8核16G的云服务器上数据库、应用服务、静态资源全部挤在一块。系统日均请求量大概在1200QPS左右促销时段会冲到3000QPS数据库CPU常年徘徊在80%以上慢查询日志里一堆排序和聚合语句。每到营销活动开始前团队的第一反应不是优化代码而是先准备一台备用机随时准备手动切换。这种状态维持了差不多半年大家心知肚明问题在积累单台机器一旦宕机全站不可用数据库磁盘占用接近85%扩容只能靠加磁盘应用重启一次要三分钟期间请求全部超时。真正让我下决心做分布式改造的不是某次故障而是一次普通的商品详情页接口抖动——那次接口平均响应时间从200ms一路涨到3秒排查到最后发现是数据库连接池被一个批量导出任务占满。这种“互相拖累”的问题在单机架构里无解因为你没有办法让订单服务和商品服务各自隔离资源。于是分布式改造被正式提上日程。1.1 流量画像与容量瓶颈做改造之前团队先花了一周时间做流量画像。我把nginx的access log拉出来按接口维度做了聚合分析结果很有意思真正消耗数据库资源的并不是用户最常点的首页和详情页而是后台管理系统里的报表查询和商品批量导入功能。这些功能一天可能只被调用几百次但每次查询都是全表扫描或者批量写入几千条记录数据库的CPU和IO瞬间被打满。也就是说单机架构下“慢接口拖垮快接口”的现象本质上不是性能问题而是资源隔离问题。我们还统计了核心链路的依赖关系一次完整的下单操作需要调用用户服务、库存服务、订单服务、支付回调以及发送短信通知和更新会员积分。这个链路在单机架构里是多个模块之间的本地方法调用看起来很快但每次调用都共享同一个进程内的线程池和数据库连接池。一旦某个模块出现阻塞整个tomcat的工作线程就会被占满其他业务的响应时间全部跟着遭殃。容量瓶颈的计算也很直观8核机器上tomcat默认最大线程数是200假设每个请求平均占用线程300ms那这台机器理论上的极限吞吐就是200除以0.3秒约等于666QPS。而我们平时的流量就已经是1200QPS早已超出理论极限之所以还“能用”是因为很多请求在静态资源层就被缓存掉了真正打到应用层的只有一小部分。1.2 分布式架构的设计目标和边界做技术方案时我给自己定了三条边界后来回头看这三条边界救了这个项目好几次。第一不追求大而全的微服务化而是把业务拆成六个可以独立部署的服务原则是“能不拆就不拆拆了就一定要独立数据库”第二所有涉及钱和库存的操作必须保证强一致其他场景允许最终一致第三改造过程要保持系统可回滚也就是说新旧架构必须共存至少一个版本周期不能搞“一步到位”的切换。这三条边界在很大程度上决定了后续的架构选型。例如六边形仓库里的用户、商品、订单、库存、营销、支付六个服务每拆出一个服务都对应一张可以独立扩容的数据库。网关层引入统一路由后前端无感知服务层通过注册中心互相发现调用关系从硬编码的HTTP地址变成了服务名级别的路由。改造分两个阶段走完第一阶段先把用户、商品、订单三个核心服务拆分出来数据库从单库拆成三个独立库同时接入缓存和消息队列解决跨服务一致性问题第二阶段再把库存、营销、支付服务拆出来并补充了全链路的可观测性体系和故障演练机制。我见过不少团队做分布式改造上来就买一堆中间件把服务拆得特别细结果部署、排查问题、数据一致性的成本反而比单体更高。这次我的做法是先从“资源隔离”的角度切入先用Spring Cloud Alibaba体系搭注册中心和配置中心服务间调用先用OpenFeign加Sentinel限流数据库拆分用ShardingSphere的分库分表能力异步场景用RocketMQ。这套组合的好处是文档多、社区活跃、团队学习成本可控而且所有组件都可以从单机模式平滑切换成集群模式为后续扩展留了余地。2. 整体架构设计以应用网关和注册中心为骨架改造后的整体架构对外只暴露一个统一入口也就是API网关。所有的前端请求、APP请求、第三方开放平台请求统一先打到网关层由网关负责鉴权、限流、路由转发再分发到后端的各个微服务。这样做的好处一眼就能看出来你不用在每个服务里都写一遍鉴权和限流逻辑安全策略的修改只需要在网关层完成。网关后面是六个微服务它们之间通过注册中心互相发现不依赖固定的IP地址。每个服务都有独立的数据库服务之间不允许直接访问对方的数据库表数据交互必须走接口或者消息队列。这套架构的核心骨架其实就是两个东西网关决定了流量怎么进来注册中心决定了服务之间怎么找到彼此。以订单服务和库存服务为例在下单链路上订单服务需要扣减库存它不需要知道库存服务的IP和端口只需要向注册中心发起一次服务发现调用拿到一个可用的库存服务实例列表然后通过负载均衡策略选一台发起请求。当库存服务从2个实例扩展到5个实例时订单服务侧不需要做任何配置变更这种弹性能力是单机架构完全不具备的。2.1 服务拆分按业务域而不是按代码包服务拆分看起来是个技术活其实第一步是业务梳理。我们当时画了一张业务域地图把整个商城系统的功能分为用户域、商品域、订单域、库存域、营销域、支付域。分域的原则不是按代码分层而是按“数据归属”来分这个域的数据最终应该由且仅由这个域的服务来读写。举个例子用户的收货地址在用户服务里订单表里只存地址快照不通过外键去关联用户表中的地址记录订单表保存订单金额冗余字段不下发到客户端后每次实时计算商品服务维护价格和库存表但订单服务只有在发起下单扣减库存时才调用库存服务接口。拆完之后六个服务的责任边界非常清晰团队内部的开发归属也符合这个划分。但拆服务最怕的是“逻辑深层依赖”比如下单时要同步调用用户服务查会员等级算出折扣后再创建订单这个跨服务调用一旦失败订单就创建不了。我们最后把会员等级折扣做成了“可降级”下单时先读缓存中的会员等级缓存读不到就返回默认折扣不和用户服务的实时数据强绑定保证核心下单链路不受影响。这种取舍很关键——分布式的核心追求不是把所有东西都做成强一致而是让最重要的链路保持稳定。2.2 注册中心与配置中心选型服务拆完之后第一个要解决的就是“服务之间怎么互相找到”。我最早考虑过直接用nginx做静态服务列表管理但微服务化之后实例数量会动态变化手动维护nginx upstream不现实。后来对比了Eureka、Consul和Nacos最终选了Nacos。理由有三点一是Nacos同时支持服务注册发现和配置管理一个组件解决两个问题二是它支持从Spring Cloud Alibaba体系里零成本集成团队不用额外学习一套新API三是它在AP模式下的表现适合我们的业务场景服务注册中心允许短暂的数据不一致但必须保证高可用。配置中心这一点容易被新手忽略但对后续运维非常重要。我们把每个服务的数据库连接池大小、缓存超时时间、限流阈值、开关配置全部迁移到Nacos配置中心通过配置的RefreshScope注解支持动态刷新。这样在线上出现问题时不需要重新发版直接修改配置就能调整参数。比如有次某个服务的数据库连接池设置得过大导致数据库连接数被打满我直接通过配置中心把连接池从100调成50几秒钟就生效避免了紧急发版。2.3 统一网关的出入口设计网关选型时对比了Spring Cloud Gateway和Zuul最终用了Spring Cloud Gateway。它基于WebFlux响应式编程模型底层是Netty性能和资源占用都比Zuul 1.x好不少。我在这台网关上做了几件固定的事全局的鉴权过滤器、灰度发布的路由规则、全局限流。集中式鉴权就是把token校验、签名校验、黑白名单集中到网关层下游服务默认信任网关传过来的用户身份信息不再重复解析token。这样服务之间的调用要想伪造身份难度会高很多因为内部调用统一通过服务名和签名机制认证。限流这一块我用了Sentinel的网关限流模块规则存到Nacos里动态下发。针对不同的API路径配置了不同的阈值比如登录接口限流每秒钟100次商品详情接口限流每秒钟2000次下单接口每秒钟500次。超过阈值的请求直接返回“系统繁忙”的友好提示避免流量瞬间打到后端服务导致雪崩。灰度发布则是通过网关路由规则实现的把一部分用户请求按userId的hash值路由到新版本服务等观察指标稳定后再逐步放量到100%。这套机制后来在商品服务的重构上线中发挥了巨大作用新版本和旧版本共存了整整一周直到监控数据没有出现任何异常才把旧版本下线。3. 数据层改造分库分表、缓存与一致性服务拆分之后最难的其实是数据库。单机架构里所有表都放在同一个库服务拆分后必须跟着把数据拆开。拿订单服务来说订单表、订单明细表、支付流水表这几张表一开始拆出去的时候数据量很快到了千万级别单表查询明显变慢。我们在订单服务里引入了ShardingSphere做分库分表按order_id的哈希值分成四个库、每库四张表。这个拆分粒度是综合考虑后确定的按当前数据量增速估算两年内单表数据量不会超过500万查询性能可控分片键用的是order_id这样同一个订单的明细一定会落在同一张表里避免了跨分片的join操作。但分库分表也带来一个约束所有查询条件必须以分片键为前提。比如“查询某个用户最近十笔订单”如果按order_id分片就查不了因为不知道每个订单的order_id只能去所有分片广播查询。为了解决这个问题我们额外维护了一张“用户与订单的映射索引表”这张表以user_id为分片键查询用户订单时先查映射表拿到要查询的分片列表再精确到分片查询。这是分库分表后的一个经典套路要么冗余数据要么维护映射索引没有免费的午餐。3.1 数据库分片与读写分离除了分库分表读写分离也是异步数据流中很重要的一环。主库负责写从库负责读通过MySQL主从复制同步数据。我们把用户服务、订单服务的读流量切到从库之后主库的压力下降了一大半。但读写分离也有延迟问题——用户支付成功后订单状态在主库更新前端页面马上刷新如果这时候页面读的是从库可能读到旧数据表现为“订单还是待支付状态”。这个问题的典型解法叫“写后读一致性”做法是从库延迟超过阈值时读请求降级到主库或者在关键状态流转之后设置一个短暂的路由开关让该用户接下来几秒的读请求强制走主库。关于分片策略这里有个值得记下来的细节不要等到单表数据量破千万才开始分片分片越晚成本越高。我们的经验是当单表预估超过300万行时就要考虑拆分了因为数据迁移、应用改造、索引重建的窗口期会随着数据量指数级增长。而且在做分片设计时一定要先想清楚核心查询场景分片键一旦定下来很难改每次改分片键都要做全量数据迁移。如果业务查询维度是多样化的宁可增加冗余表也不要试图用一个分片键解决所有查询。3.2 缓存策略与热点处理数据库压力降下来很大一部分功劳要给缓存。我们用的是Redis集群三个主节点三个从节点哨兵模式自动故障转移。缓存的使用有几个层次最基本的是商品详情缓存key是商品IDvalue是组装好的商品数据JSON过期时间设置为30分钟加随机扰动防止缓存雪崩再往上一层是“热点商品缓存”系统根据最近五分钟的访问量自动识别热点商品对热点商品延长缓存时间到两小时还有一层是“防击穿缓存”对于查询结果为空的数据设置一个短时间的空值缓存防止不存在的数据每次查询都穿透到数据库。缓存与数据库的一致性我采用了一个被验证过多次的模式Cache Aside Pattern也就是先更新数据库再删除缓存。为什么不是先更新缓存再更新数据库因为并发环境下非常容易出问题。比如两个线程同时更新同一个商品价格线程A更新数据库价格为100线程B更新数据库价格为200如果先更新缓存可能出现缓存最终保留的是旧值的情况。而“先更新数据库、再删除缓存”配合缓存的懒加载策略即使删缓存失败最多就是下一次请求多查一次数据库把正确的结果重新加载进缓存一致性风险要小得多。这里需要注意删除缓存时如果有并发读请求刚好在删之前把旧数据写回了缓存就可能出现一段时间的不一致缓解手段是给缓存设置一个较短的过期时间兜底。3.3 分布式事务的取舍订单服务扣减库存、创建订单、生成支付流水这三个操作涉及到订单库和库存库两个数据源单机上的本地事务已经无法保障跨库的原子性。我们的做法是引入RocketMQ的事务消息把“创建订单”和“扣减库存”拆成两个步骤。订单服务先发送一条半消息同时执行本地事务创建订单本地事务成功后提交半消息库存服务消费这条消息执行扣减库存扣减失败时通过消息重试反复尝试。这套机制的核心原理是把分布式事务变成一个本地事务加一个消息的可靠投递。本地事务保证订单数据一定是正确写的RocketMQ的定时重试机制保证扣减库存的消息最终会被消费。有一个地方要特别小心库存服务的消息处理逻辑必须是幂等的因为消息可能在网络超时后被重复投递。我们的做法是库存扣减表里增加了“消息去重表”用唯一键订单号加消息ID做约束重复扣减时抓住数据库的唯一约束报错直接返回成功不再真正扣减库存。这个细节如果不注意线上很容易出现“订单只下一个库存却扣了两遍”的事故。分布式事务的原则在这套架构里是“能不用就不用”。大多数业务场景根本不需要强一致比如用户下单支付之后营销积分可以延迟增加订单发货之后物流状态的更新延迟几秒完全能接受。只有库存、支付流水这类直接影响资金和资源的操作才需要事务消息来保证最终一致。把“强一致”的范围缩得越小整个系统的吞吐能力和稳定性就越好这个观点我在和不少同行交流时反复验证过。4. 异步化消息队列把链路“拉平”订单创建之后要做的事情非常多发短信通知、推送App消息、更新会员积分、生成财务报表、同步搜索引擎索引。如果在下单接口里把这些操作全部同步做完响应时间会变得不可接受。所以我们在下单成功之后只发送一条“订单创建成功”的消息到RocketMQ让下游的这些模块各自订阅消费把本来串行执行的链路变成并行异步执行。这样下单接口的实际耗时只包含订单入库和扣减库存两部分接口响应时间从原来的800ms降到了150ms左右。异步化还有一个额外好处系统的峰值处理能力不再受限于最慢的那个环节。举个例子某次大促活动瞬间涌入大量下单请求短信服务所在的第三方通道响应很慢如果走同步调用下单接口就得一直等待短信通道的响应改造成异步之后短信服务对慢通道的影响被隔离在下游不会阻塞主流程。消息队列在这里起的是一个“削峰填谷”的作用把瞬间的流量峰值消化在队列里让下游消费方按照自己的处理能力平缓地消费。4.1 异步削峰场景怎么选消息队列市面上常见的消息队列有Kafka、RocketMQ、RabbitMQ、Pulsar我们最终选了RocketMQ。选择理由很少但每条都关键一个是它支持事务消息这是我们实现分布式事务的依赖另一个是它的消息消费支持“顺序消息”同一个订单的消息可以被严格按顺序消费先扣库存再创建订单这种顺序敏感的场景用得上还有一个是它的延迟消息机制可以方便地实现“下单后30分钟未支付自动关闭”这类业务逻辑不需要自己写定时任务。Kafka确实吞吐量更高但它的吞吐亮点主要体现在日志类数据的高效处理事务消息和顺序消息的实现不如RocketMQ成熟。RabbitMQ则更轻量但它在消息堆积、高并发消费场景下的表现不如RocketMQ这种以队列模型为中心的中间件。选型没有绝对的“最好”关键是和你当前的业务形态匹配。我们当时的业务是电商系统事务和顺序性需求强烈RocketMQ几乎是顺理成章的选择。4.2 消息可靠性与幂等消费消息队列用起来简单但真正生产环境里最容易出问题的恰恰是消息的可靠性。消息丢失有三种情况生产端发送时丢失、消息队列存储时丢失、消费端处理时丢失。RocketMQ默认提供了多副本机制Broker的数据在多个节点之间同步复制单节点宕机不会导致消息丢失生产端我们设置了同步发送模式也就是producer.send()方法会等待Broker的确认这个确认可能超时或失败所以生产端还需要对发送失败的MQ消息做补偿——把失败的消息写入一张本地消息表由定时任务扫描重发。消费端丢失消息的场景最典型的就是“先改数据库再提交消费位点”如果业务代码执行过程中抛异常消息消费失败后RocketMQ会默认重试16次。这里有一个特别容易踩的坑消费端业务逻辑必须做幂等控制因为消息消费失败重试时同一消息可能会被消费多次。比如“更新会员积分”这个消息如果第一次执行成功后正好在提交消费位点之前应用宕机了重启后RocketMQ会再次投递这条消息积分就可能被加两次。我们的做法是在消费端落一张“消息消费记录”表以消息ID做唯一键每次消费先查这个记录存在就直接返回成功。4.3 异步化后的链路追踪服务变成异步之后排查问题的难度陡增。以前是A调用B出问题只要看A和B的日志现在是A发消息、B异步消费、B可能再发消息给C中间的调用链是“断”的。所以我们在第二个阶段接入了全链路追踪系统技术选型是SkyWalking。每个请求进来时生成一个全局的traceId通过HTTP头传递给下游的所有同步调用异步场景下生产者在发送消息时把traceId塞到消息的扩展字段里消费者拿到消息后把traceId提取出来继续挂在新的线程上下文里这样一条业务链路无论是同步还是异步都能在SkyWalking的拓扑图上完整串起来。链路追踪上线后我们找到了很多以前根本无法定位的性能问题。比如有次用户反馈“下单成功但等了很久才收到短信”通过链路追踪很快看到消息从MQ发出后短信服务在读取模板配置时有两次多余的数据库查询每次耗时约300ms消费一条消息的总耗时将近800ms。把这些慢查询优化掉之后短信的到达时间从秒级提升到毫秒级。链路追踪不是为“追时髦”上的它直接决定了一个分布式系统能不能被有效运维。5. 容量计算、部署与故障演练分布式架构搭建完成之后下一步就是确定每个服务应该部署多少实例、每台机器的配置调到什么水平。很多团队上分布式系统后的第一个问题不是功能跑不通而是资源规划不合理要么过度配置导致成本翻倍要么配置不足导致上线就被流量打垮。我沿用了一套容量预估的方法先确定核心接口的QPS目标再根据单实例能够处理的QPS反推实例数量。这里面有个前提每个服务在压测环境里要测出自己的单实例极限值不能靠猜。拿订单服务来说压测结果显示一台4核8G的实例在应用层加缓存、数据库走独立实例的前提下单机极限是1000QPS左右。我们预估大促峰值是2000QPS留出30%的冗余目标容量就是2600QPS2000乘以1.3用2600除以1000相当于需要3个实例。再加一个用于故障切换的备用实例最终订单服务部署了4个实例高峰期负载在60%左右这个水位既能保证性能又能给突发流量留出缓冲。集群内通过网关的负载均衡策略轮询访问单实例宕机后流量会自动切到其他实例整个过程用户无感知。5.1 容量预估计算过程我把商品服务的容量预估过程展开一下。商品详情接口是这个服务中最核心的读接口平峰期QPS是1500大促峰值会乘以4倍到6000QPS。Redis缓存命中率在正常时候是90%但大促刚开启的瞬间大量第一次访问的请求会穿透缓存打到应用层此时穿透率最高能到40%也就是2400QPS打到应用。商品服务单实例压测极限是800QPS那目标容量就是峰值2400乘以1.3大约3120QPS除以800等于4个实例再加一个冗余一共5个实例。数据库的容量预估更看重连接数而不是QPS。每个应用实例默认数据库连接池是505个实例就是250个连接数据库实例的最大连接数设置成300预留一部分给后台管理工具和运维操作。连接数和QPS之间也要有关系把每个接口的平均SQL执行时间统计出来估算数据库并发执行的能力。我们的数据库采用一主两从架构主库只承担写流量读流量全部走从库从库的CPU负载在高峰期控制在50%以内这就是个比较安全的健康水位。5.2 发布与回滚策略分布式系统的发布比单机复杂得多最大的变化是不能“下掉所有实例再上新版本”。我们采用的发布策略是滚动发布每次只更新一个实例等新实例健康检查通过后再继续更新下一个实例直到所有实例都完成版本切换。这就需要网关层的负载均衡支持主动摘除实例发布开始时先把要更新的实例从负载均衡列表里摘掉等更新完成并且健康检查通过后再加回来。这套流程已经写成了标准化的Jenkins流水线开发只需要点击“构建”系统会自动完成打包、镜像构建、滚动发布。配置项和代码分开管理之后回滚也变简单了。代码回滚就是重新构建上一个版本的镜像再滚动发布一次配置回滚则直接在Nacos配置中心把配置改回去不需要重发代码。但有两种情况需要特别注意一是数据库结构变更不能跟随代码一起滚动发布必须先执行数据库迁移脚本再发布新版本代码否则新代码启动时会因为缺字段报错二是分布式环境下“回滚”要分成“应用代码回滚”和“数据补偿回滚”两步走比如新版本多写了一张表旧版本读不到回滚后还要把数据补回去这类数据补偿操作通常要提前写成SQL脚本或者补偿任务避免人工手动执行误操作。5.3 故障演练与复盘架构上线前团队组织了一轮故障演练把几个最常见、最影响可用性的故障场景都过了一遍。演练清单包括订单服务一个实例宕机、Redis集群某个主节点宕机、数据库主库宕机、消息队列Broker宕机、某一个下游服务响应突然变慢。每一步演练都要记录“从故障发生到业务恢复需要多少时间”以及“故障期间有多少请求受到影响”这两项数据直接决定了系统的可用性水平。演练暴露出两个问题。第一个是部分服务的连接池配置偏小比如营销服务在正常负载下连接池只需要30个连接但当它对应的下游服务变慢时线程会长时间占用连接导致其他请求拿不到连接出现“连接池耗尽”的连锁故障。我们为此给每个核心服务的Feign客户端配置了独立的线程池并且设置了合理的超时时间例如连接超时1秒、读超时3秒宁可快速失败让上游走降级逻辑也不能无限期地占住线程等待。第二个问题是依赖了“Redis缓存自动过期再重建缓存”这个过程的客户端集中请求一旦缓存同时过期会导致瞬间全部穿透到数据库我们在缓存层面加了一个分布式锁重建缓存并给不同的缓存key设置了不同的过期时间避免全局同时过期。6. 常见问题排查与避坑实录分布式系统的运营维护跟单体时代的最大区别就是出问题时你面对的是一堆机器、几十个日志文件、无数条调用链如果排查思路不清晰很容易被表面现象误导。这里列几个我们在线上真实遇到的典型问题每个都对应着一种排查方法和预防措施。第一个问题服务调用偶尔超时但不是每次都会失败。排查后发现原因是服务实例在发布过程中旧实例下线时还在处理中的请求被强制中断新实例又还没有完全就绪导致网关转发出了一小段空窗期。解决方法是给服务实例增加优雅停机机制收到关闭信号后先通知注册中心下线自己的服务地址再等待正在处理的请求完成最多等待15秒最后才关闭进程。网关侧则配置了重试策略当一次请求转发失败时自动重试下一个可用实例。但要特别小心的是写操作的接口不能盲目重试比如扣款接口重试可能导致重复扣款所以重试策略只应用在查询类接口上。第二个问题缓存击穿导致数据库瞬时压力剧增。原因是某个热点商品在直播中被反复推荐大量新用户第一次访问缓存里没有数据全部打到数据库。解决方法是热点数据识别和Singleflight机制。我们在商品服务里加了一个“热点发现”模块每秒钟统计一次各商品的访问量瞬时访问量超过阈值的商品自动进入热点缓存池把过期时间延长到2小时。同时引入Singleflight机制当多个并发请求同时查询同一个缓存key时只有一个请求真正去数据库加载数据其他请求等待第一个请求的结果返回。这样即便出现缓存没有命中数据库的压力也只会增加一次查询而不是N次。第三个问题消息队列消息堆积严重消费端无论如何也追不上生产速度。排查后发现不是消费能力不够而是消费端逻辑里有连接外部系统的阻塞操作。积分服务消费每条消息时要调用会员系统的接口查询等级这个接口单次耗时在500ms以上而且没有超时控制。后来我们把外呼操作改成异步批量调用消费线程不再等待外部响应堆积量在半小时内被消化掉了。这个话题想提示的是消费端的逻辑一定要保持轻量凡是涉及外部依赖的操作优先做批量聚合或者异步化处理消费速率才能稳步提升。6.1 线上故障排查速查表我把自己经验中觉得最值得记录的排查套路整理成了一张表供做分布式系统的同行直接参考故障现象优先排查方向常用工具/手段接口响应变慢但QPS未增长数据库慢查询、锁等待、连接池耗尽MySQL慢查询日志、Druid连接池监控、jstack部分请求成功部分请求失败负载均衡是否摘除异常实例、网关重试设置Nacos服务列表、网关日志、健康检查接口缓存命中率突然下降缓存是否集中过期、key是否被误删、新版本代码是否改keyRedis监控大盘、缓存命中率指标消息堆积不消费消费者是否宕机、消费逻辑是否有外部阻塞、是否有死信RocketMQ控制台、消费者日志、消费位点监控数据库CPU突然升高慢SQL、缺少索引、全表扫描慢查询日志、performance_schema、EXPLAIN服务消费超时异常上游服务线程池是否满、下游是否阻塞、网络问题链路追踪Trace、Sentinel监控、ping链路延迟排查时的一个原则也是我在这个项目里学到的教训是“先看监控数据再翻日志最后才是改代码”。分布式系统的信息分散在各个节点里没有监控数据做方向指引直接把日志翻一遍往往会淹没在海量信息里。我们的监控大盘覆盖了每个服务的QPS、响应时间、错误率、线程池活跃数、数据库连接池活跃数以及JVM的内存和GC情况。任何指标出现异常先确定是哪个节点、哪个服务、哪个时间点开始异常的再顺着链路追踪去看那个时间段的完整调用链定位速度会快非常多。6.2 分布式环境下线程与日志分布式环境下有两个细节经常被忽略但线上出故障时它们往往是关键。第一个是线程池的命名和隔离。在Spring Boot项目里如果我们创建的线程池不显式地指定一个有意义的名字线程池里的线程会全部叫pool-N-thread-M出现线程问题排查时完全不知道哪个业务在用这些线程。我给每个线程池都设置了业务前缀比如订单处理线程池命名成order-process-thread-库存扣减线程池命名为stock-deduct-thread-这样用jstack抓线程栈时一眼就能分辨出是哪个业务逻辑在占用线程。线程池还要做好隔离不同优先级或不同重要程度的业务不要共用同一个线程池否则一个慢业务会把另一个核心业务的线程池都占满和最初单机里的“互相拖累”问题没有任何区别。第二个是日志的标准化。分布式系统里日志不打好统一格式排查问题基本上等于睁眼瞎。我们规定每个服务里所有的业务日志必须包含traceId、用户ID、业务ID这几个字段日志级别严格区分业务异常打WARN系统异常打ERROR框架错误打ERROR。之所以强调这个是因为线上用户反馈问题时客服往往只能拿到一个粗略的时间点和描述如果能通过用户ID在日志系统里搜索到对应的全部链路日志问题定位会从“大海捞针”变成“按图索骥”。日志系统我们还接入了日志平台做集中式采集各服务以文件方式输出日志到本地再由agent采集到日志平台支持按关键词全文检索和按时间范围过滤。6.3 避免过度设计什么东西不需要分布式最后必须说一点容易被人忽略的实话不是所有功能都适合拆成微服务也不是所有数据都需要放进缓存和消息队列。我们这个系统的几个后台管理的报表功能本来也有人提议拆成独立服务加分布式缓存被我拦下来了。原因是这些功能近乎纯内部工具日访问量极低对实时性要求也不高拿单体架构里的定时任务加报表导出完全能解决。拆成独立服务后不仅要额外部署计算资源和应对数据同步问题还会因为“跨服务查询数据”引入数据接口的排期和调试成本完全是得不偿失。分布式架构的美妙之处在于“伸缩”而不是“拆分”如果只是为了让架构图好看就无限划分最终只会增加运维负担。我的建议是首先每个服务都要能独立演进如果两个服务永远是一起发布、一起扩容那它们本来就应该是一个服务其次每个服务的数据必须能独立读取如果一个服务的数据一定要通过另一个服务的接口才能查到这个边界是失败的最后优先考虑集群化部署现有单体通过负载均衡和缓存把它变成分布式而不是一上来就把代码拆成微服务。这套“够用就好”的思路是我在多次踩坑之后逐渐形成的。在我个人的实际体验里分布式架构带来的最大变化不是在技术上而是在团队协作和故障处理的方式上。以前一个系统几个人一起改互相影响的概率很大现在服务边界清楚了每个小组负责一块发布也不互相牵制了。这类改造项目往往不是一劳永逸的后续你可以在这个基础上继续做容灾多活、弹性伸缩、甚至Serverless化但底子是否打得结实取决于改造初期对业务边界的理解和那些“不做分布式”的克制。希望这篇记录能给你提供一些可复用的判断依据。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →