微服务改造落地复盘:从单体拆分到生产级架构的完整路径
发布时间:2026/10/23 12:22:39 锦皓数字建站

1. 业务需求先把“为什么拆”想清楚这两年云原生微服务几乎成了后端开发的默认选项但把一套单体系统真正拆成生产级微服务前期最难的反而是一些看似平常的问题业务需求怎么收敛、服务边界怎么切、每项技术栈凭什么这么选。最近我完整复盘了一个零售业务中台项目从梳理痛点、划分领域边界到确定注册中心、网关、消息队列、监控体系整个过程踩了不少坑。这篇内容不谈空泛概念只记录一个可以复用的决策过程适合正在准备微服务改造或者刚拿到一个从零搭建云原生架构任务的团队参考。很多团队一上来就急着画服务拓扑、选中间件却忽略了最先应该做的事搞清楚现在的系统到底为什么撑不住了。这个问题回答不清楚拆出来的微服务大概率只是把单体复制了 N 份运维成本直线升高业务并发却没什么改善。所以这一章先从业务侧讲起。1.1 原来的单体系统到底卡在哪我复盘的项目背景很典型一个零售品牌的线上商城最初是一套 Java 单体应用订单、库存、营销、用户、商品全部打在一个 war 包里多个团队共用同一个代码仓库和同一个数据库。平时日活几千还能撑住但一到促销节点整个 Tomcat 和 MySQL 的 CPU 直接飘红数据库连接被打满紧接着是一连串雪崩用户请求超时、下单失败、库存数据对不上。把问题拆开看痛点其实集中在这几类代码耦合严重一次下单要查用户、查商品、查优惠券、扣库存、生成订单、发消息所有业务逻辑都写在一个大事务里。表面上保证了强一致实际成了“慢查询放大器”一个环节慢整条链路就超时。资源无法独立扩容订单服务流量高但所有模块在同一个进程里只能整体扩容。内存和 CPU 都被非关键模块浪费成本高效果差。发布和协作冲突多个团队同时改一个仓库合并代码经常冲突任何一个模块出现紧急 bug都会阻塞其他团队的发布窗口。数据模型过度集中订单表、库存表、营销表混在一个库表数量庞大索引和锁竞争越来越严重DBA 维护困难。故障隔离能力为零一个接口 OOM整个应用退出所有业务全部不可用。这些问题的根本原因不是技术债而是“单体的边界”和“业务增长的复杂度”不匹配。所以第一步不是选框架而是把业务域彻底梳理清楚。1.2 业务域拆解与限界上下文做微服务拆分时我建议先用领域驱动设计里“限界上下文”的思路而不是按功能模块拍脑袋。具体做法是从业务流程出发找出所有业务事件比如用户下单、支付成功、库存锁定、商品上下架、物流履约、通知触达。每个事件都属于某一个明确的业务域这就是服务划分的候选边界。我们最终拆出来的核心服务如下服务名核心职责核心数据用户服务用户身份、账户、收货地址、会员等级用户表、地址表、积分表商品服务商品基本信息、类目、上下架、价格管理商品表、类目表、SKU表库存服务库存查询、锁库存、扣减库存、库存回滚库存表、库存流水表订单服务订单创建、订单状态流转、订单查询订单表、订单明细表支付服务支付单创建、支付回调、退款、对账支付表、退款表营销服务优惠券、活动、促销规则、用户权益活动表、优惠券表履约服务发货单、物流轨迹、签收确认发货表、物流轨迹表通知服务短信、邮件、站内信、推送通知记录表、模板表拆分原则很简单每个领域的数据由同一个服务独占其他服务想拿数据只能通过接口或事件不允许直接跨库查询。刚开始拆的时候不用追求“一个服务一张表”的粒度更合理的是把强相关数据放进同一个服务比如订单服务和订单明细服务合并成订单服务避免频繁的分布式事务。1.3 非功能性需求和技术约束业务需求不只是“能下单”还要回答一组关键指标不然到后面没法验收。这个项目硬性要求是这样的整体可用性 99.9%大促峰值 TPS 3000订单创建接口 P99 响应时间小于 500 毫秒支付回调数据不能丢失支持按环境独立部署和扩容。这些指标直接决定了架构和选型方向。技术约束同样重要。团队以 Java 为主运维人力有限不可能自研中间件也无法养一个专门的运维团队去维护复杂的服务网格。因此我们倾向于选择云原生领域成熟、社区活跃的开源组件而不是什么都自己搭。把“生产级”三个字翻译成落地要求就是可观测、可容灾、可弹性伸缩、可灰度发布缺一样都不能叫生产级。2. 架构设计把一个单体切成一张网需求清楚之后架构设计就有了边界。微服务架构不是把模块改成独立进程那么简单而是要重新设计接入方式、服务通信、数据一致性、故障隔离和部署形态。下面是我这套架构的核心思路。2.1 整体分层与部署形态整个系统从上到下可以分成四层每层只做自己该做的事接入层外部流量入口使用域名、CDN、负载均衡和应用防火墙最外层挡住恶意请求并终结 TLS 证书。网关层Kubernetes Ingress 负责南北向流量转发Spring Cloud Gateway 作为业务网关负责鉴权、限流、路由和部分协议转换。服务层所有业务微服务按领域边界划分为用户、商品、库存、订单、支付、营销、履约、通知等服务。服务间通过 OpenFeign 同步调用或 RocketMQ 异步解耦。基础能力层注册中心、配置中心、消息队列、分布式缓存、对象存储、监控告警、日志收集、链路追踪这些组件构成了微服务运行的底座。数据层每个服务独享自己的数据库实例或数据库通过权限隔离禁止跨服务直接访问。部署形态上我们使用 Kubernetes 集群按环境划分命名空间比如 dev、staging、prod 完全隔离。每个服务都通过 Helm Chart 发布通过一套流水线构建镜像并自动部署。网关层内部再区分“外部 API 网关”和“内部服务调用”避免业务服务被外部直接暴露。2.2 服务间通信与接口设计服务拆开后通信方式必须提前定好否则会出现“用 Feign 调异步接口”、“用 MQ 做同步请求”这类混乱。我们定的原则很直接请求响应的实时查询、事务内必要的写操作用 OpenFeign 同步调用跨服务最终一致、需要削峰填谷、需要广播通知的操作用 RocketMQ 异步事件。接口设计上统一使用 RESTful 风格路径以服务名开头比如/api/order接口必须带版本号比如/api/v1/order。字段命名统一用下划线时间统一传时间戳或用 ISO 8601 字符串错误码统一返回code/message/trace_id结构。最关键的一点是幂等设计所有写接口都要求客户端传X-Request-ID服务端用 Redis 记录请求唯一键重复提交直接返回第一次的结果。没有这一步微服务下的网络重试会造成大量重复订单。2.3 数据一致性与事务策略微服务最容易被问的问题就是分布式事务。我在实际项目里的结论是不要追求所有业务都强一致否则系统会变得极慢极复杂。需要强一致的场景其实很少更多场景可以接受秒级甚至分钟级最终一致。针对不同场景我们用了三种策略本地事务加事务消息订单服务和库存服务之间先开启本地事务创建订单和扣减预占库存流水同时向 RocketMQ 发送事务消息等消息确认后再异步更新库存可用数量。这样保证“订单已创建库存已预占消息一定发出”这三个动作要么都成功要么可以补偿。可靠消息最终一致支付回调场景支付服务接收回调后修改支付单状态再发送“支付成功”事件订单服务消费事件后更新订单状态并触发积分、通知等下游动作。只要消息不丢下游不断重试最终就是一致的。定时对账兜底任何消息链路都可能出现极端问题所以每天晚上跑一次对账任务拉取订单、支付、库存流水进行比对发现不一致就触发补偿流程。后来在某些高一致性场景比如库存扣减我们用了 Redis 加 Lua 脚本进行预扣减再异步同步到 MySQL。这里必须想清楚一个原则宁可对账复杂一点也不能让主流程因为分布式事务锁而拖垮。2.4 高可用与弹性设计生产级系统的另一个重点是高可用。Kubernetes 里的多副本是基础但副本越多不代表越稳定还要考虑故障隔离和自动伸缩。我们给每个服务配置了 HPA比如订单服务 CPU 超过 70% 时自动扩容副本数范围是 3 到 10缩容也设置了冷却时间避免流量抖动导致频繁扩缩容。资源层面每个容器的request和limit必须写清楚防止某个服务把节点内存打爆。探针也要区分存活和就绪存活探针失败时重启容器就绪探针失败时摘除流量。服务调用侧统一设置超时和重试但必须加熔断器比如 Sentienl 的熔断规则否则重试会变成雪崩助推器。我们还配置了 Pod 反亲和让同一个服务的多个副本尽量分布在不同的节点上避免单节点故障导致所有副本一起挂。3. 技术栈选型每一项都是取舍到了技术选型这一步最容易翻车的就是“什么热选什么”。正确的思路是先明确需求再列出候选最后根据团队能力、运维成本、社区生态拍板。我按实际项目的选型过程逐一讲。3.1 服务框架与生态服务框架基本没有悬念我们选了 Spring Boot 3.2.x 加 Spring Cloud Alibaba。为什么不是 Spring Cloud Kubernetes不是因为后者不好而是它把注册发现、配置管理交给 Kubernetes 原生的 Service、ConfigMap抽象粒度太粗对于 Java 微服务团队反而不好做细粒度控制。为什么不用 Go 或其它语言重写团队现有积累全在 Java业务复杂度高重写风险太大。Spring Boot 3 的启动速度和内存占用比 2.x 有明显提升加上 Spring Cloud Alibaba 对 Nacos、Sentinel、Seata 的一体化集成能少写很多胶水代码。这里建议选型时写清楚版本矩阵Spring Boot 版本、Spring Cloud 版本、Spring Cloud Alibaba 版本必须严格对应直接采用官方推荐的版本组合不要自己随便升级否则会遇到各种兼容性坑。3.2 注册中心、配置中心与网关注册中心选型时对比了 Nacos、Eureka、Consul。Eureka 2.x 基本不再演进首先排除Consul 是 CP 系统强一致但性能上限偏保守而且配置中心的体验不如 NacosNacos 2.x 支持 AP 和 CP 两种模式服务发现用 AP配置中心自带这样我们不用再单独维护注册中心和配置中心两套系统。实际使用中 Nacos 的命名空间和 group 设计在多环境隔离下非常方便配置按服务名-环境.yaml动态下发改动后不用重启服务。网关我们对比了 Spring Cloud Gateway 和 APISIX。APISIX 基于 OpenResty性能高但引入的是 Lua/Go 插件体系团队短时间内难以上手。Spring Cloud Gateway 和微服务生态一致基于 WebFlux 写路由和过滤器很顺手于是选了它。Kubernetes Ingress 继续负责域名和证书Gateway 只做业务路由两者分工明确。如果团队对性能有极端要求再考虑替换 APISIX但对大多数业务系统来说Gateway 的表现已经足够了。3.3 消息、缓存、数据库消息队列选型是项目里讨论最多的一项。RocketMQ、Kafka、RabbitMQ 都能用但侧重点完全不同。RabbitMQ 足够轻量但顺序消息和事务消息支持不够Kafka 吞吐很高但运维复杂度高而且事务消息需要额外引入 read-only 表来补偿麻烦RocketMQ 原生支持事务消息、延迟消息、顺序消息对这个项目几乎全是加分项所以最终选 RocketMQ。如果你所在团队没有运维开源中间件的经验更建议用云厂商托管版把运维风险降到外部。缓存和数据库方面Redis 7 集群承担缓存、分布式锁、库存预扣减MySQL 8.0 作为主力数据库订单表后续按用户 ID 分库分表使用 ShardingSphere商品搜索用 Elasticsearch而不是直接在 MySQL 里LIKE通知记录和审计日志写入 MongoDB这类数据不需要强事务列式扩展体验好。选型时注意一点不要为了一种数据库而造业务场景只有数据访问模式明确后才引入新组件。3.4 可观测性、CI/CD 与安全生产级系统必须把可观测性当成功能来做。监控使用 Prometheus 抓取指标Grafana 做可视化Alertmanager 负责告警日志使用 Loki统一采集到对象存储通过 Grafana 查询链路追踪使用 Micrometer Tracing 加 Zipkin所有服务在网关入口生成 traceId通过 Feign 拦截器向下游传递。这三个数据源最好在同一个 Grafana 里联动排查问题才高效。CI/CD 上我们是 GitLab CI 触发构建采用多阶段 Dockerfile 构建镜像推送私有镜像仓库随后 Helm 部署到 Kubernetes。每个环境独立命名空间配置通过 Nacos 下发不在镜像里写死。安全方面网关统一做 JWT 校验服务间使用内网通信关键敏感数据加密存储。服务间 mTLS 我们评估后没有直接上服务网格因为维护成本高初期通过内网和网络策略控制也能兜住安全底线。4. 核心服务落地实战以订单服务为例讲完设计和选型我以订单服务为例把从代码初始化到 Kubernetes 部署的完整过程跑一遍。这个流程能覆盖大部分微服务的落地套路照着做可以少走很多弯路。4.1 初始化工程与目录结构订单服务使用 Spring Initializr 创建依赖包括spring-boot-starter-web、spring-cloud-starter-openfeign、spring-cloud-starter-alibaba-nacos-discovery、spring-cloud-starter-alibaba-nacos-config、spring-boot-starter-actuator、MySQL 驱动、MyBatis-Plus。要注意的是Spring Cloud 2023 之后不少配置项需要引入spring-cloud-starter-bootstrap否则外置配置读取会失效。典型的目录结构如下order-service ├── src/main/java │ └── com/company/order │ ├── controller // 接收HTTP请求 │ ├── service // 业务逻辑 │ ├── dao // 数据访问层 │ ├── domain // 领域模型 │ ├── mq // RocketMQ生产者消费者 │ ├── feign // 远程调用 │ └── config // 配置类 ├── src/main/resources │ ├── bootstrap.yml // Nacos优先加载的配置 │ └── application.yml ├── Dockerfile └── helm/ // 部署模板bootstrap.yml是一个非常容易踩坑的地方。它负责提前连接 Nacos再拉取order-service.yaml配置文件。很多问题比如“启动报错找不到配置”、“配置修改不生效”八成都是bootstrap.yml里的 namespace、group、dataId 写错了。4.2 核心业务链路与代码实现订单服务最核心的下单逻辑我们把它拆成三步幂等校验、创建订单、发布事件。核心代码长这样PostMapping(/api/v1/order) public ResultLong createOrder(RequestBody CreateOrderCommand cmd, RequestHeader(X-Request-ID) String requestId) { // 幂等校验Redis中存在requestId直接返回已有结果 if (idempotentService.check(requestId)) { return Result.success(idempotentService.getResult(requestId)); } // 同一个本地事务里写订单表 写订单事件表 Long orderId txTemplate.execute(() - { OrderEntity order orderService.createOrder(cmd); orderEventService.savePendingEvent(order.getId(), OrderCreatedEvent.TYPE); idempotentService.set(requestId, order.getId()); return order.getId(); }); // 事务提交后发送RocketMQ事务消息消费端是库存服务、履约服务 rocketMqTemplate.sendMessageInTransaction(order_topic, new OrderCreatedEvent(orderId, cmd.getUserId(), cmd.getItems()), null); return Result.success(orderId); }这里最容易被忽略的是“事务消息”的用法先本地事务写业务数据和事件记录再发送事务消息RocketMQ 会回调检查本地事务状态来决定是否最终提交。这样才能保证“用户看到的订单一定创建成功下游一定会收到消息”。4.3 配置中心、限流与熔断订单服务在 Nacos 里的配置大概是这样的server: port: 8080 order: timeout: 30 auto-close-minutes: 30 spring: redis: cluster: nodes: redis-1:7001,redis-2:7002,redis-3:7003页面或控制台修改配置后服务会自动通过 Nacos 长轮询感知并刷新。但注意配置要生效类必须加上RefreshScope不加就是白搭。很多同学说配置中心没用最后发现是忘了加注解这个问题我们在 5.3 节会重点说。限流我们通过 Sentinel 网关和控制台配合。网关层配置/api/v1/order/**的限流规则针对用户维度做 QPS 限制超过阈值直接返回友好错误。核心服务内部对库存服务的 Feign 调用加了熔断规则当请求慢调用比例超过 30% 时触发熔断并降级返回预生成提示。4.4 容器化与 Kubernetes 部署Dockerfile 采用多阶段构建避免把 Maven 依赖打进运行镜像FROM maven:3.9-eclipse-temurin-17 AS builder COPY . /work WORKDIR /work RUN mvn -q package -DskipTests FROM eclipse-temurin:17-jre RUN useradd --create-home appuser USER appuser COPY --frombuilder /work/target/order-service.jar /app/order-service.jar ENTRYPOINT [java, -jar, /app/order-service.jar]Kubernetes 的 Deployment 配置里我最关心的是三部分探针、资源、启动参数。apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: prod spec: replicas: 3 strategy: rollingUpdate: maxUnavailable: 0 maxSurge: 1 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.internal/order-service:1.4.2 ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 20 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 20 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2 memory: 1Gi env: - name: JAVA_OPTS value: -XX:MaxRAMPercentage80 -Djava.security.egdfile:/dev/./urandomMaxRAMPercentage一定注意当容器有内存 limit 时JVM 默认是按照宿主机内存来计算堆大小不设置这个参数很容易让容器因内存超限被杀这是 K8s 部署 Java 应用最常见的坑。4.5 网关路由与全链路联调网关服务里加一段路由配置spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/v1/order/** filters: - StripPrefix1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200lb://order-service表示从 Nacos 按服务名负载均衡不需要写死 IP。StripPrefix1会把/api去掉这样网关把/api/v1/order转发到订单服务后订单服务收到的是/v1/order路由不冲突。联调验证的命令我习惯这样跑curl -X POST https://api.internal.example.com/api/v1/order \ -H Content-Type: application/json \ -H X-Request-ID: 20240615-00001 \ -d {user_id:1001,items:[{sku_id:2001,quantity:1}]} \ -v返回结果里一定要能看到trace_id。之后在 Zipkin 页面上按 traceId 搜索可以看到网关、订单服务、库存服务的完整调用链。如果链路断在某个节点优先检查 Feign 拦截器是否把 traceId 往下传了。5. 生产落地中的典型问题与排查清单再好的设计落到生产环境都会遇到新问题。这一章我把项目里真实踩过的几个高频问题整理出来按“现象、排查思路、解决方法”的顺序写方便大家直接对照。5.1 服务反复重启与探针失败现象是 Pod 起来了但马上进入CrashLoopBackOff或者Readiness一直不通过。大部分原因是依赖没有就绪比如 MySQL 还没连上、Nacos 连接超时或者健康检查配置错误。不要一上来就盲目调探针先用kubectl describe pod和kubectl logs看事件和应用日志。如果日志显示 MySQL 连接慢就调大initialDelaySeconds同时把启动命令的-Djava.security.egd加上避免随机数阻塞拖慢启动。5.2 Nacos注册正常但Feign调用失败很多同学在 Nacos 控制台能看到服务列表但服务之间调用报“No provider”。这个问题绝大多数是命名空间或 group 不一致。比如订单服务在 dev 命名空间库存服务在 prod 命名空间Feign 当然找不到。排查时先在 Nacos 控制台逐个核对服务所在命名空间和 group再看FeignClient(name inventory-service)里的名字是否和application.yml的spring.application.name完全一致大小写不要忽略因为默认是精确匹配。5.3 配置中心动态刷新失效配置中心改完值服务没变化这是非常打击信心的问题。我把它拆成三个检查点一是配置是否真的加载到了 Nacos服务启动日志里有 Config 拉取记录二是bootstrap.yml不受application.yml影响如果配置写在application.yml里Nacos 根本管不到三是目标 Bean 有没有RefreshScope。要注意的是RefreshScope应该加在配置属性类的 Bean 上而不是加在使用这个配置的 Service 类上否则刷新后实例不会重新构造配置还是旧的。5.4 分布式事务与消息重复消费事务消息能解决“本地事务和发消息不一致”但不能解决消费端重复消费。RocketMQ 的 at-least-once 语义决定了消费者必须自己做幂等。我们在订单服务和库存服务里都加了消费幂等表用唯一键event_id service_name作为主键重复消息来了直接跳过。扣减库存时还要注意并发我们在 Redis 里用 Lua 脚本预扣减脚本里校验库存是否充足不足则拒绝避免超卖。5.5 链路追踪与日志串联不上网关层的 traceId 能生成但调用链到某个服务就断了通常是因为 Feign 内部没有传递 traceId。Spring Boot 3 使用 Micrometer Tracing它会自动处理常见的 Feign、RestTemplate但要确认所有依赖都是同一个版本体系比如micrometer-tracing-bridge-zipkin。如果还不行就手动加一个 Feign RequestInterceptor从上层 context 取出 traceId放进 header。日志方面建议所有服务统一用 JSON 格式输出Grafana 或 Kibana 中直接按 traceId 字段过滤这样排查效率会提高很多。5.6 快速排查速查表症状可能原因排查方法解决方案服务反复重启Readiness 失败依赖未就绪或探针路径错误kubectl describe pod、查看应用日志调整探针延迟、检查依赖连接Feign 调用 No providerNacos 命名空间/group 不一致Nacos 控制台核对服务注册信息统一 namespace、group、服务名Nacos 配置更新不生效缺少 RefreshScope 或配置位置不对检查启动日志、加载源修正配置加载方式和注解消息重复执行同一业务消费端未做幂等查消费日志、Redis 唯一键增加幂等表和去重逻辑链路追踪 traceId 断开Feign 未传 traceId、依赖版本不一致查各服务日志 trace_id统一版本、手动传递 trace_idJVM 频繁 OOM 或容器被杀未设置 MaxRAMPercentage看 K8s events、监控内存使用 -XX:MaxRAMPercentage805.7 经验教训如果再让我做一遍复盘完这个项目我最想提醒三点。第一技术栈选型一定要把版本矩阵固定下来最好一开始就构建一个最小的“模拟链路”一个服务注册到 Nacos通过网关路由到另一个服务链路追踪贯通再开始铺业务。这个骨架跑通后再加业务代码可以避免后期排查一堆环境问题。第二可观测性必须和非功能需求同步设计而不是在项目快上线时补。我最后悔的就是监控指标定义得太晚导致前面一段时间出了问题只能靠日志慢慢翻。先确定核心服务的 RED 指标再写业务功能后面的排查成本会低一个量级。第三不要为了微服务而微服务。我们这个项目之所以拆得顺利是因为业务需求确实复杂、并发确实增长团队协作确实有痛点。如果只是“觉得微服务高级”就去拆最后只会得到一套分布式单体的复杂版本。任何时候架构决策都应该先回答“为什么”再回答“怎么做”。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。