Analysis of Ribbon principle and Nacos service discovery principle

Posted by amity on Thu, 10 Feb 2022 23:21:31 +0100

Ribbon principle analysis

Ribbon uses interceptors to realize remote invocation of services. The source code is as follows:

public class LoadBalancerAutoConfiguration {

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

	@Autowired(required = false)
	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

	@Bean
	// Loop all enhancement classes to enhance restTemplate, and add interceptors to it here
	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
			for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
				for (RestTemplateCustomizer customizer : customizers) {
					customizer.customize(restTemplate);
				}
			}
		});
	}

	@Bean
	@ConditionalOnMissingBean
	public LoadBalancerRequestFactory loadBalancerRequestFactory(
			LoadBalancerClient loadBalancerClient) {
		return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	static class LoadBalancerInterceptorConfig {

		@Bean
		// Register the load balancing interceptor. LoadBalancerClient is the function class that is executing the service balancing
		public LoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient,
				LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}

		@Bean
		@ConditionalOnMissingBean
		// Register the enhanced class of RestTemplate
		public RestTemplateCustomizer restTemplateCustomizer(
				final LoadBalancerInterceptor loadBalancerInterceptor) {
			return restTemplate -> {
				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
						restTemplate.getInterceptors());
				// Add interceptor
				list.add(loadBalancerInterceptor);
				restTemplate.setInterceptors(list);
			};
		}

	}
  //  Slightly

After entering the intercept of loadbalancerinceptor, you can see that it uses LoadBalancerClient to execute the real process. LoadBalancerClient is responsible for calling the process, and the specific execution is executed by ILoadBalancer load balancer

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null,
				"Request URI does not contain a valid hostname: " + originalUri);
		// Perform load balancing
		return this.loadBalancer.execute(serviceName,
				this.requestFactory.createRequest(request, body, execution));
	}

Then enter the execute method of RibbonLoadBalancerClient to view.

	public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException {
		// 1. Get load balancer from container
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		// 2. Use ILoadBalancer to get a service
		Server server = getServer(loadBalancer, hint);
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		// Assemble information into a service instance
		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
				isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
		// 3. Call this service
		return execute(serviceId, ribbonServer, request);
	}

Service discovery and load balancing take place in these three steps. We focus on what these three steps do

1,getLoadBalancer

Obtain the load balancer from the container, and register the zonewareloadbalancer with the container in the RibbonClientConfiguration, so it is actually obtained.

Then look at the construction process of a ZoneAwareLoadBalancer and what it has done. Click in to find the DynamicServerListLoadBalancer construction method of its parent class. The source code is as follows

 public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                         ServerList<T> serverList, ServerListFilter<T> filter,
                                         ServerListUpdater serverListUpdater) {
        // Call the superior structure to initialize some configuration information
        super(clientConfig, rule, ping);
        // serverList service list, which is used to discover the service
        this.serverListImpl = serverList;
        // Service filters
        this.filter = filter;
        // Updater for service list
        this.serverListUpdater = serverListUpdater;
        if (filter instanceof AbstractServerListFilter) {
            ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
        }
        // Initialize, pull service and start service update
        restOfInit(clientConfig);
    }

restOfInit method source code

    void restOfInit(IClientConfig clientConfig) {
        boolean primeConnection = this.isEnablePrimingConnections();
        // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
        this.setEnablePrimingConnections(false);
        // Enable listening of service list
        enableAndInitLearnNewServersFeature();
		// Pull service
        updateListOfServers();
        if (primeConnection && this.getPrimeConnections() != null) {
            this.getPrimeConnections()
                    .primeConnections(getReachableServers());
        }
        this.setEnablePrimingConnections(primeConnection);
        LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
    }

Enableandinitrelearnnewserversfeature starts a scheduled task pull service, which calls updateListOfServers. It mainly depends on how updateListOfServers pull the service. The method source code is as follows:

    public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
        	// Call the service list service to pull and update the service
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);

            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
        // Update service list information of memory
        updateAllServerList(servers);
    }

serverListImpl is a standard interface for load balancing, which is used to obtain services. Nacos also implements this interface to obtain the service list from Ribbon

public interface ServerList<T extends Server> {

    public List<T> getInitialListOfServers();
    
    /**
     * Return updated list of servers. This is called say every 30 secs
     * (configurable) by the Loadbalancer's Ping cycle
     * 
     */
    public List<T> getUpdatedListOfServers();   

}

Part of the source code of nacoserverlist:

	@Override
	public List<NacosServer> getUpdatedListOfServers() {
		return getServers();
	}

	private List<NacosServer> getServers() {
		try {
			String group = discoveryProperties.getGroup();
			// Use Nacos's NameingService to initiate Api calls to obtain the list of service instances
			List<Instance> instances = discoveryProperties.namingServiceInstance()
					.selectInstances(serviceId, group, true);
			return instancesToServerList(instances);
		}
		catch (Exception e) {
			throw new IllegalStateException(
					"Can not get service instances from nacos, serviceId=" + serviceId,
					e);
		}
	}

Then you will know how the service discovery is. The rest is to select a service to invoke according to the load policy. Then look at the next step, getServer

2,getServer

Go back to the second step of RibbonLoadBalancerClient getServer. This step is to use the load balancer to select a service:

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		if (loadBalancer == null) {
			return null;
		}
		// Use 'default' on a null hint, or just pass it on?
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}

Because the zone will not be used by default, it will directly enter the chooseServer method of BaseLoadBalancer. The source code is as follows:

    public Server chooseServer(Object key) {
    	// Create counter or + 1 operation
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
            	// Select a service using load policy
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

In this step, I use the load policy to select a service. As for the Rule policy, I will analyze it in the next step, and then look at the next step

3,execute

This step is to call the service

	public <T> T execute(String serviceId, ServiceInstance serviceInstance,
			LoadBalancerRequest<T> request) throws IOException {
		Server server = null;
		if (serviceInstance instanceof RibbonServer) {
			server = ((RibbonServer) serviceInstance).getServer();
		}
		if (server == null) {
			throw new IllegalStateException("No instances available for " + serviceId);
		}

		RibbonLoadBalancerContext context = this.clientFactory
				.getLoadBalancerContext(serviceId);
		RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

		try {
			// Send assembly return results
			T returnVal = request.apply(serviceInstance);
			statsRecorder.recordStats(returnVal);
			return returnVal;
		}
		// catch IOException and rethrow so RestTemplate behaves correctly
		catch (IOException ex) {
			statsRecorder.recordStats(ex);
			throw ex;
		}
		catch (Exception ex) {
			statsRecorder.recordStats(ex);
			ReflectionUtils.rethrowRuntimeException(ex);
		}
		return null;
	}

Ribbon's load and Nacos's service discovery process are basically analyzed!!

Topics: Spring Spring Cloud