Last time I wrote about the implementation of the ribbon on the Gateway. Since the ribbon is blocked and not suitable for use on the gateway, switch to Spring Cloud LoadBanlancer and use the CircuitBreaker with it

Reference 0.

Article 1 Spring Tips: Spring Cloud LoadBalancer Spring Cloud LoadBalancer Spring Cloud LoadBalancer Spring Cloud Commons LoadBalancer Spring – cloud. Version Hoxton. SR7 spring – the boot – starter – parent 2.3.2. RELEASE

1. The train of thought

It’s still based on the Weight Route Predicate Factory. The idea is to configure the version number and weight ratio on the config to achieve the purpose of specifying the version distribution. For example, the configuration file is configured as follows

-id: temp_old URI: lb:// Template predicates: -path =/temp/** -versionWeight =group1, 99, v1 # Filters :// Template predicates: -path =/temp/** -versionWeight =group1, 99, v1 # - StripPrefix=1 - id: temp_new uri: lb://TEMPLATE predicates: - Path=/temp/** - VersionWeight=group1, 1, v2 filters: - StripPrefix=1

99% of the traffic will go to the old version V1 and only 1% to the new version V2. The Gateway uses WebFlux, but my service is a normal servlet, so I still use Ribbion’s load balancing strategy on the server side

1.1 Call link

Client request link – > Weight Route Predicate (assertions) – > ReactiveLoadBalancerClientFilter (if the configuration on the uri “lb” will enter the reactive load balancing) – > Chooes (ServerWebExchange exchange), the method derives from the LoadBalancerClientFactory ReactorLoadBalancer < ServiceInstance > is load balancing strategy beans) -> LoadBalancer.Choose (CreateRequest ()) (Load Balancing algorithm picks out the service instance) -> requests to the specific service

Complete the service instance selection

1.2 Custom load balancing strategy

The official documentation mentions that custom load balancing policies can use custom load balancing profiles

public class CustomLoadBalancerConfiguration {

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

Once this is defined, @LoadBalancerClient specifies the custom configuration for each Service(‘ Stores’ in the code is ServiceID)

@Configuration
@LoadBalancerClient(value = "stores", configuration = CustomLoadBalancerConfiguration.class)
public class MyConfiguration {
}

2. Point

  1. Write a GrayContext that handles the context passing of version based on ThreadLocal.
  2. A custom VersionLoadBalancer inheritance ReactorServiceInstanceLoadBalancer, implement internal choose method, (which can be reference RoundRobinLoadBalancer implementation).

    public class VersionLoadBalancer implements ReactorServiceInstanceLoadBalancer{ ... / / omit other code @ Override public Mono < Response < ServiceInstance > > choose Request (Request) {ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider .getIfAvailable(NoopServiceInstanceListSupplier::new); return supplier.get().next().map(this::getInstanceResponse); } private Response<ServiceInstance> getInstanceResponse( List<ServiceInstance> instances) { if (instances.isEmpty()) { log.warn("No servers available for service: " + this.serviceId); return new EmptyResponse(); } / / GrayUtil custom tools from the current thread to retrieve the version String version = GrayUtil. GetCurrentConextStrValue (" version "); If (StringUtil.isNotEmpty(version)){// If no metadata version is configured, take a random reference roundrobinLoadBalancer if(! instances.stream().allMatch((item->item.getMetadata() ! =null && item.getMetadata().get("version") ! = null))){ int pos = Math.abs(this.position.incrementAndGet()); ServiceInstance instance = instances.get(pos % instances.size()); return new DefaultResponse(instance); } // When version is configured, Choose the version of the same services Optional < ServiceInstance > Optional = instances. The stream (). The filter (item - > {the if (item. For getMetadata ()! = null && item.getMetadata().get("version") ! = null){ return version.equals(item.getMetadata().get("version")); } return false; }).findFirst(); if(optional.isPresent()){ return new DefaultResponse(optional.get()); } } log.warn(String.format("No servers available for service %s, version: %s", this.serviceId, version)); return new EmptyResponse(); }}
  3. Custom LoadBalancerConfiguration

    public class VersionLoadBalancerConfiguration { @Bean ReactorLoadBalancer<ServiceInstance> versionLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new VersionLoadBalancer(loadBalancerClientFactory .getLazyProvider(name, ServiceInstanceListSupplier.class), name); } @Bean public ServiceInstanceListSupplier versionClientServiceInstanceListSupplier( ConfigurableApplicationContext context) { ServiceInstanceListSupplier serviceInstanceListSupplier = ServiceInstanceListSupplier.builder() .withDiscoveryClient().withZonePreference() //.withCaching() // The test environment does not cache.build(context); return serviceInstanceListSupplier; }}
  4. Custom @ EnableGrayLoadBalancerClient annotations, consult @ LoadBalancerClient implementation rules for each service binding his own load.

    @Configuration(proxyBeanMethods = false) @Import(GrayLoadBalancerClientConfigurationRegistrar.class) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface EnableGrayLoadBalancerClient {/** * config file * @return */ Class<? > configuration() ; }
    public class GrayLoadBalancerClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar , ResourceLoaderAware { private ApplicationContext context; private Binder binder; @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { Map<String, Object> attrs = metadata .getAnnotationAttributes(EnableGrayLoadBalancerClient.class.getName()); List<String> appNames = getServiceIdfromRoute(); appNames.forEach(name->{ if (attrs ! = null && attrs.containsKey("configuration")) { registerClientConfiguration(registry,name, attrs.get("configuration")); }}); } private static void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(LoadBalancerClientSpecification.class); builder.addConstructorArgValue(name); builder.addConstructorArgValue(configuration); registry.registerBeanDefinition(name + ".VersionLoadBalancerClientSpecification", builder.getBeanDefinition()); } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.context = (ApplicationContext) resourceLoader; this.binder = Binder.get(this.context.getEnvironment()); } // Those that use the versionWeight assertion are added to the payload, but not all are. private List<String> getServiceIdfromRoute(){ Map<String, Object> routes = binder.bind("spring.cloud.gateway.routes" , Bindable.mapOf(String.class, Object.class)).get(); return routes.values().stream().filter(item->{ LinkedHashMap<String, List> map = (LinkedHashMap<String, List>) item; LinkedHashMap<String, Object> predicates =(LinkedHashMap<String, Object>) map.get("predicates"); return predicates.values().stream().anyMatch(lm ->{ if(lm instanceof LinkedHashMap){ return ((LinkedHashMap)lm).values().stream().anyMatch(s->{ if(s instanceof String){ return ((String)s).contains("VersionWeight"); } return false; }); }else if(lm instanceof String){ return ((String)lm).contains("VersionWeight"); } return false; }); }).map(item->((LinkedHashMap)item).get("uri")).map(item->{ String strItem = (String)item; return strItem.substring(5); }).distinct().collect(Collectors.toList()); }

    @ LoadBalancerClient is done by LoadBalancerClientSpecification children isolation, and the realization way fegin consistent. Can join the LoadBalancerClientFactory bean, and container instantiation is in clientFactory getInstance (name) to be instantiated. GetInstance this method is invoked in ReactiveLoadBalancerClientFilter, implied that will not happen when the first call to instantiate and container loading LoadBalancer.

  5. Monitor routing refresh event (RefreshScopeRefreshedEvent) considering the gray level we released is in the routing configuration, when routing change, add server routing, service will need to rewrite the new rules of the load.

    public class RefreshGrayRouteListener implements ApplicationListener<ApplicationEvent>, BeanFactoryAware, BeanDefinitionRegistryPostProcessor { private BeanFactory beanFactory; private BeanDefinitionRegistry beanDefinitionRegistry; / / registered LoadBalancerClientSpecification private static String registerClientConfiguration (BeanDefinitionRegistry registry, Object name) { String beanName = name + ".VersionLoadBalancerClientSpecification"; BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(LoadBalancerClientSpecification.class); builder.addConstructorArgValue(name); builder.addConstructorArgValue(new Class[]{VersionLoadBalancerConfiguration.class}); registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); return beanName; } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof RefreshScopeRefreshedEvent) { refreshLoadBalancerBean(); } } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } private void refreshLoadBalancerBean() { LoadBalancerClientFactory clientFactory = beanFactory.getBean(LoadBalancerClientFactory.class); // Todo: the clientFactory.destroy() that cannot be deleted; GatewayProperties properties = BeanFactory.getBean (gatewayProperties. Class); List<String> ServiceIds = properties.getRoutes().stream().filter(routeDefinition -> { return routeDefinition.getPredicates().stream().anyMatch(predicateDefinition -> "VersionWeight".equals(predicateDefinition.getName())); }).map(routeDefinition -> routeDefinition.getUri().getHost()).distinct().collect(Collectors.toList()); / / uninstall existing loadBalancerClientSpecification Map < String, LoadBalancerClientSpecification> loadBalancerClientSpecificationMap = ((DefaultListableBeanFactory) beanFactory).getBeansOfType(LoadBalancerClientSpecification.class); loadBalancerClientSpecificationMap.forEach((k, v) -> { if (! k.startsWith("default")) { ((DefaultListableBeanFactory) beanFactory).removeBeanDefinition(k); }}); / / register LoadBalancerClientSpecification ServiceIds. ForEach (name - > registerClientConfiguration(beanDefinitionRegistry,name)); loadBalancerClientSpecificationMap = ((DefaultListableBeanFactory) beanFactory).getBeansOfType(LoadBalancerClientSpecification.class); List<LoadBalancerClientSpecification> list = new ArrayList<>(loadBalancerClientSpecificationMap.size()); loadBalancerClientSpecificationMap.forEach((k, v) -> { if (! k.startsWith("default")) { list.add(v); }}); clientFactory.setConfigurations(list); ServiceIds.forEach(name->clientFactory.getInstance(name)); } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException { this.beanDefinitionRegistry = beanDefinitionRegistry; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { } }
  6. Customize the GrayFilter, which inherits from the OncePerRequestFilter to fetch the version in the head and put it in the ThreadLocal.

     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         String version = request.getHeader("version");
         if(StringUtil.isNotEmpty(version)){
             GrayUtil.setCurrentConextStrValue("version", version);
         }
         filterChain.doFilter(request, response);
         return;
     }

    7.

3. Problems to be solved

  • When/Refresh is called, it takes two refreshes to initialize the load rule for the newly added service. The first refresh is successful, but there are no initialized beans.
  • When a service does not need to be load-balanced, i.e. the VersionWeight assertion is removed, it will still enter the VersionWeight load method (I don’t know what to do, but I will just leave the weight evenly divided –).