This is my 28th day of the August Genwen Challenge
This series code address: github.com/HashZhang/s…
We used Spring Cloud LoadBalancer as our client LoadBalancer, which is officially recommended by Spring Cloud. In the previous section, we looked at the structure of Spring Cloud LoadBalancer. Now let’s talk about what we want to do with Spring Cloud LoadBalancer:
- We are going to implementDifferent clusters do not call each other through instances
metamap
In thezone
configurationTo distinguish between instances of different clusters. instance-onlymetamap
In thezone
Instances that are configured the same can call each other. This is custom by implementationServiceInstanceListSupplier
Can be realized - Load balancing polling algorithm that requires isolation between requests, cannot share the same position, so the retry after a failed request is the same as the original failed instance. Default as you saw in the previous section
RoundRobinLoadBalancer
Is that all threads share the same atomic variableposition
Atomic increments 1 per request. There is A problem in this case: Suppose you have microservice A with two instances: instance 1 and instance 2. When request A arrives,RoundRobinLoadBalancer
Return instance 1, request B arrives,RoundRobinLoadBalancer
Return instance 2. Then if request A fails and retry,RoundRobinLoadBalancer
Instance 1 is returned. This is not what we expected to see.
We wrote our own implementations for each of these functions.
Zone configuration in Spring Cloud LoadBalancer
Spring Cloud LoadBalancer defines LoadBalancerZoneConfig:
Public class LoadBalancerZoneConfig {// Identifies which zone the load balancer is in. public LoadBalancerZoneConfig(String zone) { this.zone = zone; } public String getZone() { return zone; } public void setZone(String zone) { this.zone = zone; }}Copy the code
If does not rely on introduction of a Eureka, this zone by spring. Cloud. Loadbalancer. The zone configuration: LoadBalancerAutoConfiguration
@Bean
@ConditionalOnMissingBean
public LoadBalancerZoneConfig zoneConfig(Environment environment) {
return new LoadBalancerZoneConfig(environment.getProperty("spring.cloud.loadbalancer.zone"));
}
Copy the code
If Eureka dependencies are introduced, then if a zone is configured in the Eureka metadata, then this zone overrides the LoadBalancerZoneConfig in Spring Cloud LoadBalancer:
EurekaLoadBalancerClientConfiguration
@PostConstruct public void postprocess() { if (! StringUtils.isEmpty(zoneConfig.getZone())) { return; } String zone = getZoneFromEureka(); if (! StringUtils.isEmpty(zone)) { if (LOG.isDebugEnabled()) { LOG.debug("Setting the value of '" + LOADBALANCER_ZONE + "' to " + zone); } // set 'LoadBalancerZoneConfig' zoneConfig. SetZone (zone); } } private String getZoneFromEureka() { String zone; / / is configured with a spring. Cloud. Loadbalancer. Eureka. ApproximateZoneFromHostname to true Boolean approximateZoneFromHostname = eurekaLoadBalancerProperties.isApproximateZoneFromHostname(); // If so, try to extract the host name from the Eureka configuration. Split the host, and then the second is the zone / / such as www.zone1.com is zone1 if (approximateZoneFromHostname && eurekaConfig! = null) { return ZoneUtils.extractApproximateZone(this.eurekaConfig.getHostName(false)); } else {// Otherwise, get the key zone from the metadata map zone = eurekaConfig == null? null : eurekaConfig.getMetadataMap().get("zone"); If (stringutils.isempty (zone) && clientConfig!) if (stringutils.isempty (zone) && clientConfig! = null) { String[] zones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); // Pick the first one from the regions we want to connect to zone = zones ! = null && zones.length > 0 ? zones[0] : null; } return zone; }}Copy the code
Implement SameZoneOnlyServiceInstanceListSupplier
To filter instances in the same zone by zone and never return instances in a different zone, we write code:
SameZoneOnlyServiceInstanceListSupplier
/** * return only service instances in the same Zone as the current instance. Service does not call each other between different zones * / public class SameZoneOnlyServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier Private final String zone = "zone"; private final String zone = "zone"; /** * Loadbalancer zone configuration */ private final LoadBalancerZoneConfig zoneConfig; private String zone; public SameZoneOnlyServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, LoadBalancerZoneConfig zoneConfig) { super(delegate); this.zoneConfig = zoneConfig; } @Override public Flux<List<ServiceInstance>> get() { return getDelegate().get().map(this::filteredByZone); } // Filter private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {if (zone == null) { zone = zoneConfig.getZone(); } if (zone ! = null) { List<ServiceInstance> filteredInstances = new ArrayList<>(); for (ServiceInstance serviceInstance : serviceInstances) { String instanceZone = getZone(serviceInstance); if (zone.equalsIgnoreCase(instanceZone)) { filteredInstances.add(serviceInstance); } } if (filteredInstances.size() > 0) { return filteredInstances; }} / * * * @ see ZonePreferenceServiceInstanceListSupplier when no instances of the same zone returns all instances * we don't call each other between different zones here in order to realize the need to return an empty list * / return List.of(); } // Read the zone of the instance, Null if no private String getZone(ServiceInstance ServiceInstance) {Map<String, String> metadata = serviceInstance.getMetadata(); if (metadata ! = null) { return metadata.get(ZONE); } return null; }}Copy the code
In the previous chapter, we mentioned that we used Spring-Cloud-sleUTH as a link tracing library. We think we can tell if it’s the same request by traceId.
RoundRobinWithRequestSeparatedPositionLoadBalancer
/ / must be must be implemented ReactorServiceInstanceLoadBalancer / / not ReactorLoadBalancer < ServiceInstance > / / because when registration is ReactorServiceInstanceLoadBalancer @ Log4j2 public class RoundRobinWithRequestSeparatedPositionLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final ServiceInstanceListSupplier serviceInstanceListSupplier; // Each request does not exceed 1 minute with retries // For requests that exceed 1 minute, the request must be heavy, Private final LoadingCache<Long, AtomicInteger> positionCache = Caffeine.newBuilder().expireAfterWrite(1) TimeUnit. MINUTES) / / random initial value, prevent begins with the first call. Every time the build (k - > new AtomicInteger (ThreadLocalRandom. The current () nextInt (0, 1000))); private final String serviceId; private final Tracer tracer; public RoundRobinWithRequestSeparatedPositionLoadBalancer(ServiceInstanceListSupplier serviceInstanceListSupplier, String serviceId, Tracer tracer) { this.serviceInstanceListSupplier = serviceInstanceListSupplier; this.serviceId = serviceId; this.tracer = tracer; } @Override public Mono<Response<ServiceInstance>> choose(Request request) { return serviceInstanceListSupplier.get().next().map(serviceInstances -> getInstanceResponse(serviceInstances)); } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) { if (serviceInstances.isEmpty()) { log.warn("No servers available for service: " + this.serviceId); return new EmptyResponse(); } return getInstanceResponseByRoundRobin(serviceInstances); } private Response<ServiceInstance> getInstanceResponseByRoundRobin(List<ServiceInstance> serviceInstances) { if (serviceInstances.isEmpty()) { log.warn("No servers available for service: " + this.serviceId); return new EmptyResponse(); } // In order to resolve the original algorithm different calls concurrent may cause a request to retry the same instance Span currentSpan = tracer.currentSPAN (); if (currentSpan == null) { currentSpan = tracer.newTrace(); } long l = currentSpan.context().traceId(); AtomicInteger seed = positionCache.get(l); int s = seed.getAndIncrement(); int pos = s % serviceInstances.size(); log.info("position {}, seed: {}, instances count: {}", pos, s, serviceInstances.size()); Return new DefaultResponse(serviceInstance.stream () // First take again for sorting sorted (Comparator.com paring (ServiceInstance: : getInstanceId)). Collect (Collectors. ToList ()). The get (pos)); }}Copy the code
In the previous section, we mentioned that the default load balancer configuration can be configured using the @loadBalancerClients annotation, which is how we do it here. Add auto-configuration classes to spring.factories first:
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.hashjang.spring.cloud.iiford.service.common.auto.LoadBalancerAutoConfiguration
Copy the code
Then write the autoconfiguration class, which is as simple as adding a @loadBalancerClients annotation and setting the default configuration class:
LoadBalancerAutoConfiguration
@Configuration(proxyBeanMethods = false)
@LoadBalancerClients(defaultConfiguration = DefaultLoadBalancerConfiguration.class)
public class LoadBalancerAutoConfiguration {
}
Copy the code
Write this default configuration class and assemble the two classes we implemented above:
DefaultLoadBalancerConfiguration
@Configuration(proxyBeanMethods = false) public class DefaultLoadBalancerConfiguration { @Bean public ServiceInstanceListSupplier serviceInstanceListSupplier( DiscoveryClient discoveryClient, Environment env, ConfigurableApplicationContext context, LoadBalancerZoneConfig zoneConfig ) { ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context .getBeanProvider(LoadBalancerCacheManager.class); Return / / open service instance cache new CachingServiceInstanceListSupplier (/ new/can only return to the same zone service instance SameZoneOnlyServiceInstanceListSupplier (/ / enabled through discoveryClient service discovery new DiscoveryClientServiceInstanceListSupplier ( discoveryClient, env ), zoneConfig ) , cacheManagerProvider.getIfAvailable() ); } @Bean public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer( Environment environment, ServiceInstanceListSupplier serviceInstanceListSupplier, Tracer tracer ) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new RoundRobinWithRequestSeparatedPositionLoadBalancer( serviceInstanceListSupplier, name, tracer ); }}Copy the code
In this way, we have implemented a custom load balancer. You also understand the use of Spring Cloud LoadBalancer.
This section examines in detail what Spring Cloud LoadBalancer does in our project, implementing a custom LoadBalancer, and understanding the use of Spring Cloud LoadBalancer. In the next section, we use unit tests to verify that the functionality we are implementing works.
Wechat search “my programming meow” public account, a daily brush, easy to improve skills, won a variety of offers