SpringCloud客户端负载均衡Ribbon

前言

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。

这篇文章我们来看下如何使用Ribbon来实现客户端负载均衡,以及Ribbon实现负载均衡的原理。

正文

客户端负载均衡

负载均衡在系统架构中非常重要,是系统实现高可用、网络压力缓解、处理能力扩容的重要手段之一。一般情况下,我们所说的负载均衡大都指的服务端的负载均衡,通常分为硬件负载均衡和软件负载均衡。

硬件负载均衡,主要通过在服务器节点之间安装专门用于负载均衡的设备来实现,如F5。

软件负载均衡,主要通过在服务器上安装一些具有负载均衡的模块或者软件来实现,如Nginx。

负载均衡设备或者软件都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务节点以保证清单中都是可以正常访问的服务节点。客户端发送请求到负载均衡设备,设备按照某种算法(轮询、权重等)从清单里取出一台服务地址,进行转发。

客户端负载均衡和上面说的服务端负载均衡最大的不同点在于上面提到的服务清单所存储的位置。在客户端负载均衡中,所有客户端都需要维护自己要访问的服务器清单,这些服务端清单来自注册中心。当然,客户端负载均衡也要通过心跳检测服务的健康性,这个步骤需要注册中心配合完成。

RestTemplate

在之前Eureka的例子中,我们引入了Ribbon实现负载均衡功能,同时知道了通过给RestTemplate对象配置@LoadBalanced注解便可以开启客户端负载均衡。

在了解Ribbon之前,我们先聊聊RestTemplate

我们可以看到RestTemplate是属于spring web模块的一个类,顾名思义,它是spring用来发送REST请求的封装模板。

对于GET请求,可以看到主要有两种类型的函数方法。

第一种,getForEntity函数。返回ResponseEntity对象。它有三个方法:

1
2
3
4
5
6
//1.
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {/**/}
//2.
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {/**/}
//3.
public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType) throws RestClientException {/**/}

对于第一个方法,其uriVariables里为GET请求的参数,通过url占位符的方式使用,如下:

1
2
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://SAKURA-SERVICE/hello?name={1}",String.class,"aaa");
String body = responseEntity.getBody();

其中访问SAKURA-SERVICE服务的hello接口时,”aaa”会替换掉{1}。

如果返回对象是个User,那么如下实现:

1
2
ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://SAKURA-SERVICE/hello?name={1}",User.class,"aaa");
User body = responseEntity.getBody();

第2,3个方法,代码如下:

1
2
3
4
5
6
7
8
9
//2.我们使用name作为占位符,则map里需要put一个key为name的参数
Map<String,String> map =new HashMap<>();
map.put("name","aaa");
ResponseEntity<String> responseEntity1 = restTemplate.getForEntity("http://SAKURA-SERVICE/hello?name={name}",String.class,map);

//3.构建uri
UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://SAKURA-SERVICE/hello?name={name}").build().expand("aaa").encode();
URI uri = uriComponents.toUri();
ResponseEntity<String> responseEntity2 = restTemplate.getForEntity(uri,String.class);

第二种,getForObject函数。该方法是对getForEntity的进一步封装。

我们使用时十分简单,如下:

1
2
3
String str = restTemplate.getForObject(uri,String.class);

User user = restTemplate.getForObject(uri1,User.class);

它也有3个重载方法:

1
2
3
4
5
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {}

public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {}

public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException {}

对于POST请求,与GET类似,它也有postForEntitypostForObject函数。

它们的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Object... uriVariables) throws RestClientException {}

public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Map<String, ?> uriVariables) throws RestClientException {}

public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
throws RestClientException {}

public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,
Class<T> responseType, Object... uriVariables) throws RestClientException {}

public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,
Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {}

public <T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType)
throws RestClientException {}

它们的使用与GET类似,我们就不在过多介绍。

1
2
3
User user = new User("aaaa",18);
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://SAKURA-SERVICE/hello",user,String.class);
String body = responseEntity.getBody();

另外POST里还有一种postForLocation函数,用来提交资源并返回新资源URI,它也有三种重载方法。

1
2
User user = new User("aaaa",18);
URI uri = restTemplate.postForLocation("http://SAKURA-SERVICE/hello",user);
1
2
3
4
5
6
7
public URI postForLocation(String url, @Nullable Object request, Object... uriVariables)
throws RestClientException {}

public URI postForLocation(String url, @Nullable Object request, Map<String, ?> uriVariables)
throws RestClientException {}

public URI postForLocation(URI url, @Nullable Object request) throws RestClientException {}

RestTemplate里的其他方法,如PUT、DELETE等我们不再介绍,有兴趣的可以看看源码。

Ribbon源码

分析完RestTemplate后,我们会想,RestTemplate本是Spring本是的东西,如何通过Ribbon实现负载均衡的呢?

因为我们之前的例子讲到当RestTemplate作用上@LoadBalanced注解后,便可以实现负载均衡了,因此我们从@LoadBalanced注解开始看起吧。

upload successful

通过@LoadBalanced注释可以看到,该注解用来给RestTemplate做标记,以使用负载均衡的客户端LoadBalancerClient来配置它。

我们搜索LoadBalancerClient可以发现这是Spring Cloud定义的一个接口。如下:

upload successful

upload successful

通过该接口,可以大致了解负载均衡客户端应具备的几种能力。

  1. choose方法:根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。

    1
    ServiceInstance choose(String serviceId)
  2. execute方法:使用从负载均衡器中挑选出来的服务实例来执行请求内容。

    1
    2
    3
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

    <T> T execute(String serviceId, ServiceInstance serviceInstance,LoadBalancerRequest<T> request) throws IOException;
  3. reconstructURI方法:为系统构建一个合适的host:port形式的URI。

    1
    URI reconstructURI(ServiceInstance instance, URI original);

我们再来看一下LoadBalancerAutoConfiguration这个类,这个类是实现客户端负载均衡的自动化配置类。

upload successful

根据上图,我们可以知道,Ribbon实现负载均衡自动化配置需要满足下面两个条件:

  • @ConditionalOnClass(RestTemplate.class):当前工程环境中需要存在RestTemplate类。
  • @ConditionalOnBean(LoadBalancerClient.class):需要存在LoadBalancerClient接口的实现Bean。

该自动化配置类主要完成以下几个功能:

  • 创建一个LoadBalancerInterceptor的 Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
  • 创建一个RetryLoadBalancerInterceptor的Bean,用于实现客户端负载均衡的重试机制。
  • 创建一个RestTemplateCustomizer的 Bean,用于给RestTemplate增加LoadBalancerInterceptorRetryLoadBalancerInterceptor拦截器。
  • 维护一个被@LoadBalanced修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptorRetryLoadBalancerInterceptor拦截器。

LoadBalancerInterceptor相关代码如下:

upload successful

通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient的实现。当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept所拦截。由于我们在使用RestTemplate时采用了服务名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的请求。

我们上面讲到了LoadBalancerClient,它只是一个接口,我们现在来看下它的实现类。我们很容易就可以找到它的实现类RibbonLoadBalancerClient

这个类我们主要看下它的execute方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
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;
}

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");
}

可以看到,在execute函数的实现中,第一步就是通过getServer根据传入的服务名serviceId去获取具体的服务实例。

同时可以看到getServer函数的实现,调用的是ILoadBalancer接口中定义的chooseServer函数。

对于ILoadBalancer接口,该接口定义了一个客户端负载均衡需要的一系列抽象操作。

1
2
3
4
5
6
7
8
9
10
11
public interface ILoadBalancer {
public void addServers(List<Server> newServers);

public Server chooseServer(Object key);

public void markServerDown(Server server);

public List<Server> getReachableServers();

public List<Server> getAllServers();
}
  • addServers:向负载均衡器中维护的实例列表增加服务实例。
  • chooseServer:通过某种策略,从负载均衡器中挑选出一个具体的服务实例。
  • markServerDown:用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前会认为服务实例正常,实际访问时出现问题。
  • getReachableServers:获取当前正常服务的实例列表。
  • getAllServers:获取所有已知服务实例列表,包括正常服务和停止服务的实例。

该接口的实现就是对负载均衡策略的一些扩展,这部分我们下节在详细讨论。

RibbonClientConfiguration配置类中,我们可以看到如下代码。

1
2
3
4
5
6
7
8
9
10
11
@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);
}

可以看到Ribbon默认使用ZoneAwareLoadBalancer来实现负载均衡器。

我们在回到RibbonLoadBalancerClient的代码逻辑,当通过ZoneAwareLoadBalancerchooseServer函数获取了负载均衡策略分配到的服务实例Server后,将其内容包装成RibbonServer对象,然后使用该对象回调LoadBalancerInterceptor请求拦截器中LoadBalancerRequestapply(ServiceInstance instance)函数,向一个具体服务实例发起请求。

apply(ServiceInstance instance)函数中传入的ServiceInstance接口对象是对服务实例的抽象定义。其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface ServiceInstance {
default String getInstanceId() {
return null;
}

String getServiceId();

String getHost();

int getPort();

boolean isSecure();

URI getUri();

Map<String, String> getMetadata();

default String getScheme() {
return null;
}
}

上面的RibbonServer对象就是该接口对的一个实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.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;
}
//.....部分代码略
}

总结

分析到这里,我们大致理清Spring Cloud Ribbon中实现负载均衡的基本脉络,了解了它的一些源码。后面我们会再来看下Ribbon的负载均衡器及负载均衡策略的一些东西。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道