微服务拆分踩坑实录:从单体到微服务,我后悔的5个决定
本文记录了我在实际项目中将单体应用拆分为微服务架构时踩过的坑希望能帮你少走弯路。前言2024年初我们团队接到一个任务把一个运行了3年的单体Spring Boot项目拆分成微服务。当时我信心满满觉得不就是拆几个服务嘛。结果三个月后我深刻体会到了什么叫拆分一时爽维护火葬场。今天这篇文章我不讲理论只讲我真实踩过的坑。一、后悔决定1按技术层拆分而不是按业务域拆分我当时怎么想的刚开始拆分时我按照技术层来拆user-service所有用户相关common-service公共逻辑gateway-service网关data-service所有数据库操作出了什么问题很快我发现一个下单操作需要调用user-service查用户信息→data-service写订单数据→common-service发通知链路又长又脆弱。更要命的是data-service成了上帝服务所有业务都往里塞跟单体有什么区别正确做法按业务领域DDD拆分order-service → 负责订单生命周期 payment-service → 负责支付结算 user-service → 负责用户注册/认证 notification-service → 负责消息推送每个服务拥有自己的数据库自己管自己的事。核心原则高内聚、低耦合 一个服务对应一个业务能力Business Capability 服务之间通过API或事件通信不共享数据库二、后悔决定2一开始就上全套Spring Cloud我当时怎么想的“既然要做微服务那Eureka、Config Server、Zuul、Hystrix全上吧”出了什么问题Eureka集群搭了3个节点开发环境根本用不到白白占资源Config Server每次改配置要重启服务不如Nacos好用Zuul 1.x性能堪忧后来换Gateway又改了一遍Hystrix已经停更结果项目上线后发现官方推荐Resilience4j我的建议# 技术选型建议2024版本注册中心:Nacos兼顾注册配置网关:Spring Cloud Gateway熔断:Resilience4j 或 Sentinel链路追踪:SkyWalking 或 Jaeger配置中心:Nacos Config不要一次性上全套按需引入。先把服务跑起来遇到问题再加组件。三、后悔决定3分布式事务用了2PC我当时怎么想的“跨服务事务一致性很重要用Seata的AT模式应该没问题吧。”出了什么问题// 下单流程扣库存 创建订单 扣余额GlobalTransactionalpublicvoidcreateOrder(OrderDTOdto){inventoryService.deduct(dto.getSkuId(),dto.getQuantity());orderService.create(dto);accountService.debit(dto.getUserId(),dto.getAmount());}问题来了性能急剧下降加了全局事务后TPS从1200降到300锁等待超时高并发下频繁出现全局锁冲突Seata Server单点故障挂了一次全部订单卡住正确做法最终一致性大多数业务场景不需要强一致性最终一致性就够了// 方案1本地消息表 MQpublicvoidcreateOrder(OrderDTOdto){// 1. 本地事务创建订单 写消息表orderMapper.insert(order);messageMapper.insert(newMessage(deduct_inventory,dto));// 2. 定时任务扫描消息表发送MQ// 3. 库存服务消费MQ扣减库存// 4. 扣减成功后回调确认}// 方案2Saga模式推荐// 每个步骤有对应的补偿操作// 扣库存失败 → 补偿回滚订单我的总结场景推荐方案强一致性转账Seata AT/TCC最终一致性下单本地消息表 MQ长事务Saga模式简单场景重试 幂等四、后悔决定4服务间通信全用同步HTTP我当时怎么想的“用Feign调一下就行了多简单。”出了什么问题// 订单服务调用链OrderService→UserService→AddressService→LogisticsService某天LogisticsService响应变慢P99从50ms飙到3s导致整条链路全部超时引发雪崩效应OrderService 线程池打满 → 拒绝所有请求 → 前端504 → 用户疯狂重试 → 更多请求涌入 → 彻底崩溃正确做法异步化 事件驱动// 改造前同步调用FeignClient(logistics-service)publicinterfaceLogisticsClient{PostMapping(/api/shipment)ShipmentResultcreateShipment(ShipmentRequestrequest);}// 改造后事件驱动ServicepublicclassOrderService{AutowiredprivateRocketMQTemplatemqTemplate;publicvoidonOrderPaid(Orderorder){// 发事件不等结果mqTemplate.asyncSend(order-paid-topic,newOrderPaidEvent(order.getId(),order.getAddress()));}}// 物流服务订阅事件RocketMQMessageListener(topicorder-paid-topic)publicclassShipmentListenerimplementsRocketMQListenerOrderPaidEvent{OverridepublicvoidonMessage(OrderPaidEventevent){// 异步创建物流单shipmentService.create(event);}}通信方式选择指南场景推荐方式需要实时返回结果同步HTTP/gRPC不关心结果/允许延迟MQ异步广播通知事件驱动高性能内部调用gRPC五、后悔决定5没有做好服务可观测性我当时怎么想的“先把功能做出来监控以后再加。”出了什么问题上线一周后用户反馈下单有时候很慢。我们排查了两天看了Nginx日志请求确实慢看了各服务日志单个服务都不慢最后发现是服务之间的网络调用慢但没有链路追踪根本定位不到是哪一跳的问题正确做法Day 1就要有三板斧# 可观测性三板斧Metrics指标:Prometheus GrafanaLogging日志:ELK / LokiTracing链路追踪:SkyWalking / Jaeger最小化接入方案SkyWalking为例!-- 只需加一个agent零代码侵入 --!-- 启动参数 ---javaagent:/path/to/skywalking-agent.jar -Dskywalking.agent.service_nameorder-service -Dskywalking.collector.backend_servicelocalhost:11800接入后你能看到每个请求经过了哪些服务每一跳耗时多少哪个服务是瓶颈异常发生在哪个节点六、拆分微服务的正确姿势总结经过这些坑我总结了一套微服务拆分的决策框架拆分前的灵魂三问你的团队有几个人3-5人的团队搞微服务就是自找麻烦你的业务复杂度到了吗如果单体还能hold住别急着拆你的基础设施Ready了吗CI/CD、容器化、监控缺一不可拆分节奏建议阶段1单体内模块化Package by Feature 阶段2识别核心域抽出1-2个独立服务 阶段3基础设施补齐注册中心、网关、配置中心 阶段4逐步拆分更多服务 阶段5引入Service Mesh可选我的避坑清单不要为了微服务而微服务先定义好服务边界推荐Event Storming每个服务独立数据库优先选择最终一致性异步优于同步Day 1就接入可观测性CI/CD必须自动化做好服务降级和熔断写在最后微服务不是银弹它解决了一些问题也引入了大量新问题。如果你的团队还小、业务还简单好的单体远胜于烂的微服务。但如果你确定要拆希望我踩的这些坑能帮你少走一些弯路。有问题欢迎评论区交流如果这篇文章对你有帮助别忘了点赞收藏你的支持是我持续输出的动力