Ribbon is an open source project released by Netflix. Its main function is to provide client-side software load balancing algorithms to connect Netflix's middle-tier services together. The Ribbon client component provides a series of complete configuration items such as connection timeout, retry, etc. Simply put, it is to list all the machines behind Load Balancer (LB for short) in the configuration file. Ribbon will automatically help you connect these machines based on certain rules (such as simple polling, instant connection, etc.). We also have a very easy way to implement custom load balancing algorithms using Ribbon.
When it comes to load balancing, you usually think of load balancing on the server. Commonly used products include LBS hardware or cloud services, Nginx, etc., which are all familiar products.
Spring Cloud provides Ribbon, which allows the service caller to have load balancing capabilities. Through close integration with Eureka, there is no need to set up load balancing services in the service cluster, which greatly simplifies the architecture within the service cluster.
I don’t want to write more virtual introductions in detail, anyway, I can see the relevant introductions everywhere.
Just open the code and see how Ribbon is implemented through the code.
Configuration
Detailed explanation:
1. RibbonAutoConfiguration configuration generates a RibbonLoadBalancerClient instance.
Code location:
spring-cloud-netflix-core-1.3.5.RELEASE.jar
org.springframework.cloud.netflix.ribbon
RibbonAutoConfiguration.class
@Configuration@ConditionalOnClass({ IClient.class, RestTemplate.class, AsyncRestTemplate.class, Ribbon.class})@RibbonClients@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})@EnableConfigurationProperties(RibbonEagerLoadProperties.class)public class RibbonAutoConfiguration { // omit @Bean @ConditionalOnMissingBean(LoadBalancerClient.class) public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory()); } // omit}Let’s first look at the configuration conditional items. The RibbonAutoConfiguration configuration must be executed before the LoadBalancerAutoConfiguration configuration, because the RibbonLoadBalancerClient instance will be used in the LoadBalancerAutoConfiguration configuration.
RibbonLoadBalancerClient inherits from the LoadBalancerClient interface, is a load balancing client and the caller of the load balancing policy.
2.LoadBalancerInterceptorConfig configuration generation:
1). Load BalancerInterceptor instance
Include:
RibbonLoadBalancerClient instance of LoadBalancerClient implementation class
LoadBalancerRequestFactory: instance
2).RestTemplate Custom RestTemplateCustomizer instance
Code location:
spring-cloud-commons-1.2.4.RELEASE.jar
org.springframework.cloud.client.loadbalancer
LoadBalancerAutoConfiguration.class
@Configuration@ConditionalOnClass(RestTemplate.class)@ConditionalOnBean(LoadBalancerClient.class)@EnableConfigurationProperties(LoadBalancerRetryProperties.class)public class LoadBalancerAutoConfiguration { // omit @Bean @ConditionalOnMissingBean public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) { return new LoadBalancerRequestFactory(loadBalancerClient, transformers); } @Configuration @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { @Bean public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); } }; } } // omitted}Let’s look at the configuration conditions first:
It is required that there must be a RestTemplate class in the project environment.
It is required that there must be an instance of the implementation class of the LoadBalancerClient interface, that is, the RibbonLoadBalancerClient generated in the previous step.
3. Configure all RestTemplate instances through the RestTemplateCustomizer created in the above step, which is to set the load balancing interceptor to the RestTemplate instance.
@Configuration@ConditionalOnClass(RestTemplate.class)@ConditionalOnBean(LoadBalancerClient.class)@EnableConfigurationProperties(LoadBalancerRetryProperties.class)public class LoadBalancerAutoConfiguration { // omit @Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializer( final List<RestTemplateCustomizer> customizers) { return new SmartInitializingSingleton() { @Override public void afterSingletonsInstantiated() { for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } } } } } }; } // omit @Configuration @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { @Bean public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); } }; } } // omitted}restTemplate.setInterceptors(list) This place is the place where the load balancing interceptor is injected.
It can actually be guessed from this place that RestTemplate can build corresponding requests to achieve load balancing through injected interceptors.
It can also be seen that you can customize the interceptor to achieve other purposes.
4. RibbonClientConfiguration configuration generates ZoneAwareLoadBalancer instance
Code location:
spring-cloud-netflix-core-1.3.5.RELEASE.jar
org.springframework.cloud.netflix.ribbon
RibbonClientConfiguration.class
@SuppressWarnings("deprecation")@Configuration@EnableConfigurationProperties//Order is important here, last should be the default, first should be optional// see https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653@Import({OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})public class RibbonClientConfiguration { // omit @Bean @ConditionalOnMissingBean public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) { if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) { return this.propertiesFactory.get(ILoadBalancer.class, config, name); } return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, serverListFilter, serverListUpdater); } // omitted}ZoneAwareLoadBalancer inherits from the ILoadBalancer interface, which has a method:
/** * Choose a server from load balancer. * * @param key An object that the load balancer may use to determine which server to return. null if * the load balancer does not use this parameter. * @return server chosen */ public Server chooseServer(Object key);
ZoneAwareLoadBalancer is a specific load balancing implementation class, and it is also the default load balancing class. A certain service instance is selected by implementing the choiceServer method.
Intercept & Request
1. Use RestTemplate to perform various requests such as Get and Post, all of which are implemented through the doExecute method.
Code location:
spring-web-4.3.12.RELEASE.jar
org.springframework.web.client
RestTemplate.class
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations { // slightly protected <T> T doExecute(URI url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor<T> responseExtractor) throws RestClientException { Assert.notNull(url, "'url' must not be null"); Assert.notNull(method, "'method' must not be null"); ClientHttpResponse response = null; try { ClientHttpRequest request = createRequest(url, method); if (requestCallback != null) { requestCallback.doWithRequest(request); } response = request.execute(); handleResponse(url, method, response); if (responseExtractor != null) { return responseExtractor.extractData(response); } else { return null; } } catch (IOException ex) { String resource = url.toString(); String query = url.getRawQuery(); resource = (query != null ? resource.substring(0, resource.indexOf('?'))) : resource); throw new ResourceAccessException("I/O error on " + method.name() + " request for /"" + resource + "/": " + ex.getMessage(), ex); } finally { if (response != null) { response.close(); } } } // omitted}The various supported http request methods ultimately call the doExecute method, which calls the creation method to create the request instance, and executes the request to get the response object.
2. Generate request instance to create factory
In the previous code, the createRequest method is called to create the request instance, which is defined in the parent class.
First organize the main inheritance relationship:
The createRequest method is actually defined in the HttpAccessor abstract class.
public abstract class HttpAccessor { private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); public void setRequestFactory(ClientHttpRequestFactory requestFactory) { Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null"); this.requestFactory = requestFactory; } public ClientHttpRequestFactory getRequestFactory() { return this.requestFactory; } protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException { ClientHttpRequest request = getRequestFactory().createRequest(url, method); if (logger.isDebugEnabled()) { logger.debug("Created " + method.name() + " request for /"" + url + "/""); } return request; }}Call the getRequestFactory method in the createRequest method to get the request instance to create the factory. In fact, getRequestFactory is not defined in the current HttpAccessor class, but is defined in the subclass InterceptingHttpAccessor.
public abstract class InterceptingHttpAccessor extends HttpAccessor { private List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>(); public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) { this.interceptors = interceptors; } public List<ClientHttpRequestInterceptor> getInterceptors() { return interceptors; } @Override public ClientHttpRequestFactory getRequestFactory() { ClientHttpRequestFactory delegate = super.getRequestFactory(); if (!CollectionUtils.isEmpty(getInterceptors())) { return new InterceptingClientHttpRequestFactory(delegate, getInterceptors()); } else { return delegate; } }}I made a small action here. First, I created and obtained the SimpleClientHttpRequestFactory factory through the HttpAccessor class. This factory mainly creates basic request instances when there is no interceptor.
Secondly, when there is interceptor injection, create the InterceptingClientHttpRequestFactory factory. This factory creates a request instance with an interceptor. Because the load balancing interceptor is injected, it is created from the InterceptingClientHttpRequestFactory factory.
3. Create a request instance through the factory
When creating an instance, it depends on the factory's createRequest method.
public class InterceptingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper { private final List<ClientHttpRequestInterceptor> interceptors; public InterceptingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory, List<ClientHttpRequestInterceptor> interceptors) { super(requestFactory); this.interceptors = (interceptors != null ? interceptors : Collections.<ClientHttpRequestInterceptor>emptyList()); } @Override protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) { return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod); }}It means new InterceptingClientHttpRequest instance and inject the interceptor and basic request instance creation factory.
4. Request the instance to call the intercept method of the load balancing intercept injected in the configuration stage
It can be seen from step 1 that after creating the request instance, the request is executed by executing the execute method of the request instance.
ClientHttpRequest request = createRequest(url, method);if (requestCallback != null) { requestCallback.doWithRequest(request);} response = request.execute();The actual request instance is InterceptingClientHttpRequest, and execute is actually in its parent class.
Class definition location:
spring-web-4.3.12.RELEASE.jar
org.springframework.http.client
InterceptingClientHttpRequest.class
Take a look at their inheritance relationship.
The executeInternal method implemented by the subclass is actually called in the execute method.
public abstract class AbstractClientHttpRequest implements ClientHttpRequest { private final HttpHeaders headers = new HttpHeaders(); private boolean executed = false; @Override public final HttpHeaders getHeaders() { return (this.executed ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override public final OutputStream getBody() throws IOException { assertNotExecuted(); return getBodyInternal(this.headers); } @Override public final ClientHttpResponse execute() throws IOException { assertNotExecuted(); ClientHttpResponse result = executeInternal(this.headers); this.executed = true; return result; } protected void assertNotExecuted() { Assert.state(!this.executed, "ClientHttpRequest already executed"); } protected abstract OutputStream getBodyInternal(HttpHeaders headers) throws IOException; protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException;}In fact, it is the executeInternal method of the InterceptingClientHttpRequest class. In which, the execution of the executor InterceptingRequestExecution is called. Pass the level and determine that if an interceptor has been injected, the intercept method of the interceptor is called.
The interceptor here is actually the load balancing interceptor LoadBalancerInterceptor instance injected into the RestTemplate instance during the configuration stage. You can refer to step 2 of the configuration stage above.
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest { // omit @Override protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException { InterceptingRequestExecution requestExecution = new InterceptingRequestExecution(); return requestExecution.execute(this, bufferedOutput); } private class InterceptingRequestExecution implements ClientHttpRequestExecution { private final Iterator<ClientHttpRequestInterceptor> iterator; public InterceptingRequestExecution() { this.iterator = interceptors.iterator(); } @Override public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { if (this.iterator.hasNext()) { ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); return nextInterceptor.intercept(request, body, this); } else { ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod()); for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) { List<String> values = entry.getValue(); for (String value : values) { delegate.getHeaders().add(entry.getKey(), value); } } if (body.length > 0) { StreamUtils.copy(body, delegate.getBody()); } return delegate.execute(); } } }}5. The load balancing interceptor calls the load balancing client
In the intercept method of the load balancing interceptor class LoadBalancerInterceptor class, the execute method of the load balancing client LoadBalancerClient implementation class is called.
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; private LoadBalancerRequestFactory requestFactory; public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) { this.loadBalancer = loadBalancer; this.requestFactory = requestFactory; } public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) { // for backwards compatibility this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer)); } @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); return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution)); }}In step 1 of the configuration phase, you can see that the implementation class is RibbonLoadBalancerClient.
6. The load balancing client calls the load balancing policy to select the target service instance and initiate a request
In the first execute method and getServer method of RibbonLoadBalancerClient, we can see that in fact, the choiceServer method of the ILoadBalancer implementation class is selected and handed over to the next request object to initiate a request.
The load balancing implementation class here is the ZoneAwareLoadBalancer area-aware load balancer instance by default, and it selects a service internally through the balancing policy.
The creation of ZoneAwareLoadBalancer can be found in step 4 of the configuration phase.
public class RibbonLoadBalancerClient implements LoadBalancerClient { @Override public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { ILoadBalancer loadBalancer = getLoadBalancer(serviceId); Server server = getServer(loadBalancer); if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); return execute(serviceId, ribbonServer, request); } @Override 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 { 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; } // slightly protected Server getServer(ILoadBalancer loadBalancer) { if (loadBalancer == null) { return null; } return loadBalancer.chooseServer("default"); // TODO: better handling of key } protected ILoadBalancer getLoadBalancer(String serviceId) { return this.clientFactory.getLoadBalancer(serviceId); } public static class RibbonServer implements ServiceInstance { private final String serviceId; private final Server server; private final boolean secure; private Map<String, String> metadata; public RibbonServer(String serviceId, Server server) { this(serviceId, server, false, Collections.<String, String> emptyMap()); } public RibbonServer(String serviceId, Server server, boolean secure, Map<String, String> metadata) { this.serviceId = serviceId; this.server = server; this.secure = secure; this.metadata = metadata; } // omitted}}After finishing the code, let’s summarize it.
When using RestTemplate to request other services, the regular http request instance is used internally to send requests.
After adding the @LoanBalanced annotation to RestTemplate, actually, through configuration, a load balancing interceptor is injected into RestTemplate, allowing the load balancer to choose the appropriate service according to its corresponding policy before sending a request.
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.