简介:一份面向后端开发与架构设计人员的微服务化改造实践文档,针对单体架构扩展性差、部署困难、故障隔离不足等痛点,系统讲解从背景分析、技术选型、架构设计规划到落地实施的完整路径。文档基于经典单体层次模型与业务输入输出场景,结合Spring Cloud、Docker、Kubernetes、Istio等主流技术栈,引入DDD领域驱动设计划分业务边界,并遵循12因素应用原则构建整体架构。内容细化到Eureka服务注册发现、API网关、Hystrix熔断降级、消息队列异步通信、CI/CD持续交付与监控回滚策略等关键环节,帮助读者掌握服务拆分边界、治理模型和稳定性保障方法。资源为单个docx文件,压缩包约690KB,章节结构清晰,含背景、微服务化改造、架构设计规划、落地实施应用及篇后语,适合正在规划或推进微服务化转型的技术团队参考学习。目前已有136人浏览学习,可用作系统梳理微服务改造方法论与工程实践的案头资料。
1. 微服务化改造的第一次会议:先搞清楚什么时候必须拆、拆完图什么
后端业务系统走到一个临界点,会有一堆信号同时冒出来:一个两三万行的 Controller 文件不敢动、每次发版都像抽签、一个接口慢导致整条业务链跟着慢、新人入职两周还在理不清模块之间的调用关系。这时候技术负责人多半会提一句"把系统微服务化改造一下",但你首先要清楚,微服务化不是把代码拆碎就完事,它是引入一套新的运行时复杂度——网络调用、分布式事务、链路追踪、配置管理全都要重新设计。本文要给的是改造前后落地的完整路径,包括领域拆分的边界怎么划、基础设施怎么搭、数据一致性怎么保,以及那些不拆不知道、拆了才踩到的坑。适合准备改造但还没动工的中小型后端团队,以及已经拆了一半正在跟基础设施搏斗的读者。
2. 从单体到微服务的拆分边界:领域划分、依赖梳理与数据库解耦
2.1 用 DDD 的限界上下文划服务边界,而不是按代码分层拆
常见的错误做法是按时钟拆:把所有 Controller 拆到一个服务、所有 Service 拆到另一个服务、所有 DAO 拆到第三个服务。这样拆完只是把单体代码从一个大 jar 变成三个小 jar,调用关系还是乱的,网络开销反而把性能拖垮。
我一般用 DDD 里的限界上下文来划。方法是拉一个包含产品、研发、运营的会议,把核心业务链路走一遍,找那些"不同角色对同一个对象的定义不一样"的边界。比如用户这个对象,在订单域关注的是收货人和联系方式,在风控域关注的是行为标签和历史订单,在营销域关注的是优惠券和等级——这就是三个限界上下文,分别拆服务才是合理的。
// 订单服务里的用户信息:只保留下单需要的最小字段 public class OrderUserInfo { private Long userId; private String receiverName; private String receiverPhone; private String province; private String city; private String detailAddress; } // 风控服务里的用户信息:关注行为特征,和订单服务是两个模型 public class RiskUserProfile { private Long userId; private List<Integer> riskTagIds; private Integer orderCountIn7Days; private BigDecimal avgOrderAmount; }这两个类描述的是同一批用户,但字段完全不同,它们就应该落在不同的服务里。不要试图做一个"用户中心"把全公司的用户字段都塞进去,那会让用户中心变成一个比原单体还大的服务。订单服务和风控服务各自通过接口去用户中心取自己需要的数据,用户中心只维护账号、密码、注册状态这类登录态相关字段。
一个可参考的起手式:先画一张服务候选清单,每一行是一个候选服务,附上它的核心职责、依赖的外部服务、被谁调用。如果发现两个候选服务互相调用的频次特别高,或者它们共用一个事务边界,就重新考虑是不是该合并成一个服务。我在实际拆分中,第一轮的候选清单常常有 10 个以上服务,经过合并后收敛到 6 到 8 个,这样改造的体量是团队能承受的。
2.2 把共享依赖和硬编码调用整理成接口契约
单体改造最揪心的是公共模块。很多人会建一个common-service包给所有微服务引用,这看起来省事,但改一个字段就要所有服务一起重新发版,微服务的好处全被它抵消了。常见做法是把公共模块拆成两类:一类是纯工具,比如 JSON 工具、日期工具、加密工具,这种可以打成公共依赖包;另一类是业务模型,比如订单 DTO、用户 DTO,这种必须禁止共享,由各服务自己定义。
接口契约的难点在于改字段的兼容性。单体系统改一个字段只影响一个方法,微服务改一个字段影响的是消费方服务。我们后来定的规矩是:服务之间传输的 DTO 只允许加字段,不允许删字段和改字段类型。加字段时还要注意序列化兼容,比如用 Jackson 的FAIL_ON_UNKNOWN_PROPERTIES必须设为 false,否则老服务收到新服务多带的字段会直接抛异常。
spring: jackson: deserialization: fail-on-unknown-properties: false这段配置的意思是:反序列化的时候,JSON 里出现目标类没有的字段,忽略掉而不是报错。微服务之间的接口演进过程中,经常出现 A 服务先上线、B 服务还没更新的情况,这个配置是保证接口平滑过渡的第一道保险。我刚改造完的那段时间,这个配置救了不止一次线上事故——有次订单服务新增了一个traceId字段,支付服务还没升级,靠它才没报错。
提示:配置在新增字段阶段有效,但如果服务端删了字段,客户端还在用,那是另一种错误——返回字段缺失,这个只能靠发布节奏控制,配置救不了。
2.3 数据库拆分的三个前提:连接、报表和事务边界
数据库拆分是微服务化里风险最高的一步,比拆代码难得多。业务系统改造到这一步,一般先从连接下手:单体的数据库连接池把所有表的访问都绕在一条链路上,拆服务后每个服务有自己独立的库,连接数需求会翻几倍。先做数据库账号隔离,再决定要不要物理拆库。
我建议的推进顺序是:先在逻辑上接管——按服务的职责把表分成清单,验证一下哪些表被多个服务访问。跨服务访问的表如果只是只读,问题不大,可以用主数据服务统一提供读取接口;如果是可写的,就要评估事务边界了。比如订单表和支付表如果拆到两个库,原来一个本地事务能搞定的"下单加扣款"就变成跨库事务,这直接决定了后面要不要引入分布式事务框架。
报表系统是另一个容易卡住的点。单体时代报表直接查业务库,拆库之后想跨服务聚合数据就麻烦了。方案一般是建一个独立的报表库,通过消息队列或者定期任务把各服务的数据同步过去。我在实际项目里更狠一点,直接给报表系统开一套异步的数据订阅通道:业务服务把变更事件发到 RocketMQ,报表服务消费事件做宽表存储。这样报表查询永远不碰业务库,业务库的压力也降下来了。
3. 基础设施先行:用 Nacos 落地注册发现与配置中心,再谈网关路由
3.1 搭建 Nacos 与 Spring Boot 项目接入:最小启动配置
微服务化的第一步不是写业务代码,而是把注册中心和配置中心搭起来。选 Nacos 的大致理由是它同时提供服务注册与发现、配置管理两大功能,开箱即用,社区活跃度高,支持 AP 和 CP 模式切换。如果你的团队对 Kubernetes 很熟,也可以考虑用 Kubernetes 的 Service 做服务发现,但对大多数业务系统来说,Nacos 更直观,排查问题也更容易。
服务接入 Nacos 的最小配置如下:
spring: application: name: order-service cloud: nacos: discovery: server-addr: 192.168.10.10:8848 namespace: prod group: DEFAULT_GROUP config: server-addr: 192.168.10.10:8848 namespace: prod group: DEFAULT_GROUP file-extension: yaml shared-configs: ->@SpringBootApplication @EnableDiscoveryClient public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }加了@EnableDiscoveryClient之后,服务启动时会自动向 Nacos 注册自身 IP 和端口,同时定时发送心跳。默认情况下服务每 5 秒发一次心跳,Nacos 在 15 秒内没收到心跳会标记该实例不健康,30 秒后将其剔除。如果客户端调用时发现服务列表里有不健康实例,会基于负载均衡策略优先过滤掉。
3.2 配置中心落地:全局参数与环境隔离的写法
配置中心最容易被低估,很多团队拆完服务还在用每个服务本地的 application.yaml 管理配置,结果改一个 Redis 密码要登录五台服务器。配置中心的意义不只是集中管理,更重要的是动态刷新——不需要重启服务就能改配置。
Nacos 的配置结构有两种组织方式,我实际用下来建议按维度拆:一个>@Component @ConfigurationProperties(prefix = "order.timeout") @RefreshScope public class OrderTimeoutConfig { private int paymentWaitSeconds; private int cancelWaitSeconds; // getter/setter }
这样配置中心改了order.timeout.payment-wait-seconds,OrderTimeoutConfig 会被重新创建,业务代码里注入这个 Bean 的地方自动拿到新值。比起一堆@Value散落在各种类里,这种方式集中、可读、好排查。
注意:数据库连接池这类底层资源即使加了 @RefreshScope 也不建议动态刷新,改了密码就要滚动重启服务,别指望运行期热更。这是很多团队在配置中心上线后踩的第一个翻车点。
3.3 用 Spring Cloud Gateway 做统一入口,顺手解决了跨域问题
服务拆完之后,客户端调接口不再是原来那个统一域名了。直接让客户端分别调订单服务、支付服务、用户中心,既有跨域问题,又暴露了服务内部 IP,而且要做鉴权、限流的时候无从下手。常规做法是引入 API 网关。
我选择 Spring Cloud Gateway,基于 WebFlux,性能和生态都比老一代的 Zuul 1.x 好。最小的路由配置:
spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1 - id: payment-route uri: lb://payment-service predicates: - Path=/api/payment/** filters: - StripPrefix=1lb://order-service的意思是网关从注册中心按服务名找到实例,然后做负载均衡,而不是硬编码 IP 地址。这个写法比配 IP 好维护得多——服务实例扩容、缩容,网关不需要改任何配置。StripPrefix=1表示把路径前缀去掉一层,客户端请求/api/order/create,转发到后端服务时就变成/create,这个设计可以让前端路径和后端服务路径解耦。
网关的跨域配置大部分团队都有血泪经验。单体时代是 Controller 加@CrossOrigin或者写个拦截器,微服务时代再在每个服务里配跨域就乱套了。网关统一处理的方式是:
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowed-origin-patterns: "http://localhost:*" allowed-methods: - GET - POST - PUT - DELETE allowed-headers: "*" allow-credentials: true这里的allowed-origin-patterns用了通配端口而不是allowed-origins: "*",原因是allow-credentials: true时浏览器不允许配通配域名。如果你开发时前端跑在http://localhost:8080调网关http://localhost:9999,这种配置方式是必须的。生产环境把允许的来源域名收敛到正式前端地址,比放开所有来源要安全得多。
4. 跨服务调用与数据一致性:Feign、分布式事务与幂等设计
4.1 OpenFeign 的声明式调用与超时、重试参数配置
服务拆完之后,服务之间互相调用就成了家常便饭。常见选择是 OpenFeign,它把 HTTP 调用声明成了接口方法,代码像调本地方法一样。但"像本地方法"只是幻觉——它毕竟走网络,超时、重试、熔断都要单独设计。
@FeignClient(name = "payment-service", fallbackFactory = PaymentClientFallbackFactory.class) public interface PaymentClient { @PostMapping("/internal/payment/create") PaymentCreateResponse createPayment(@RequestBody PaymentCreateRequest request); }@FeignClient注解里的name对应 Nacos 注册中心里的服务名,调用时会自动从注册中心拉取实例列表并负载均衡。fallbackFactory是容错处理:当调用异常时进入降级逻辑。我用 fallbackFactory 而不是 fallback,是因为 fallbackFactory 可以拿到具体的异常原因,方便记录日志和判定是不是配置错误。
超时参数是最容易调错的。Feign 的默认连接超时是 10 秒,读超时是 60 秒,这在单体时代不算问题,拆成微服务后如果还是这个配置,一个慢接口会占住调用方线程很久,积压之后整个服务线程池被打满。经验值是连接超时 2 到 3 秒,读超时按业务接口的最慢响应时间乘 1.5 来设。如果你的支付回调接口最慢要 8 秒,就设 12 秒,别拍脑袋设个 3 秒然后线上天天报超时。
feign: client: config: default: connectTimeout: 2000 readTimeout: 10000 loggerLevel: BASIC还有重试的坑。feign默认不重试,但很多老项目重试配置会叠加。举一个经典翻车场景:订单服务调用支付服务创建支付单时超时,Feign 自动重试了 3 次,支付服务实际已经创建成功了两笔支付单,订单服务最后拿到失败结果,用户看到的是支付失败但钱被扣了。这个场景的正确做法是:写操作服务除了要设置合理的超时时间,必须做幂等控制,调用方对重试要慎之又慎。我的习惯是写接口不开自动重试,读接口可以设置最多 1 到 2 次重试。
4.2 分布式事务的取舍:从 XA 到 TCC 到最终一致
很多从单体拆过来的团队,第一反应是找分布式事务框架解决跨服务数据一致性问题。先明确一个结论:分布式事务框架不是银弹,它会带来性能损失和代码侵入,能通过业务设计规避的场景就不要引入。
先说 XA 协议。它通过两阶段提交保证强一致,但性能损耗大,协调者成了所有事务的瓶颈,而且很多业务系统根本不需要强一致。我在实际改造中只有在钱相关的强一致场景才考虑 TCC 方案。
TCC(Try-Confirm-Cancel)是一种补偿型分布式事务方案,它把业务逻辑拆成三个阶段写模板代码,侵入性很强。比如下单加扣库存:
@TccBusinessAction public OrderCreateResult tryCreateOrder(OrderCreateCommand cmd) { // Try阶段:创建订单,状态为PENDING // 扣减库存,冻结不真正扣掉 // 扣减用户余额,冻结额度 } @TccConfirm public void confirmCreateOrder(OrderCreateCommand cmd) { // Confirm阶段:订单状态改为CREATED // 扣减库存真正执行 // 冻结余额改为扣减 } @TccCancel public void cancelCreateOrder(OrderCreateCommand cmd) { // Cancel阶段:订单状态改为CANCELLED // 库存冻结释放 // 余额冻结释放 }TCC 的问题是网络调用超时后的状态判断极难。Executing 阶段卡住时,协调者不知道 Confirm 或 Cancel 到底执行了没有,如果两个都执行或者都不执行,数据就乱了。所以 TCC 需要配套一个事务状态表,每个参与者的状态写入数据库,通过定时任务扫描并补偿。
我实际采用最多的方案是"本地消息表 + 消息队列"的最终一致性。比如新增订单后需要给用户发送积分,订单服务在本地事务里写入一条消息记录,同时更新订单状态和消息状态为待发送,然后投递到 RocketMQ,积分服务消费消息加积分。如果投递失败,定时任务扫描消息表重投。
-- 本地消息表 CREATE TABLE order_local_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, message_body TEXT NOT NULL, status TINYINT NOT NULL COMMENT '0:待发送 1:已发送 2:已消费', retry_count INT DEFAULT 0, next_retry_time DATETIME, create_time DATETIME );这个方案看起来比 TCC 轻,但它有一个前提:业务上允许最终一致性,且消费方要做幂等。积分加重复、通知发重复,这类场景宽容度高,很适合。而支付扣款这种不允许延迟的路径,才考虑更重的协调方案。
4.3 接口幂等的三种常见实现
微服务环境下接口幂等不是可选优化,是必须做的事。上文提到重试会导致重复扣款,幂等设计是防住这类事故的最后一层。
第一种是数据库唯一索引。以支付回调为例,同一个支付单号只能对应一条回调结果记录:
CREATE TABLE payment_callback ( payment_no VARCHAR(64) PRIMARY KEY, status VARCHAR(16) NOT NULL, raw_data TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );用payment_no做主键,重复的回调插入会直接报唯一键冲突,被 catch 住后当作"已处理"返回成功即可。这是最简单也最可靠的幂等方案,能用它解决的场景不要用更复杂的方案。
第二种是状态机校验。订单状态从待支付改成已支付,只允许一次流转:
@UpdateSql("UPDATE orders SET status = #{targetStatus} WHERE id = #{orderId} AND status = #{sourceStatus}") boolean compareAndSetStatus(Long orderId, int sourceStatus, int targetStatus);利用 SQL 的WHERE status = sourceStatus条件来实现 CAS(Compare And Set),更新行数为 0 就说明有人抢先改了状态,当前请求不需要再处理。这种方式适合状态流明确的场景,比如下单、支付、发货、完成、取消。
第三种是 Token 机制,适合前端提交型操作。前端先调用服务端获取一个唯一 token,提交时带上,服务端处理完就把 token 作废:
public String generateIdempotentToken() { String token = UUID.randomUUID().toString().replace("-", ""); stringRedisTemplate.opsForValue().set("idem:" + token, "1", 30, TimeUnit.MINUTES); return token; } public boolean tryConsumeToken(String token) { Boolean deleted = stringRedisTemplate.delete("idem:" + token); return Boolean.TRUE.equals(deleted); }这里的逻辑是delete返回 true 说明 token 在缓存里存在且被删除了,这个请求可以继续执行;返回 false 说明 token 不存在(已被消费或过期),需要拒绝重复请求。注意不能用get再set,要直接用delete原子操作,否则并发场景两个请求同时进来都会认为自己拿到了 token。它们的区别在于tryConsumeToken的delete只能有一个调用方删成功,这是原子性的体现。
5. 微服务化改造避坑指南:5个最容易翻车的细节
5.1 本地调试变成"配置地狱"
现象:改造前启动一个应用,改一行代码,重启一下,完事。改造后要本地同时启动注册中心、配置中心、网关和四五个微服务,光配置文件就有七八个要改,新同事入职第一周基本在折腾环境。
原因:微服务的进程多、配置多,本地开发环境的配置管理没有跟上基础设施的节奏,每个人本地都有一套手工维护的配置。
解决:用 Nacos 把配置按环境隔离做好之后,本地启动只需要在一个bootstrap.yaml里指定 Nacos 地址和环境,剩下的全从配置中心拉。再配合 Docker Compose 把基础设施一键拉起,新同事克隆代码后跑一条命令就能起全套环境。我踩过的教训是坚持所有配置都进配置中心,本地配置只保留 Nacos 地址和本机 IP,这样"在我本地是好的"就不再成为一个玄学话题。
5.2 定时任务被多个实例重复执行
现象:改造前一个服务只有一个实例,定时任务没问题。拆微服务后为了高可用,每个服务至少部署两个副本,结果定时任务每个副本都跑一遍,数据重复处理。
原因:微服务架构下默认就是多实例,定时任务没有抢锁机制就是会重复执行。
解决:引入基于数据库的行锁或者 Redis 的分布式锁。我常用 ShedLock 这个框架,配合数据库锁表实现:
@Scheduled(cron = "0 0 2 * * ?") @SchedulerLock(name = "dailyStatJob", lockAtMostFor = "PT15M", lockAtLeastFor = "PT5M") public void dailyStatJob() { // 每日统计逻辑 }lockAtMostFor是锁最多持有 15 分钟,防止任务崩溃导致锁永久不释放;lockAtLeastFor是最少锁定 5 分钟,避免任务执行太快导致锁提前释放、后一个调度周期趁虚而入。这两个参数要按任务实际执行时间的上下限来设,别照抄,否则要么锁劫持太久,要么起不到防重作用。
5.3 数据库连接池数量被"服务数 × 实例数"放大了
现象:单体时代一条系统连接池配 50 个连接,跑得好好的。拆成 8 个服务、每个服务部署 2 个副本之后,DBA 发现数据库连接数飙到 800 多,数据库快撑不住了。
原因:每个服务的连接池是独立的,总量等于所有服务和实例的和。改造成微服务后如果连接池参数照抄单体,数量级膨胀是必然的。
解决:每个服务的连接池要精打细算。我的习惯是先把连接池上限从 50 压到 20,然后配合监控看连接池的使用率,取随业务峰值再微调。还要注意一个点:事务型连接和非事务型连接要分开考虑。事务范围越短,连接池可以越小;如果一个服务有长事务,那连接池小了会导致大量请求在等待获取连接。
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000connection-timeout: 3000的意思是获取连接的等待时间最多 3 秒,超时就报错,避免在连接池耗尽时请求无限排队把 Tomcat 线程池也拖垮。这里的作用是让失败快速暴露,而不是让系统卡着不动,配合重试和降级才有意义。
5.4 日志分散后,报错不知道在哪台机器
现象:某个请求突然报错,你只知道接口名,但服务有 3 个实例在跑,不能确认是哪个实例出的问题。上一台机器扒日志,没有;再到下一台找,找到了。一天来几次,心态就崩了。
原因:微服务化后日志跟着进程走,每台机器的日志是孤岛,没有一个全局视角。单体时代那套"单机 tail -f"定位问题的习惯彻底失效。
解决:日志聚合是基础设施,不是可选项。最轻量的做法是服务直接通过 Logback 的 socket appender 把日志推到 Logstash 或者直接推到 Elasticsearch,然后接 Kibana 查询。如果不想维护这套 ELK 太重,至少要在日志里打上traceId并且把日志往云厂商的日志服务里传,按 traceId 一搜就能看到整条链路的日志。
关键动作有两个:一是在网关生成 traceId 并放进请求头,二是所有服务从请求头取 traceId 放到 MDC(Mapped Diagnostic Context)里,日志模板里打印出来。代码大致这样:
@Component public class TraceIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { String traceId = request.getHeader("X-Trace-Id"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString().replace("-", ""); } MDC.put("traceId", traceId); chain.doFilter(request, response); MDC.remove("traceId"); } }5.5 全链路灰度与回滚的版本混乱
现象:改造完成后的第一次大版本上线,订单服务新版本有问题,紧急回滚到旧版本。结果发现旧版本不兼容新版本支付服务的数据结构,都滚完了整个链路还报错。
原因:微服务化之后,服务的版本管理不能只靠"上一个版本能跑",服务之间有接口契约和数据格式的强关联。单独回滚一个服务,等同于让它用旧契约和"新契约"服务对接,兼容性问题立刻暴露。
解决:上线前必须做好版本兼容性评估,明确哪些服务可以独立发布、哪些服务必须成组发布。我的习惯是维护一张发布依赖表,列明每个服务的接口版本、依赖方、兼容策略。回滚的时候按依赖顺序成组回滚。另外在网关层做灰度发布,先切 5% 流量到新版本,跑一段观察再逐步放大,有问题就切回,这个机制比"双保险"式的全量上线要靠谱得多。
6. 改造后的验证与进阶:链路追踪、灰度发布与把回归成本降下来
6.1 用 SkyWalking 建立 trace 链路,把"查日志猜原因"变成"看图定位"
改造完之后,服务调用次数变多,一个请求跨三个服务,按以前的方式定位问题需要一台台查日志拼时间线。引入 SkyWalking 之后,每个服务通过 agent 方式接入——不改业务代码,发一个 Java agent 参数启动即可。它自动采集调用链数据、性能数据和依赖关系,在 UI 上能直接看到一次请求的完整调用拓扑,哪个环节慢、哪个服务报错,一目了然。
启动姿势示例:
java -javaagent:/opt/skywalking-agent/skywalking-agent.jar \ -Dskywalking.agent.service_name=order-service \ -jar order-service.jaragent.service_name一定要和注册中心的服务名保持一致,不然界面上对不上哪个服务是哪个。另外一个细节是 agent 采集到的数据包括 SQL 和 HTTP 请求参数,生产环境要按等级脱敏,特别是密码和支付敏感字段。SkyWalking 的 trace 数据默认保留几天,如果发现历史链路查不到,就要加长存储周期或导到外部存储,这个按实际排障频率调整。
6.2 灰度发布的最小实现:权重路由与版本标记
真正尝到微服务架构甜头的团队,都会把灰度发布用起来。最小的实现是在 Nacos 里给每个服务配置一个version元数据,网关根据请求头里的灰度标记把流量打到指定版本。
spring: cloud: nacos: discovery: metadata: version: v2网关侧读取请求头X-Gray-Version: v2,命中灰度条件的请求路由到打了 v2 标记的服务实例。这套方案的代价是最小但能应付大多数场景:先让测试同学带请求头访问,再放内部用户,最后逐步放开。后面有需要再升级到按用户 ID 取模的权重灰度,思路一致,只是路由规则换成 SpEL 表达式。
6.3 回归验证清单与验收指标
改造上线不能靠"感觉没问题",要有一套验收清单。我后来形成习惯的回归清单包括:一条核心业务链路的全流程通过——从登录、下单、支付到发货全跑通;一个报表查询比对——改前和改后的同一时段数据是否一致;一批异常场景——支付超时、余额不足、库存不足,确认返回的错误码和提示没有变。性能指标上,单体到微服务一般会有一次成本上升,但核心接口的 P95 响应时间不能比改造前多出一倍,否则说明拆分粒度或网络调用设计有问题。
我最想留给你的一个教训是:灰度发布和可观测能力,一定要在拆分第一批服务时就同步搭好,不要等拆完了再补。补的话会有一段漫长的"黑匣子"期,报错了不知道是哪个服务、哪个实例、哪次调用的问题,那时候你会有强烈的后悔药需求。把这些白纸黑字写进方案、排进里程碑,后面才安稳。希望帮到你。
本文还有配套的精品资源,点击获取