本文從源碼視角簡述Ribbon如何爲客戶端提供負載均衡能力。
一. 入口
在《SpringCloud負載均衡組件Ribbon相關實踐》中我們提到,只需要爲RestTemplate增加@LoadBalanced註解,就可以爲RestTemplate整合Ribbon,使其具備負載均衡的能力。那麼,@LoadBalanced是如何爲RestTemplate提供這種能力的呢?
下面我們先從這個註解開始說起:
/**
* Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
* @author Spencer Gibb
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
這個類並沒有什麼特別有用的線索,通過在IDE工具上查看該註解的引用情況,發現如下兩個類會引用該註解:
由此可以猜想,應該是通過這兩個類,實現了Ribbon的功能。
除此之外,我們也可以查看所在項目的META-INF目錄下的spring.factories文件,如下所示:
# AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.client.CommonsClientAutoConfiguration,\
org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\
org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguration,\
org.springframework.cloud.commons.util.UtilAutoConfiguration,\
org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration,\
org.springframework.cloud.commons.httpclient.HttpClientConfiguration,\
org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration
同樣可以看出上面提到的兩個類AsyncLoadBalancerAutoConfiguration,LoadBalancerAutoConfiguration。由於AutoConfiguration引入的類都會在項目啓動時被添加到Spring容器中,因此也驗證了我們的猜想。
二. 跟蹤分析
下面我們從LoadBalancerAutoConfiguration上開始分析:
1. LoadBalancerAutoConfiguration
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
//1.通過@LoadBalanced註解中的@Qualifier註解,自動裝載含有@LoadBalanced註解的RestTemplate
// 添加到restTemplates中
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
//2.創建LoadBalancerRequestFactory工廠
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
//3.創建LoadBalancerInterceptor攔截器,同時注入LoadBalancerRequestFactory工廠
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
//4.爲前面的restTemplates注入LoadBalancerInterceptor攔截器
@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);
}
};
}
}
}
通過對這個類的分析,可以看出其主要作用是爲加了@LoadBalanced註解的RestTemplate注入LoadBalancerInterceptor攔截器。下面分析LoadBalancerInterceptor這個攔截器。分析攔截器之前,我們需要先分析RestTemplate。一般我們通過
restTemplate.getForObject("http://zz-provider-user/user/" + id, User.class)遠程調用。下面分析getForObject方法。
2. RestTemplate.getForObject()
@Override
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
HttpMessageConverterExtractor<T> responseExtractor =
new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
}
@Override
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
URI expanded = getUriTemplateHandler().expand(url, uriVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
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 {
//1.創建請求
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
//2.執行請求
response = request.execute();
//3.處理請求
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();
}
}
}
可以看出,restTemplate發送請求的三個步驟爲:創建請求,執行請求,處理請求。因爲我們知道,Ribbon實現了執行請求時的負載均衡,因此我們重點分析第二步,執行請求:也就是 request.execute():
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
// 1.定義攔截器
private final Iterator<ClientHttpRequestInterceptor> iterator;
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
// 2.依次執行攔截器
return nextInterceptor.intercept(request, body, this);
}
------中間內容省略------
return delegate.execute();
}
}
我們可以看出,在execute方法中,會先執行配置的攔截器,再執行原方法,因此可以得出結論:前面所定義的LoadBalancerInterceptor攔截器,正是在這裏被注入。下面分析LoadBalancerInterceptor裏面的主要工作:
3. LoadBalancerInterceptor
@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));
}
主要會調用RibbonLoadBalancerClient.execute方法,下面繼續分析。
4. RibbonLoadBalancerClient.execute()
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//1.獲取具體的LoadBalanced
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
//2.獲取要調用的服務
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));
//3.執行
return execute(serviceId, ribbonServer, request);
}
主要分析第二步,因爲第二步獲取服務的時候肯定需要採用負載均衡的手段來實現(不然這個註解就沒用了)。下面分析getServer這個方法:
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
可以看到,通過ZoneAwareLoadBalancer的chooseServer方法實現負載均衡(爲什麼是ZoneAwareLoadBalancer呢?因爲這個類是默認實現),於是繼續分析這個方法:
public Server chooseServer(Object key) {
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
上面的代碼有點複雜,注意到判斷條件:getLoadBalancerStats().getAvailableZones().size() <= 1。於是代碼可以這樣子理解:假設可用節點只有一個,那就直接選這一個,否則走一堆很複雜的邏輯,來選擇出其中一個可用的節點。那麼再繼續分析chooseServer方法:
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
//1.通過rule去選擇
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
@Override
public Server choose(Object key) {
//2.獲取用戶配置的ILoadBalancer(這個可以自定義配置)
ILoadBalancer lb = getLoadBalancer();
//3.根據前面獲取的ILoadBalancer,從所有服務中獲取一個可用的服務節點
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
最終,我們看到調用了chooseRoundRobinAfterFiltering方法,它的主要作用是根據我們配置的rule,從所有server中獲取一個可用的server,最終實現RestTemplate的負載均衡。其中第2步的自定義配置,可以參考《SpringCloud負載均衡組件Ribbon相關實踐》。
三. 總結
最後的最後,我們做個小總結,Ribbon的實現主要內容爲:
1. 用戶創建了RestTemplate,並配置了@LoadBalanced註解;
2. 項目啓動時,Ribbon通過LoadBalancerAutoConfiguration類,爲加了@LoadBalanced註解的RestTemplate注入LoadBalancerInterceptor攔截器;
3. 當用戶使用RestTemplate請求時,主要有 創建請求,執行請求,處理請求 三個步驟,其中Ribbon作用於第二步;
(1) Ribbon在RestTemplate的第二步執行請求時,會先執行配置的LoadBalancerInterceptor攔截器,再執行原方法;
(2) 當請求在LoadBalancerInterceptor攔截器中時,會根據用戶自定義配置的規則,從所有服務中獲取一個可用的服務,最終實現RestTemplate的負載均衡。