JAVA面试题分享四百六十:Spring Cloud Gateway + Nacos 灰度发布

小明 2025-05-02 13:34:09 7

���录

前言

环境搭建

父项目

服务提供者 provider

网关 gateway

动态路由

灰度发布

依赖配置

负载均衡策略

过滤器加载负载均衡

注入过滤器

发布灰度服务

测试

仓库地址


前言

本文将会使用 SpringCloud Gateway 网关组件配合 Nacos 实现灰度发布(金丝雀发布)

环境搭建

创建子模块服务提供者  provider,网关模块  gateway

父项目

pom.xml 配置

     4.0.0     com.example     spring-gateway-demo     0.0.1-SNAPSHOT     pom     spring-gateway-demo     spring-gateway-demo              11         11         11         3.8.1         2.3.7.RELEASE         2.2.2.RELEASE         Hoxton.SR9         2.2.0.RELEASE                   provider         gateway                                com.alibaba.cloud             spring-cloud-starter-alibaba-nacos-config                               com.alibaba.cloud             spring-cloud-starter-alibaba-nacos-discovery                                                          org.springframework.cloud                 spring-cloud-dependencies                 ${spring-cloud.version}                 pom                 import                                           org.springframework.boot                 spring-boot-dependencies                 ${spring-boot.version}                 pom                 import                                           com.alibaba.cloud                 spring-cloud-alibaba-dependencies                 ${spring-cloud-alibaba.version}                 pom                 import                            

服务提供者 provider

这里我们计划引入  nacos, 所以先创建一个 nacos 配置文件  dataId  为  provider.properties, 这里用默认的命名空间  public, 默认分组  DEFAULT_GROUP

version=2 

provider 的 pom 配置依赖

              org.springframework.boot         spring-boot-starter-web                   org.springframework.boot         spring-boot-starter-test         test                                       org.junit.vintage                 junit-vintage-engine                            

application.yml

server:   port: 9001 spring:   application:     name: provider   cloud:     nacos:       config:         server-addr: 127.0.0.1:8848       discovery:         server-addr: 127.0.0.1:8848 

启动类上添加 @EnableDiscoveryClient 注解

@EnableDiscoveryClient @SpringBootApplication public class ProviderApplication {     public static void main(String[] args) {         SpringApplication.run(ProviderApplication.class, args);     } } 

然后添加测试 controller

@RefreshScope @RestController @RequestMapping("/test") public class TestController {     @Autowired     private Environment env;     @Value("${version:0}")     private String version;          @GetMapping("/port")     public Object port() {         return String.format("port=%s, version=%s", env.getProperty("local.server.port"), version);     } } 

注意,这里配置 nacos 的时候需要配置下面两个文件 provider.properties 和 provider,然后实际配置最终 nacos 是采用的 provider 文件,否则后端控制台就会持续输出 400 错误,可能是新版本问题,其他版本暂时不清楚(后面网关配置也是同理)

后端控制台输出,也可以看出需要两个

[fixed-localhost_8848] [subscribe] provider.properties+DEFAULT_GROUP [fixed-localhost_8848] [add-listener] ok, tenant=, dataId=provider.properties, group=DEFAULT_GROUP, cnt=1 [fixed-localhost_8848] [subscribe] provider+DEFAULT_GROUP [fixed-localhost_8848] [add-listener] ok, tenant=, dataId=provider, group=DEFAULT_GROUP, cnt=1 

Nacos 版本:2.3.0-BETA

网关 gateway

gateway 服务的 pom 依赖配置如下:

                          org.springframework.cloud             spring-cloud-starter-gateway                               org.projectlombok             lombok             true                               org.springframework.boot             spring-boot-starter-test             test                                                   org.junit.vintage                     junit-vintage-engine                                                             org.springframework.boot             spring-boot-starter-actuator               

application.yml

# 应用服务 WEB 访问端口 server:   port: 9000 # 应用名称 spring:   application:     name: gateway   cloud:     nacos:       config:         server-addr: 127.0.0.1:8848       discovery:         server-addr: 127.0.0.1:8848     gateway:       routes: # http://127.0.0.1:9000/actuator/gateway/routes         - id: provider  # 路由 ID,保持唯一           uri: lb://provider # uri指目标服务地址,lb代表从注册中心获取服务           predicates:             - Path=/provider/**  # http://127.0.0.1:9000/provider/port 会转发到 http://localhost:9001/provider/port, 和预期不符合, 需要StripPrefix来处理           filters:             - StripPrefix=1 # StripPrefix=1就代表截取路径的个数为1, 这样请求 http://127.0.0.1:9000/provider/test/port 会转发到 http://localhost:9001/test/port management:   endpoint:     gateway:       enabled: true   endpoints:     web:       exposure:         include: gateway 

同样启动类上添加 @EnableDiscoveryClient 注解

查看所有路由:/actuator/gateway/routes 查看指定路由(GET):/actuator/gateway/routes/{id} 查看全局过滤器:/actuator/gateway/globalfilters 查看路由过滤器:/actuator/gateway/routefilters POST 方式刷新路由缓存:/actuator/gateway/refresh

测试

curl http://127.0.0.1:9001/test/port port=9001, version=2 curl http://127.0.0.1:9000/provider/test/port port=9001, version=2 

动态路由

实现动态路由有两种方式,一个是改写  RouteDefinitionRepository(实测失败),一个是基于  nacos  的监听器给  RouteDefinitionRepository  动态更新值。实现逻辑大同小异

Spring Cloud Gateway 中加载路由信息分别由以下几个类负责 1、PropertiesRouteDefinitionLocator:从配置文件中读取路由信息 (如 YML、Properties 等) 2、RouteDefinitionRepository:从存储器中读取路由信息 (如内存、配置中心、Redis、MySQL 等) 3、DiscoveryClientRouteDefinitionLocator:从注册中心中读取路由信息(如 Nacos、Eurka、Zookeeper 等)

下面使用 RouteDefinitionRepository 配置动态路由

gateway-router.json

[{     "id": "provider",     "predicates": [{         "name": "Path",         "args": {             "_genkey_0": "/provider/**"         }     }],     "filters": [{         "name": "StripPrefix",         "args": {             "_genkey_0": "1"         }     }],     "uri": "lb://provider",     "order": 0 }] 

NacosRouteDefinitionRepository 配置类

@Component public class NacosRouteDefinitionRepository implements RouteDefinitionRepository, ApplicationEventPublisherAware {     private static final Logger log = LoggerFactory.getLogger(NacosRouteDefinitionRepository.class);     @Autowired     private NacosConfigManager nacosConfigManager;     // 更新路由信息需要的     private ApplicationEventPublisher applicationEventPublisher;     private String dataId = "gateway-router.json";     private String group = "DEFAULT_GROUP";     @Value("${spring.cloud.nacos.config.server-addr}")     private String serverAddr;     private ObjectMapper objectMapper = new ObjectMapper();     @PostConstruct     public void dynamicRouteByNacosListener() {         try {             nacosConfigManager.getConfigService().addListener(dataId, group, new Listener() {                 public void receiveConfigInfo(String configInfo) {                     log.info("自动更新配置...\r\n{}", configInfo);                     applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));                 }                 public Executor getExecutor() {                     return null;                 }             });         } catch (NacosException e) {             e.printStackTrace();         }     }     @Override     public Flux getRouteDefinitions() {         try {             String configInfo = nacosConfigManager.getConfigService().getConfig(dataId, group, 5000);             List gatewayRouteDefinitions = objectMapper.readValue(configInfo, new TypeReference() {             });             return Flux.fromIterable(gatewayRouteDefinitions);         } catch (NacosException e) {             e.printStackTrace();         } catch (JsonMappingException e) {             e.printStackTrace();         } catch (JsonProcessingException e) {             e.printStackTrace();         }         return Flux.fromIterable(Lists.newArrayList());     }     @Override     public Mono save(Mono route) {         return null;     }     @Override     public Mono delete(Mono routeId) {         return null;     }     @Override     public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {         this.applicationEventPublisher = applicationEventPublisher;     } } 

然后重启网关,访问 http://127.0.0.1:9000/actuator/gateway/routes ,查看是否生效

[     {         "predicate": "Paths: [/provider/**], match trailing slash: true",         "route_id": "provider",         "filters": [             "[[StripPrefix parts = 1], order = 1]"         ],         "uri": "lb://provider",         "order": 0     } ] 

灰度发布

首先需要明白灰度的场景, 因为有不同版本的服务需要共存, 所以新的节点升级的时候必然代码及配置会存在差别, 所以我们根据这种差别来判断服务版本是新版本还是线上稳定版本。这里我们用  prod  和  gray  来标识 2 个版本。

实现的整体思路:

  1. 编写带版本号的灰度路由(负载均衡策略)

  2. 编写自定义 filter

  3. nacos 服务配置需要灰度发布的服务的元数据信息以及权重(在服务 jar 中配置)

注意, 应该先修改 nacos 配置实现动态路由, 然后再升级灰度节点. 本案例只是简单示例灰度原理。

下面进行网关配置

依赖配置

首先排除掉默认的 ribbon 依赖

                     com.alibaba.cloud             spring-cloud-starter-alibaba-nacos-discovery                                                   org.springframework.cloud                     spring-cloud-starter-netflix-ribbon                                        

引入官方新的负载均衡包

                     org.springframework.cloud             spring-cloud-starter-loadbalancer          

负载均衡策略

public class VersionGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {     private ObjectProvider serviceInstanceListSupplierProvider;     private String serviceId;     private final AtomicInteger position;     public VersionGrayLoadBalancer(ObjectProvider serviceInstanceListSupplierProvider, String serviceId) {         this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000));     }     public VersionGrayLoadBalancer(ObjectProvider serviceInstanceListSupplierProvider, String serviceId, int seedPosition) {         this.serviceId = serviceId;         this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;         this.position = new AtomicInteger(seedPosition);     }     @Override     public Mono choose(Request request) {         HttpHeaders headers = (HttpHeaders) request.getContext();         ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);         return ((Flux) supplier.get()).next().map(list -> processInstanceResponse((List) list, headers));     }     private Response processInstanceResponse(List instances, HttpHeaders headers) {         if (instances.isEmpty()) {             return new EmptyResponse();         } else {             String reqVersion = headers.getFirst("version");             if (StringUtils.isEmpty(reqVersion)) {                 return processRibbonInstanceResponse(instances);             }             List serviceInstances = instances.stream()                     .filter(instance -> reqVersion.equals(instance.getMetadata().get("version")))                     .collect(Collectors.toList());             if (serviceInstances.size() > 0) {                 return processRibbonInstanceResponse(serviceInstances);             } else {                 return processRibbonInstanceResponse(instances);             }         }     }          private Response processRibbonInstanceResponse(List instances) {         int pos = Math.abs(this.position.incrementAndGet());         ServiceInstance instance = instances.get(pos % instances.size());         return new DefaultResponse(instance);     } } 

过滤器加载负载均衡

public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {     private static final Log log = LogFactory.getLog(ReactiveLoadBalancerClientFilter.class);     private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;     private final LoadBalancerClientFactory clientFactory;     private LoadBalancerProperties properties;     public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {         this.clientFactory = clientFactory;         this.properties = properties;     }     @Override     public int getOrder() {         return LOAD_BALANCER_CLIENT_FILTER_ORDER;     }     @Override     public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {         URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);         String schemePrefix = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);         if (url != null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix))) {             ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);             if (log.isTraceEnabled()) {                 log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);             }             return this.choose(exchange).doOnNext((response) -> {                 if (!response.hasServer()) {                     throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());                 } else {                     URI uri = exchange.getRequest().getURI();                     String overrideScheme = null;                     if (schemePrefix != null) {                         overrideScheme = url.getScheme();                     }                     DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance) response.getServer(), overrideScheme);                     URI requestUrl = this.reconstructURI(serviceInstance, uri);                     if (log.isTraceEnabled()) {                         log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);                     }                     exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);                 }             }).then(chain.filter(exchange));         } else {             return chain.filter(exchange);         }     }     protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {         return LoadBalancerUriTools.reconstructURI(serviceInstance, original);     }     private Mono choose(ServerWebExchange exchange) {         URI uri = (URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);         VersionGrayLoadBalancer loadBalancer = new VersionGrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());         if (loadBalancer == null) {             throw new NotFoundException("No loadbalancer available for " + uri.getHost());         } else {             return loadBalancer.choose(this.createRequest(exchange));         }     }     private Request createRequest(ServerWebExchange exchange) {         HttpHeaders headers = exchange.getRequest().getHeaders();         Request request = new DefaultRequest(headers);         return request;     } } 

注入过滤器

@Configuration public class GrayGatewayReactiveLoadBalancerClientAutoConfiguration {     @Bean     @ConditionalOnMissingBean({GrayReactiveLoadBalancerClientFilter.class})     public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {         return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties);     } } 

发布灰度服务

生产环境配置文件 application-prod.yml

server:   port: 9002 spring:   application:     name: provider   cloud:     nacos:       config:         server-addr: 127.0.0.1:8848       discovery:         metadata:           version: prod         server-addr: 127.0.0.1:8848 

灰度环境配置文件 application-gray.yml

server:   port: 9003 spring:   application:     name: provider   cloud:     nacos:       config:         server-addr: 127.0.0.1:8848       discovery:         metadata:           version: gray         server-addr: 127.0.0.1:8848 

idea 启动参数指定配置文件

 同时注意配置两个环境的 nacos 文件(prod version: 4,gray version: 5)

测试

然后分别启动三个服务:9000 端口网关 gateway 服务,9002 端口生产环境 provider-prod 服务,9003 端口灰度环境 provider-gray 服务

E:\Nacos\nacos>curl http://127.0.0.1:9000/provider/test/port port=9003, version=5 E:\Nacos\nacos>curl -X GET -H "version:prod" http://127.0.0.1:9000/provider/test/port port=9003, version=5 E:\Nacos\nacos>curl -X GET -H "version:gray" http://127.0.0.1:9000/provider/test/port port=9002, version=4 

仓库地址

如果对于文章中代码有疑问,可以直接查看下方作者仓库

仓库地址:ReturnTmp/spring-gateway-demo: 网关配置 + 灰度发布 + 配置中心 示例仓库 (github.com)[1]

The End
微信