文章目录
阅读提示
本文是SpringCloud系列第三篇,请先阅读前置文章。
一、Ribbon负载均衡的流程
上一节中,我们给RestTemplate添加了@LoadBalanced注解,即可以实现负载均衡功能,这是什么原理呢?其实底层是基于Ribbon组件进行负载均衡,Ribbon通过拦截客户端请求,动态地选择服务实例,从而实现了请求分发。
Ribbon 实现负载均衡的流程图如下:
用户发送一个请求http://userservice/user/1,Ribbon负载均衡器对这条url进行解析,获取到服务名称为userservice,再根据这个服务名称到eureka-server注册中心去获取userservice服务实例的ip列表,微服务模式下,一个服务有多个实例,因此Ribbon根据当前的负载均衡策略,将请求分发到其中的一个实例。
也就是说,Ribbon是一个与SpringCloud集成的组件,实现了以下功能:
1.负载均衡:Ribbon可以将客户端请求均匀地分发到多个服务实例上,以实现负载均衡。它支持多种负载均衡算法,如轮询、随机、加权轮询等。
2.故障转移与容错:当某个服务实例发生故障或不可用时,Ribbon能够自动忽略故障实例,并将请求转发给其他健康的实例,以提高系统的可用性和容错能力。
3.服务发现:Ribbon可以与服务注册中心集成,通过查询服务注册中心获取可用的服务实例列表,并根据负载均衡策略选择目标实例。
二、源码分析
2.1 LoadBalancerIntercepor
Ribbon实现负载均衡是基于LoadBalancerInterceptor这个拦截器类,我们可以在IDEA中查看它的代码:
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,
this.requestFactory.createRequest(request, body, execution));
}
}
从名称上来看LoadBalancerInterceptor翻译过来是负载均衡拦截器,这个类实现了ClientHttpRequestInterceptor客户端http拦截器这个接口,可以看到ClientHttpRequestInterceptor接口里就只有这一个intercept方法。
@FunctionalInterface
public interface ClientHttpRequestInterceptor {
ClientHttpResponse intercept(HttpRequest var1, byte[] var2, ClientHttpRequestExecution var3) throws IOException;
}
回到LoadBalancerInterceptor这个类的代码中,我们发现这里的intercept拦截器方法主要做了以下几件事:
1.获取请求的url,例如http://userserice/user/1
2.获取请求的host,其实就是服务名称,http://userserice/user/1的host就是userservice
3.判断服务名称不为空
4.调用this.loadBalancer.execute()方法继续处理处理服务名称和用户请求
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI(); //获取用户请求的url
String serviceName = originalUri.getHost(); //获取请求的host 即服务名称
Assert.state(serviceName != null, //判断服务名称不为空
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, //调用loadBalancer进一步处理
this.requestFactory.createRequest(request, body, execution));
}
2.2 LoadBalancerClient
这里的this.loadBalancer是初始化时注入的类LoadBalancerClient,译名负载均衡客户端,我们进入这个类,发现是一个接口,因此this.loadBalancer中注入的类一定是实现了LoadBalancerClient接口的具体类,从这个接口中的方法声明不难发现,该接口就是用于根据服务名称和请求来做负载均衡的。
public interface LoadBalancerClient extends ServiceInstanceChooser {
/**
* Executes request using a ServiceInstance from the LoadBalancer for the specified
* service.
* @param serviceId The service ID to look up the LoadBalancer.
* @param request Allows implementations to execute pre and post actions, such as
* incrementing metrics.
* @param <T> type of the response
* @throws IOException in case of IO issues.
* @return The result of the LoadBalancerRequest callback on the selected
* ServiceInstance.
*/
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
/**
* Executes request using a ServiceInstance from the LoadBalancer for the specified
* service.
* @param serviceId The service ID to look up the LoadBalancer.
* @param serviceInstance The service to execute the request to.
* @param request Allows implementations to execute pre and post actions, such as
* incrementing metrics.
* @param <T> type of the response
* @throws IOException in case of IO issues.
* @return The result of the LoadBalancerRequest callback on the selected
* ServiceInstance.
*/
<T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException;
/**
* Creates a proper URI with a real host and port for systems to utilize. Some systems
* use a URI with the logical service name as the host, such as
* http://myservice/path/to/service. This will replace the service name with the
* host:port from the ServiceInstance.
* @param instance service instance to reconstruct the URI
* @param original A URI with the host as a logical service name.
* @return A reconstructed URI.
*/
URI reconstructURI(ServiceInstance instance, URI original);
}
有两个类实现了该接口:
第一个是SpringCloud自带的负载均衡器,第二个是Ribbon
我们进入第二个类RibbonLoadBalancerClient,与execute()方法有关的代码如下:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
}
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonServer ribbonServer = new RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}
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);
} else {
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 var8) {
statsRecorder.recordStats(var8);
throw var8;
} catch (Exception var9) {
statsRecorder.recordStats(var9);
ReflectionUtils.rethrowRuntimeException(var9);
return null;
}
}
}
很有趣的事情,这里竟然一下子重载了三个execute方法,我们的拦截器intercept方法调用的是第一个execute方法,而这个execute方法会直接调用第二个execute方法,并多给出一个Object hint参数,这个参数看字面意思是用于后续指示选择服务实例的提示信息,也就是负载均衡的规则。
(但是我奇怪的是这里为什么要这样设计,拦截器调用execute方法时只需要传入请求名和request,而Object hint参数每次都传null值,好像没啥意义呀,这里留个悬念。)
第二个execute方法中,getLoadBalancer(serviceId)是用服务名称去eureka获取服务列表;getServer(loadBalancer, hint)则是根据负载均衡规则,去服务列表中选取一个服务实例的地址。例如127.0.0.1:8081,接下来就是调用第三个execute方法来真正完全分发请求的步骤。
2.3 负载均衡策略IRule
进入getServer(loadBalancer, hint)方法:
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
发现loadBalancer.chooseServer()这个方法传入了hint参数,即负载均衡规则。
继续跟进loadBalancer.chooseServer()方法,是一个接口,有三个实现方法,经排查是第一个,因为其他两个都返回server为空,很显然server为空的时候,第二个execute方法会抛异常,如果不放心可以前端发一条指令,全程debug模式。
进入BaseLoadBalancer的chooseServer()方法中:
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
这里是根据rule的choose方法,来选择服务实例,点击rule:
private final static IRule DEFAULT_RULE = new RoundRobinRule();
private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();
private static final String DEFAULT_NAME = "default";
private static final String PREFIX = "LoadBalancer_";
protected IRule rule = DEFAULT_RULE;
可以看到,rule的模型类型是RoundRobinRule类,进入这个类:
/**
* The most well known and basic load balancing strategy, i.e. Round Robin Rule.
*/
public class RoundRobinRule extends AbstractLoadBalancerRule {
//略
}
通过注释就可以发现,rule是负载均衡的方法,而默认的rule规则是轮询Round Robin Rule。
而轮询负载均衡的具体实现则是在RoundRobinRule类的public Server choose()方法当中,这里的key就是之前execute方法中构造的hint参数,居然完全没有使用到,那么我猜测只有一种可能性,旧版本是使用Object hint这个参数来指定负载均衡方法的,后来为了方便用户调用和修改负载均衡方法,这个参数就不用了默认传null,转而使用IRule接口,同时修改这么多代码的接口过于麻烦,就一直保留了这个参数。
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
总结:
我们总结一下Ribbon组件的负载均衡执行流程,首先负载均衡拦截器拦截请求,获取服务为名称,接着RibbonLoadBalancerClient中的execute方法会调用getLoadBalancer()根据user-service到eureka拉取服务列表;获取到服务列表后,根据负载均衡规则IRule进行请求分发,把请求发到一个实例中。
三、自定义负载均衡策略
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
不同规则的含义如下:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule(默认) | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻 |
默认的实现就是ZoneAvoidanceRule,是一种高级一点的轮询方案。
我们有两种方式自定义负载均衡规则:
1.代码方式
在order-service中的OrderApplication类中,定义一个新的IRule:
@Bean
public IRule randomRule(){
return new RandomRule();
}
这种方式定义的规则是全局的,在order-service中调用任何其他微服务时,都使用该负载均衡规则。
2.配置文件方式
在order-service的application.yml文件中,添加新的配置也可以修改规则:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
这种方式可以对不同的服务指定不同的负载均衡规则,更加灵活。
四、饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
加载的过程包括有拉取服务列表,创建负载均衡器。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,在order-service中配置开启饥饿加载:
ribbon:
eager-load:
enabled: true
clients: userservice
重新启动服务测试,可以发现第一次请求耗时更低了。