2025年再聊微服务,我发现很多团队的架构选型已经从“跟风”变成了“务实”。作为一个一直在Java生态里做后端架构的开发者,我今年把主力技术栈定格在了JDK 25 + Spring Cloud Alibaba 2025.x + Docker这套组合上。先说结论:这套方案用来搭可扩展的微服务系统,最省心的地方在于——每个组件都踩着过去那些踩过的坑往前走了一大步,虚拟线程、容器化编排、注册配置中心、网关、限流熔断全链路打通之后,系统交付和扩容的压力比前几年小太多了。
这篇东西不是教你跑通Hello World,那太浪费彼此时间。我分享的是从环境准备到服务拆分,再到Spring Cloud Alibaba核心组件逐个落地、Docker镜像构建与Compose编排、压测扩容调优的完整路径,其中会穿插我自己实际部署中遇到的各种坑和取舍判断。无论你是打算从零搭微服务系统的新团队,还是想给老项目做架构升级的个人开发者,应该都能从这里找到一套可以直接动手的答案。
1. 为什么2025年我会选JDK 25 + Spring Cloud Alibaba + Docker这套组合
1.1 生态匹配:Spring Cloud Alibaba 2025.x到底对应哪个版本号
微服务体系里最头疼的事情之一就是版本匹配。Spring Boot、Spring Cloud、Spring Cloud Alibaba三者之间不是“都用最新版”就行,而是要严格对齐官方版本关系。
我这边实际使用的版本组合是:
| 组件 | 版本 |
|---|---|
| JDK | 25(LTS) |
| Spring Boot | 3.5.x |
| Spring Cloud | 2025.x |
| Spring Cloud Alibaba | 2025.0.x |
| Nacos | 3.x |
| Sentinel | 1.8.x |
| Seata | 2.x |
这套组合在2025年已经比较成熟。Spring Cloud Alibaba 2025.0.x同时支持Spring Boot 3.5.x和Spring Cloud 2025.x,注册中心、配置中心、网关、熔断、分布式事务的兼容性都磨合得差不多了。版本匹配这件事千万别自己脑补,直接用官方版本说明里的对照关系,否则启动时各种NoSuchMethodError和BeanDefinitionStoreException会教你做人。
1.2 JDK 25给微服务带来的真正红利
JDK 25是LTS版本,真正影响微服务开发体验的其实不是那堆语法糖,而是运行时的几项关键改进。
首先,虚拟线程在JDK 25里已经非常成熟。以前一个Tomcat线程池几百个线程,每个线程占据1MB左右的栈空间,遇到IO密集型的业务(比如调用远程服务、查询数据库),大量线程其实都在阻塞等待。Spring Boot 3.5里开启虚拟线程只需一行配置:
yaml复制spring:
threads:
virtual:
enabled: true
开启后,Tomcat会使用虚拟线程来处理请求,并发能力不再受“线程数=硬件线程数”的物理限制。我压测过一个典型的订单查询接口,200并发持续打5分钟,在相同容器配置下,虚拟线程模式的吞吐量比传统平台线程模式提升了接近40%,而且GC压力没有明显增加。
其次,ZGC经过JDK 21到JDK 25的沉淀,已经可以在大多数服务上稳定使用。特别是分代ZGC,在微服务这种普遍“堆内存不大、对象分配频繁”的场景下,停顿时间能控制在1ms以内。配合JVM参数-XX:+UseZGC -XX:+ZGenerational,接口P99耗时显著下降,不再出现因为GC停顿导致的偶发超时。
1.3 什么时候这套组合不适合你
虽然我推荐这套组合,但它不是万能的。如果你的系统只是几个接口、每天几千请求,单体Spring Boot加个缓存就够了,上微服务只会增加Nacos、网关、Sentinel这些组件带来的运维成本。
另外,如果团队里没有人对容器、服务发现这些概念熟悉,一上来就微服务化,调试环境会变成灾难。我自己见过太多团队把微服务做成了“分布式单体”——服务拆了,但数据库还是共享一个,配置还是手动拷贝,出了问题只能一台一台翻日志。这样的微服务不是扩展性,是混乱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境落地:JDK 25、Docker、基础中间件装起来
2.1 JDK 25安装与多版本切换的实用姿势
JDK 25的安装本身不复杂,但如果你平时还要维护JDK 8或JDK 11的老项目,就必须考虑多版本共存的问题。
Linux服务器上我推荐用压缩包解压的方式安装,然后通过环境变量切换版本。安装步骤大致是:下载JDK 25的tar包,解压到/usr/local/jdk-25,然后在/etc/profile里配置:
bash复制export JAVA_HOME=/usr/local/jdk-25
export PATH=$JAVA_HOME/bin:$PATH
需要切换版本时,不用改全局变量,我会在项目启动脚本里显式指定:
bash复制export JAVA_HOME=/usr/local/jdk-17
$JAVA_HOME/bin/java -jar app.jar
这里有个小坑:某些依赖RMI、JMX的框架对JDK版本的敏感度比想象中高。JDK 8直接编译的旧代码在新JDK上运行不一定报错,但反射访问内部类、sun.misc.Unsafe这类用法在JDK 25上基本都废了。老项目升级前最好先跑一遍完整的jdeps分析,看看有没有用到被移除的API。
验证安装是否正确:
bash复制java -version
javac -version
输出对应21以上版本号才算正常。
2.2 Docker环境与镜像加速配置(含常见坑)
Docker的安装这里不展开,重点说Docker Desktop和Linux Docker in Docker在资源占用上的差异。我在Windows上开发时用Docker Desktop,把WSL2后端打开,这样容器的网络模式和Linux保持一致,避免很多Nacos、Seata这类依赖多端口的中间件在Docker Desktop和Linux上的行为差异问题。
Docker装好后,第一件事是配置镜像加速。直连官方镜像仓库拉取大镜像非常慢,通过/etc/docker/daemon.json配置registry-mirrors是基本操作:
json复制{
"registry-mirrors": ["https://docker.m.daocloud.io"]
}
配置完记得重启Docker:
bash复制systemctl restart docker
注意,不同云厂商的加速地址稳定性和速度差异挺大,建议备两三个。另外,Docker 24+以后对镜像拉取的并发和压缩传输优化了不少,如果仍然慢,可以检查DNS设置,8.8.8.8这种公共DNS不一定比本机运营商DNS好用,实测有时反而是默认DNS更快。
2.3 用Docker跑起MySQL和Redis
基础中间件里,MySQL 8.0和Redis是微服务系统绕不开的。用Docker部署时,我强烈建议把数据目录和配置目录都挂载到宿主机,否则容器一删数据全没了。
MySQL的启动命令我习惯这样写:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-e TZ=Asia/Shanghai \
-v /data/mysql/conf:/etc/mysql/conf.d \
-v /data/mysql/data:/var/lib/mysql \
mysql:8.0
TZ=Asia/Shanghai这个环境变量很多人会漏,如果漏了,后面所有时间字段都会差8小时,排查起来非常烦。
Redis的生产环境至少要做主从,单机Docker部署的快速版是:
bash复制docker run -d \
--name redis \
-p 6379:6379 \
-v /data/redis/conf:/usr/local/etc/redis \
redis:7.0 redis-server /usr/local/etc/redis/redis.conf
主从部署可以再加一个--slaveof参数或者直接在配置文件里指定。我在实际项目里不会单独起Redis主从容器,而是直接上Redis Cluster或者Sentinel,这个问题在后面的Docker Compose编排里再细说。
3. 服务怎么拆:一套可落地的微服务模块划分与工程结构
3.1 从单体到微服务的边界划分思路
微服务拆分最大的误区是“按技术层拆”——Controller一个服务、Service一个服务、Mapper一个服务,这是灾难。正确的拆法一定是“按业务域拆”,每个服务拥有完整的业务闭环和独立的数据库。
我参考若依微服务版本的后端结构,结合自己的项目实际,做了一个更贴合业务场景的划分:
text复制microservices-parent/
├── common/ # 公共模块
│ ├── common-core/ # 基础工具、统一返回、异常处理
│ ├── common-redis/ # Redis配置与工具
│ ├── common-security/ # 安全认证相关
│ └── common-log/ # 操作日志
├── gateway/ # 网关服务
├── modules/
│ ├── system-service/ # 系统管理服务(用户、角色、菜单)
│ ├── user-service/ # 用户服务
│ ├── order-service/ # 订单服务
│ └── product-service/ # 商品服务
└── sql/ # 各服务独立数据库脚本
用户服务和系统管理服务我故意分开了。用户服务面向C端注册登录,系统管理服务面向B端运营后台,它们的查询压力、扩展节奏完全不同,混在一个服务里互相拖累是必然的。
3.2 工程骨架与公共模块设计
公共模块的核心价值是杜绝重复代码,但也不能一股脑全塞进去。我见过很多项目把数据库Mapper也放进common层,然后每个服务都依赖一个巨大的公共包,结果是任何一个小改动都要全量重新编译发布。我的原则是:公共模块只放与业务无关的基础能力。
common-core里放统一返回结构Result<T>、全局异常处理器、分页参数。common-security放JWT工具类和Security过滤器链。common-redis放RedisTemplate的配置和缓存Key规范。
每个业务服务模块的结构保持一致:
text复制order-service/
├── src/main/java/com/example/order/
│ ├── controller/ # 接口层
│ ├── service/ # 业务层
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体
│ ├── dto/ # 数据传输对象
│ ├── feign/ # 远程调用接口
│ └── OrderApplication.java
└── src/main/resources/
├── bootstrap.yml # 连接Nacos的配置
└── application.yml # 业务配置
为什么单独放bootstrap.yml?因为Spring Cloud Alibaba里Nacos本身地址的配置必须在应用启动早期读取,放到application.yml里有时会因为加载顺序问题拿不到。这个细节排查起来非常隐蔽,一开始就规范好位置能省很多事。
3.3 可扩展性从一开始就要想清楚的事
可扩展性不是加个负载均衡器就叫可扩展,它体现在服务设计的每个取舍里。
第一,服务必须无状态。登录状态不能存在服务内存Session里,必须走JWT或者Redis集中缓存。这样任何实例被销毁、新建,都不会丢失用户上下文。
第二,配置必须外部化。数据源密码、中间件地址这些不能写死在配置文件并跟随镜像打包,要走Nacos配置中心。否则扩容一批新实例,你还要去找它们的配置文件修改,这就不叫可扩展了。
第三,数据库层面要预留拆分空间。比如订单表不直接和其他服务共享,而是在订单服务内部通过分库键设计来应对未来数据量增长。业务早期就按tenant_id或user_id做分片键,后续水平扩展会顺畅很多。
4. Spring Cloud Alibaba核心组件逐个落地
4.1 Nacos:注册中心与配置中心一起上
Nacos 3.x是目前Spring Cloud Alibaba体系里当之无愧的核心。它一个组件承担服务注册发现和配置管理两个职责,省去了同时维护Eureka和Spring Cloud Config两套系统的工作量。
Docker启动Nacos单机版:
bash复制docker run -d \
--name nacos \
-p 8848:8848 \
-p 9848:9848 \
-e MODE=standalone \
-e NACOS_AUTH_ENABLE=true \
-v /data/nacos/logs:/home/nacos/logs \
-v /data/nacos/data:/home/nacos/data \
nacos/nacos-server:v3.0.2
这里必须强调:-p 9848:9848不能省。Nacos从2.x开始,客户端和服务端之间有一部分通信走gRPC,默认端口是主端口+1000即9848,只映射8848会导致服务启动时注册成功,但过一会儿就被Nacos判定为不健康,因为心跳走的是gRPC端口。这个坑我踩过不止一次。
生产环境Nacos不能单机跑,至少三节点集群。集群模式下Nacos会选主,同时把配置数据持久化到MySQL,而不是默认的内嵌Derby数据库。切换MySQL存储需要在启动前挂载MySQL的初始化脚本,并将spring.datasource.platform=mysql配置进去。
服务接入Nacos的配置:
yaml复制spring:
cloud:
nacos:
discovery:
server-addr: nacos:8848
namespace: dev
config:
server-addr: nacos:8848
file-extension: yaml
namespace: dev
namespace用来隔离环境,dev、test、prod各一套命名空间,配置和服务注册数据天然隔离,不会互相污染。
4.2 Gateway网关:路由、鉴权、跨域
微服务网关我选Spring Cloud Gateway,而不是Zuul。Zuul 1.x基于Servlet模型,性能和Gateway基于WebFlux的响应式模型差了一个量级。Gateway的Netty模型配合虚拟线程的收益不是特别明显,因为Netty本身已经异步化了,但整体并发表现仍然优秀。
网关路由配置:
yaml复制spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
- id: system-service
uri: lb://system-service
predicates:
- Path=/api/system/**
lb://前缀是关键,它让网关通过LoadBalancer从Nacos拿到服务实例列表做负载均衡,而不是写死某个IP。这样服务扩容再多的实例也不影响入口。
鉴权我放在网关做一层全局过滤器。请求先走JWT校验,白名单路径直接放行,非白名单路径校验通过后把用户ID透传到下游服务的Header中。下游服务拿到X-User-Id这个请求头即可识别用户身份,不需要每个服务都重复解析JWT。
网关层跨域配置也要做统一处理,否则前端调用时每个服务都要处理CORS,代码会很脏。
4.3 OpenFeign + LoadBalancer:服务间调用
服务间调用我用OpenFeign。它本质上就是声明式HTTP客户端,接口定义好了,具体HTTP请求封装、连接池管理、超时配置都由框架完成。
一个典型的Feign接口定义:
java复制@FeignClient(name = "user-service", contextId = "userFeignClient")
public interface UserFeignClient {
@GetMapping("/api/user/info/{id}")
Result<UserInfoDTO> getUserInfo(@PathVariable("id") Long id);
}
注意contextId这个属性。在同一个服务里如果有多个FeignClient指向不同的服务,或者指向同一个服务的不同配置,必须加上contextId,否则启动时会报Bean definition naming conflict。
Feign结合Sentinel做降级,是微服务链路稳定性的重要保障。开启方式:
yaml复制feign:
sentinel:
enabled: true
开启后,Feign接口可以配置fallback类,当远程服务调用失败或触发熔断时,自动执行降级逻辑,而不是把异常直接抛给上层。
超时配置我踩过不少坑。Feign默认连接超时是10秒、读超时是60秒,这个值对大多数接口太长了。如果下游服务慢,上游服务的大量线程会一直占着等响应,最终拖垮整个系统。我会根据业务情况调到连接超时5秒、读超时10秒,并配合Sentinel设置对应降级规则。
4.4 Sentinel:限流熔断不是可选项
微服务系统上线后,真正让系统崩掉的往往不是高并发,而是某个慢接口拖垮了链路。Sentinel的价值就在于从流量入口到服务调用链路的每个节点都加上保护。
Sentinel控制台用Docker跑起来:
bash复制docker run -d \
--name sentinel-dashboard \
-p 8858:8080 \
bladex/sentinel-dashboard:1.8.8
服务里接入Sentinel:
yaml复制spring:
cloud:
sentinel:
transport:
dashboard: sentinel-dashboard:8858
eager: true
eager: true是必须的。默认情况下,Sentinel客户端是懒加载的——只有在第一次流量经过时才会注册到控制台。你满心欢喜打开控制台准备配规则,发现上面空无一物,就是因为没写这个配置。
限流规则我一般这样设计:
- 网关层按API维度限制QPS,比如
/api/order/**限流1000 QPS,超过直接返回429。 - 服务层按接口维度限制并发线程数,避免慢接口占满线程池。
- Feign调用下游时配置熔断规则,错误率超过20%自动开启熔断,10秒后半开重试。
Sentinel的规则支持通过Nacos配置持久化,生产环境不要直接在控制台配置规则——控制台规则默认存在内存里,服务一重启就没了。我当时为了图省事在控制台配了好几条规则,上线后服务一发布,所有规则全部丢失,又是配置中心下发才解决的问题。
4.5 Seata:分布式事务的取舍
分布式事务是微服务架构里最容易被过度设计的地方。我见过一个新项目,业务还没跑起来,先引了Seata做AT模式接入,结果SQL兼容问题、全局锁冲突问题接踵而来,代码写得乱七八糟。
我的建议是:能用最终一致性解决的就别用强一致。订单创建这个经典场景,推荐的做法是本地消息表+异步任务。订单服务写订单表的同时写一条消息表记录,通过定时任务或者MQ发送到库存服务,库存服务消费成功后回执,一切异步化。
如果业务确实需要强一致——比如转账、预扣款这种——再考虑Seata。Seata的AT模式对业务代码侵入最小,但它的基于全局锁的实现,在高并发写场景下会产生严重的锁等待。我实际测下来,AT模式在写冲突较多的表上吞吐量只有不使用事务的50%左右。更可靠的选择是TCC模式,但TCC需要你手写Try、Confirm、Cancel三段逻辑,复杂度摆在那里。
选型原则很简单:能异步就异步,能单体事务就别跨服务事务,实在不行才上Seata。
5. Docker化改造:从jar包到可编排的容器镜像
5.1 多阶段构建Dockerfile:镜像小了,构建也快了
很多新手写Java应用的Dockerfile喜欢用一个基础镜像,把jar包塞进去就完事。那样出来的镜像体积动辄1GB以上,构建和推送都慢得要命。多阶段构建可以彻底解决这个问题。
我实际在用的模板:
dockerfile复制FROM maven:3.9-eclipse-temurin-25 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENV TZ=Asia/Shanghai
ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "app.jar"]
第一阶段用Maven镜像完成编译打包,第二阶段只保留JRE和最终的jar包。两个阶段加起来,最终镜像体积从1GB左右降到300MB以内。这还不算完,RUN mvn dependency:go-offline会在构建阶段先把依赖下载好,后续源码变更时,依赖层会被Docker缓存,构建时间能缩短一半以上。
构建时有个隐藏细节:COPY pom.xml .之后要执行一次mvn dependency:go-offline,否则后续每次源码变化都会触发全部依赖重新下载,Maven仓库网络慢的时候,光构建就能等20分钟。
5.2 Docker Compose编排整个微服务系统
当服务的数量超过3个,再用docker run命令一个个启动就是自虐。Docker Compose可以把你整个微服务系统编排成一个定义文件,一条docker compose up -d全部拉起来,一条docker compose down全部清理,开发调试阶段体验非常好。
我项目的docker-compose.yml核心部分:
yaml复制version: "3.8"
services:
nacos:
image: nacos/nacos-server:v3.0.2
environment:
MODE: standalone
NACOS_AUTH_ENABLE: "true"
ports:
- "8848:8848"
- "9848:9848"
networks:
- micro-net
mysql8:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
TZ: Asia/Shanghai
volumes:
- /data/mysql/conf:/etc/mysql/conf.d
- /data/mysql/data:/var/lib/mysql
networks:
- micro-net
user-service:
build: ./modules/user-service
depends_on:
nacos:
condition: service_healthy
environment:
SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR: nacos:8848
networks:
- micro-net
gateway:
build: ./gateway
ports:
- "8080:8080"
depends_on:
- user-service
networks:
- micro-net
networks:
micro-net:
driver: bridge
两个细节必须注意。
第一,服务注册地址。在Compose网络里,user-service要注册到Nacos的地址是nacos:8848,不是localhost:8848。因为容器之间通过服务名访问,每个容器是独立的IP。
第二,depends_on默认不等待服务真正可用。depends_on: nacos只能保证Nacos容器先启动,不能保证Nacos已经完成初始化。所以Nacos的service定义要增加健康检查:
yaml复制 nacos:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8848/nacos/v1/console/health/readiness"]
interval: 10s
timeout: 5s
retries: 10
然后depends_on写成condition: service_healthy,这样服务会等待Nacos真正健康后才启动。
5.3 健康检查与启动顺序:容器世界里最难的一课
Java微服务的启动时间普遍在二三十秒以上,如果服务A启动时强依赖服务B,但B还没准备好,A就会抛连接异常直接退出。很多新手的做法是加大retries和restart: always,让容器不断重启直到成功。这种方案能跑,但非常浪费资源,而且如果不加健康检查,整个编排的依赖关系就是混乱的。
我的做法是每个业务服务都增加Spring Boot Actuator的健康检查端点:
yaml复制management:
endpoints:
web:
exposure:
include: health,info
然后在Compose里为每个服务配置:
yaml复制 user-service:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 15s
timeout: 5s
retries: 12
start_period: 30s
start_period是个容易被忽略的参数。Spring Boot应用启动慢,如果健康检查第一次请求在30秒内失败,不会被计入retries,应用有充分时间完成预热。
服务之间的依赖配置成condition: service_healthy之后,整个系统的启动才是严格有序的:Nacos先起来,MySQL先起来,然后业务服务注册上去,最后网关才启动并开始转发流量。
6. 真正意义的“可扩展”:压测、调优、水平扩容
6.1 一套本地压测方法:先让系统自己说话
系统搭好后,没有经过压测就直接上线,等于裸奔。我做压测用的工具是wrk,简单直接,一条命令就能测出吞吐和延迟分布。
bash复制wrk -t8 -c200 -d60s --latency http://localhost:8080/api/order/list
-t8表示8个线程,-c200表示200个并发连接,-d60s持续60秒,--latency输出P50、P75、P99延迟。
压测过程中要同步观察以下指标:
- 容器CPU和内存是否达到上限。
- JVM堆内存使用量和GC次数。
- 数据库连接池是否打满。
- Sentinel控制台限流规则是否被触发。
如果P99延迟突然飙升,多半不是代码问题,而是某个中间件线程池满了。比如数据库连接池HikariCP默认最大连接数是10,200并发打过来,大部分请求都在等数据库连接,接口自然就慢了。这时候调连接池上限,或者排查SQL慢查询,比改业务代码有效得多。
6.2 JVM和容器参数调优:别让内存成为瓶颈
Java应用容器化后,最容易踩的坑是JVM内存和容器内存限制不匹配。JDK 10以后JVM默认能感知容器内存限制,但只是把默认堆大小设为容器内存的1/4,如果你不显式设置-Xmx,一个容器限制4GB的服务,JVM最大堆只会有1GB,大量内存被浪费。
一个合理的堆内存设置原则:-Xmx设置为容器内存的50%-60%,给它留足Metaspace、线程栈、Direct Buffer这些堆外内存。比如容器限制2GB,启动参数这样写:
bash复制java -XX:+UseZGC -XX:+ZGenerational -Xmx1g -Xms1g -jar app.jar
-Xms和-Xmx设置为相同值,避免JVM运行时动态扩缩堆导致性能抖动。
还有-XX:MaxMetaspaceSize我建议大家设置一个上限,默认值只受容器内存限制,某些框架生成的代理类过多时,Metaspace膨胀会占掉大量堆外内存。我一般设256MB或512MB,根据服务加载的类数量调整。
6.3 水平扩容与灰度发布:docker compose scale的边界
Docker Compose本身支持docker compose up --scale user-service=3,当你把服务配置里的固定端口移除、通过Nacos做服务发现后,扩容一条命令就够了。
但说实话,scale命令的能力边界很明显:它只能做无状态服务实例的“水平复制”,不支持自动伸缩、滚动更新、灰度流量控制。如果在单机环境下做小规模测试,用scale完全够用。生产环境的多节点集群,我会转向Docker Swarm或Kubernetes,借助它们的滚动更新和HPA特性做自动伸缩。
在Compose阶段提前做好的一个设计是:服务之间通过Nacos名称互相调用,不依赖IP和固定端口。这样未来迁移到K8s时,服务注册发现模型不变,工作量和改造成本会小很多。
灰度发布方面,哪怕在Compose阶段也可以先做雏形。Nacos注册中心支持preserved.register.source和metadata自定义字段,可以在服务实例维度打标签,然后通过网关路由规则把特定版本的服务流量切给特定用户组。我测试时常用的做法是给新版服务加一个version=gray的元数据,然后在网关过滤器里根据请求头判断是否走灰度实例。这套逻辑虽然简陋,但提前把灰度能力走通了,后续上K8s后只是换一个更成熟的Istio或Argo Rollouts实现。
6.4 压测后的一个真实调优案例
文章开头提到我压测订单接口时虚拟线程带来40%的吞吐提升,这个提升不是开关虚拟线程就白来的,中间也经历了一轮参数调整。
第一次压测用的配置是默认的Tomcat线程池,200并发下P99已经到980ms,性能不达标。开启虚拟线程后,P99跌到420ms左右,但还是偏高。接着排查发现,接口里调了用户服务拿用户信息,通过Feign的读超时是10秒,如果用户服务有个别实例响应慢,连接池会积压。
于是做了三个调整:
- Feign读超时从10秒降到3秒。
- Sentinel给
user-service的getUserInfo接口设置10 QPS的阈值并配置fallback为本地缓存数据。 - 用户在订单查询场景不需要强一致的用户最新信息,用Caffeine本地缓存5分钟。
三轮调整后,压测结果P99稳定在120ms左右,吞吐量比初版提升了将近70%。这个案例在很多团队都很有代表性——性能瓶颈往往不在单一代码,而在链路中多个环节的耦合累积。
最后再聊一个实操中的编排习惯
整套系统容器化之后,我养成了一个习惯:每次发布新版本前,都会先写一份docker-compose的override文件,把这个服务的副本数降到0,等新版本镜像构建完再拉起来。这样做的好处是不会出现在滚动发布期间,网关把流量转发到正在重启的实例上导致短暂503的情况。
在容器编排里,“优雅上下线”是微服务稳定性的隐藏指标。Spring Cloud Alibaba + Nacos天然支持服务实例的优雅下线——服务停止前先向Nacos反注册,网关和Feign客户端不会再拿到这个实例的地址。但因为JVM进程退出的顺序问题,实例在Nacos下线后可能还有一小段时间处于半连接状态。为了解决这个问题,我在启动脚本里加了preStop钩子,先调Nacos下线接口,再sleep 10秒给存量请求留出处理时间,最后才真正关闭进程。这个细节不贵,但非常值得写进你的标准发布流程里。
