前面讲的负载均衡技术实际上都是服务端负载均衡,一个请求需要被发送到哪台服务器做出响应,是由我们的服务器决定的。而在SpringCloud中,采用的缺是客户端负载均衡技术。那么客户端负载均衡的优势到底在哪里?以及客户端负载均衡跟服务端负载均衡比较的优势在哪里?接下来我为你娓娓道来。
一、客户端负载均衡
服务器端负载均衡:例如Nginx,通过Nginx进行负载均衡,先发送请求,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。例如在Dubbo中, zookeeper即注册中心帮我们实现调度和负载均衡的能力。
客户端负载均衡:例如spring cloud中的Ribbon,客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配。
从上面的描述我们可以看出,客户端负载均衡和服务端负载均衡最大的区别在于服务清单所存储的位置。在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端清单,这些清单统统都是从Eureka服务注册中心获取的。在Spring Cloud中我们如果想要使用客户端负载均衡,方法很简单,开启@LoadBalanced注解即可,这样客户端在发起请求的时候会先自行选择一个服务端,向该服务端发起请求,从而实现负载均衡。
二、Ribbon负载均衡介绍
Eureka为所有Netflix服务提供服务注册。Ribbon客户端的创建和配置为每个目标服务执行。Ribbon客户端组件提供一系列完善的配置选项,比如连接超时、重试、重试算法等。Ribbon内置可插拔、可定制的负载均衡组件。下面是用到的一些负载均衡策略:
- 简单轮询负载均衡
- 加权响应时间负载均衡
- 区域感知轮询负载均衡
- 随机负载均衡
负载均衡的核心:
- 服务发现,发现依赖服务的列表
- 服务选择规则,在多个服务中如何选择一个有效服务
- 服务监听,检测失效的服务,高效剔除失效服务
三、Ribbon客户端负载均衡实现原理
Ribbon是一个基于HTTP和TCP的客户端负载均衡器,当我们将Ribbon和Eureka一起使用时,Ribbon会从Eureka注册中心去获取服务端列表,然后进行轮询访问以到达负载均衡的作用,服务端是否在线这些问题则交由Eureka去维护。那么这个时候发现这两者的机制差不多,都是通过心跳维护有效服务清单列表,不同之处在于清单列表的存储位置。在Spring Cloud中我们如果想要使用客户端负载均衡,方法很简单,开启@LoadBalanced注解即可,这样客户端在发起请求的时候会先自行选择一个服务端,向该服务端发起请求,从而实现负载均衡。
接下来看一下@LoadBalanced,这个注解是用来给RestTemplate做标记,以使用LoadBalancerClient来配置它。而LoadBalancerClient是一个接口:
public interface ServiceInstanceChooser {
ServiceInstance choose(String serviceId);
}
public interface LoadBalancerClient extends ServiceInstanceChooser {
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
URI reconstructURI(ServiceInstance instance, URI original);
}
这几个方法的作用:
- ServiceInstance choose(String serviceId)根据传入的服务名serviceId从客户端负载均衡器中挑选一个对应服务的实例。
- T execute() ,使用从负载均衡器中挑选出来的服务实例来执行请求。
- URI reconstructURI(ServiceInstance instance, URI original)表示为系统构建一个合适的URI,服务的逻辑名称(http://HELLO-SERVICE/hello)不是具体的服务地址,在reconstructURI方法中,第一个参数ServiceInstance实例是一个带有host和port的具体服务实例,第二个参数URI则是使用逻辑服务名定义为host和port的URI,而返回的URI则是通过ServiceInstance的服务实例详情拼接出的具体的host:port形式的请求地址。一言以蔽之,就是把类似于http://HELLO-SERVICE/hello这种地址转为类似于http://195.124.207.128/hello地址(IP地址也可能是域名)。
四、SpringCloud中的Feign负载均衡
Feign是一个声明式Web服务客户端,能让编写web客户端更加简单,创建一个接口并在上面添加注解去使用Feign,它支持Feign注解和JAX-RS注解。Feign也支持可插拔式的编码器和解码器,Feign 默认整合了Eureka和Ribbon实现客户端负载均衡。
Feign核心是使得编写Java Http客户端变得更容易,使用接口和注解(类似Mybatis中Dao和@Mapper)来完成对服务提供者接口的绑定,从而简化操作。Feign集成了Ribbon,可以利用Ribbon实现负载均衡。
Spring cloud已经使用Ribbon在客户端负载均衡,为什么还会有feign方式呢?Ribbon调用是基于url来调用的,而java是面向对象的语言,所以Ribbon显得有点格格不入了,这也就是Feign会出现的原因.
可以理解为Feign将Ribbon封装成了面向对象的调用方式。Feign负载均衡默认是通过Ribbon实现的。也可以进行扩展。
源码导读:
LoadBalancerFeignClient是一个非常重要的类,该类主要是维护负载均衡器,通过负载均衡器里面的IRule(负载均衡策略)来寻找具体的微服务来完成url调用。
execute方法:
//核心方法
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
requestConfig).toResponse(); //这句话是核心,下面有具体分析
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
lbClient(clientName): 该方法是获取负载均衡器,简单来说他就是一个类,该类里面持有负载均衡策略,你定义的负载均衡策略会实例化,然后动态的注入该类中。
executeWithLoadBalancer方法:该方法看名字都能看出意思,这个就是负载均衡的入口方法了
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
.withLoadBalancerContext(this)
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri())
.build();
try {
return command.submit( //核心方法submit,采用rxjava,理解成java的观察者模式即可
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
分析:
里面主要是新建了一个类LoadBalancerCommand,这个类的核心方法submit采用了RxJava的响应式编程,理解成java中的观察者模式即可。下面我把submit的核心代码整理出来:
public Observable<T> submit(final ServerOperation<T> operation) {
final ExecutionInfoContext context = new ExecutionInfoContext();
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();
// Use the load balancer
Observable<T> o =
(server == null ? selectServer() : Observable.just(server)) //selectServer就是根据负载均衡规则选取微服务
.concatMap(new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
public Observable<T> call(Server server) {
context.setServer(server);
final ServerStats stats = loadBalancerContext.getServerStats(server);
// Called for each attempt and retry
Observable<T> o = Observable
.just(server)
.concatMap(new Func1<Server, Observable<T>>() {
// do other ……
}
});
});
}
分析:
selectServer():该方法就是根据负载均衡策略来选取合适的消费者微服务。根据负载均衡器的上下文调用getServerFromLoadBalancer方法获取消费者微服务:
public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
String host = null;
int port = -1;
ILoadBalancer lb = getLoadBalancer();
if (host == null) {
if (lb != null){
Server svc = lb.chooseServer(loadBalancerKey); //负载均衡器根据负载均衡算法来寻找消费者微服务,然后获取具体的端口和ip
if (svc == null){
throw new ClientException(ClientException.ErrorType.GENERAL,
"Load balancer does not have available server for client: "
+ clientName);
}
host = svc.getHost();
if (host == null){
throw new ClientException(ClientException.ErrorType.GENERAL,
"Invalid Server for :" + svc);
}
logger.debug("{} using LB returned Server: {} for request {}", new Object[]{clientName, svc, original});
return svc;
} else{
…………
……………
}
return new Server(host, port);
}
ILoadBalancer lb = getLoadBalancer(): 这句就是获取具体的负载均衡器。
Server svc = lb.chooseServer(loadBalancerKey); 负载均衡器根据WeightedResponseTimeRule策略来寻找消费者微服务 Server svc,该svc中包含host和port,这样我们就可以利用httpclient愉快的调用了。
五、Ribbon和Feign的使用方式
我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!
参考资料:
- https://blog.csdn.net/qq_20619819/article/details/81089997
- https://blog.csdn.net/gyshun/article/details/82808128
- https://blog.csdn.net/davis2015csdn/article/details/77434505
- https://blog.csdn.net/hnkd16/article/details/81704085
转载:https://blog.csdn.net/m0_37609579/article/details/100609554