springcloud-gateway+nacos 实现灰度发布

现状

原理

网关的大致实现原理如下图:
在这里插入图片描述

问题

当某一天,服务1扩展了一个新功能, 但这一部分功能只针对某一部分用户开发,抽象来说就是:请求方直接圈定了具体的服务节点,该如何实现呢?
在这里插入图片描述

基于 LoadBalancerClientFilter +Ribbon 实现

集成

maven依赖

 <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.2.6.RELEASE</version>
      <relativePath/>
  </parent>
  <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      
	<dependency>
	     <groupId>com.alibaba.cloud</groupId>
	     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	 </dependency>
	
	 <dependency>
	     <groupId>com.alibaba.cloud</groupId>
	     <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
	 </dependency>
   </dependencies>

<dependencyManagement>
      <dependencies>
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-dependencies</artifactId>
              <!--"Hoxton.SR3": "Spring Boot >=2.2.0.M4 and <2.3.0.BUILD-SNAPSHOT" -->
              <version>Hoxton.SR3</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>


          <dependency>
              <groupId>com.alibaba.cloud</groupId>
              <artifactId>spring-cloud-alibaba-dependencies</artifactId>
              <version>2.2.1.RELEASE</version>
              <type>pom</type>
              <scope>import</scope>
          </dependency>
      </dependencies>
  </dependencyManagement>

properties配置

spring:
  application:
    name: my-gateway
  cloud:
    nacos:
      discovery:
        server-addr: YOUR_IP:8848
        register-enabled: false
        group: DEFAULT_GROUP
      config:
        server-addr: YOUR_IP:8848
        file-extension: yaml
        group: DEFAULT_GROUP
    gateway:
      enabled: true
      discovery:
        locator:
          ## 注册发现
          enabled: true
          lower-case-service-id: true

配置NacosRule

@Bean
@Scope(value = "prototype")
public IRule nacosRule() {
	return new NacosRule();
}

注册发现源码分析

xxxAutoConfiguration

//org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration
public class GatewayLoadBalancerClientAutoConfiguration {

	@Bean
	@ConditionalOnBean(LoadBalancerClient.class)
	@ConditionalOnMissingBean({ LoadBalancerClientFilter.class,
			ReactiveLoadBalancerClientFilter.class })
	public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client,
			LoadBalancerProperties properties) {
		return new LoadBalancerClientFilter(client, properties);
	}

}

//org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration
public class RibbonAutoConfiguration {
	@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}
}

源码分析

//org.springframework.cloud.gateway.filter.LoadBalancerClientFilter
//1.
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
		String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
		//如果url前缀不是 lb:// 直接返回 
		if (url == null
				|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
			return chain.filter(exchange);
		}
		// preserve the original url
		addOriginalRequestUrl(exchange, url);


		//1.1
		final ServiceInstance instance = choose(exchange);


		URI uri = exchange.getRequest().getURI();


		//替换url
		URI requestUrl = loadBalancer.reconstructURI(
				new DelegatingServiceInstance(instance, overrideScheme), uri);
		//略.....
		return chain.filter(exchange);
	}

	//1.1 从注册中心选择 实例 
	//loadBalancer 为: org.springframework.cloud.client.loadbalancer.LoadBalancerClient 实例: RibbonLoadBalancerClient
	protected ServiceInstance choose(ServerWebExchange exchange) {
		return loadBalancer.choose(
				((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
	}
}


//2.
public class RibbonLoadBalancerClient implements LoadBalancerClient {

	@Override
	public ServiceInstance choose(String serviceId) {
		return choose(serviceId, null);
	}

	public ServiceInstance choose(String serviceId, Object hint) {
		//2.1
		//2.2
		Server server = getServer(getLoadBalancer(serviceId), hint);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
	}


	//2.1 返回 com.netflix.loadbalancer.ILoadBalancer 的实现类 DynamicServerListLoadBalancer, 并设置serviceId
	protected ILoadBalancer getLoadBalancer(String serviceId) {
		return this.clientFactory.getLoadBalancer(serviceId);
	}

	//2.2 通过DynamicServerListLoadBalancer 获取服务实例
	protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}
}

//3. DynamicServerListLoadBalancer.chooseServer
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
	//public Server chooseServer(Object key) 由父类实现
}

//4.  com.netflix.loadbalancer.BaseLoadBalancer
public Server chooseServer(Object key) {
    if (counter == null) {
        counter = createCounter();
    }
    counter.increment();
    if (rule == null) {
        return null;
    } else {
        try {
        	//rule 由容器注入NacosRule
            return rule.choose(key);
        } catch (Exception e) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
            return null;
        }
    }
}

//5.  com.alibaba.cloud.nacos.ribbon.NacosRule(略)
// 根据NameService查找服务实例,再通过自身的负载均衡算法 选取节点

扩展

扩展LoadBalancerClientFilter
拦截请求头中的accept_version_id,并封装为自定义对象,由后续的负载均衡策略使用。

public class GrayReleaseRibbonLoadBalancerClientFilter extends LoadBalancerClientFilter {
    private static final String ACCEPT_VERSION_ID_HEADER_KEY = "accept_version_id";

    private boolean support;

    public GrayReleaseRibbonLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
        super(loadBalancer, properties);
        support = loadBalancer instanceof RibbonLoadBalancerClient;
    }

    @Override
    protected ServiceInstance choose(ServerWebExchange exchange) {
        String serviceId = ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost();
        if (support) {
            List<String> acceptVersionIds = exchange.getRequest().getHeaders().get(ACCEPT_VERSION_ID_HEADER_KEY);
            return ((RibbonLoadBalancerClient) loadBalancer).choose(serviceId, new GrayReleaseKey(acceptVersionIds));
        }
        return loadBalancer.choose(serviceId);
    }

    public static class GrayReleaseKey {
        private static final List<String> EMPTY_VERSION_LIST = Collections.emptyList();

        @Getter
        private List<String> acceptVersionIds;

        public GrayReleaseKey(List<String> acceptVersionIds) {
            this.acceptVersionIds = acceptVersionIds == null ? EMPTY_VERSION_LIST : acceptVersionIds;
        }
    }
}

重写NacosRule
通过key携带的版本信息,进一步筛选服务实例,从而达到灰度发布的效果。

public class GrayReleaseNacosRule extends AbstractLoadBalancerRule {
    @Override
    public Server choose(Object key) {
    	String clusterName = this.nacosDiscoveryProperties.getClusterName();
		DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
		String name = loadBalancer.getName();

		NamingService namingService = nacosDiscoveryProperties
				.namingServiceInstance();
		List<Instance> instances = namingService.selectInstances(name, true);
		if (CollectionUtils.isEmpty(instances)) {
			return null;
		}
		List<Instance> instancesToChoose = instances;
		//根据clusterName筛选服务(略...)
		
		//自定义实现: 根据传递来的版本号
       instancesToChoose = chooseInternel(key, instancesToChoose);
       Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);

       return new NacosServer(instance);
    }

protected List<Instance> chooseInternel(Object key, List<Instance> instancesToChoose) {
        List<Instance> versionInstanceToChoose = instancesToChoose;
        List<String> candidateVersionIds;
        if (key instanceof GrayReleaseRibbonLoadBalancerClientFilter.GrayReleaseKey && !(candidateVersionIds = ((GrayReleaseRibbonLoadBalancerClientFilter.GrayReleaseKey) key).getAcceptVersionIds()).isEmpty()) {
            List<Instance> sameVersionInstances = instancesToChoose
                    .stream()
                    .filter(item -> {
                        String versionId = item.getMetadata().get(VERSION_ID_METADATA_KEY);
                        return candidateVersionIds.contains(versionId);
                    })
                    .collect(Collectors.toList());
            if (!sameVersionInstances.isEmpty()) {
                versionInstanceToChoose = sameVersionInstances;
           
            }
        }
        return versionInstanceToChoose;
    }
}

注入扩展实现类

    @Bean
    public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client,
                                                             LoadBalancerProperties properties) {
        return new GrayReleaseRibbonLoadBalancerClientFilter(client, properties);
    }

    @Bean
    @Scope(value = "prototype")
    public IRule nacosRule() {
        //重写 NacosRule
        return new GrayReleaseNacosRule ();
    }

服务注册-设置metadata
在服务注册到nacos注册中心时,添加metadata._version_id用来标识版本号。

spring:
  application:
    name: authentication-center-service
  cloud:
    nacos:
      discovery:
        metadata:
          _version_id: v_20210114

请求
请求头中指定版本信息。
在这里插入图片描述

基于 ReactiveLoadBalancerClientFilter 实现(待续)

springcloud gateway 基于WebFlux实现,推荐使用ReactiveLoadBalancerClientFilter来实现负载均衡策略。
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值