『 LoadBalancer』Ribbon 负载均衡器原理解析
前言
Ribbon 是 Spring Cloud Netflix 体系下,一个客户端侧的负载均衡器,在 Spring Cloud Netflix 体系中,一般结合 Feign 使用。
目前 Ribbon 已不做新特性开发,处于维护状态,Netflix 团队已经把精力转移到 gRPC上。
但 Ribbon 依然跑在很多公司的生产环境上,所以并不影响我们对它的学习。
学习一些框架的设计思想/实现方式,更有助于我们日常开发写出高质量代码。
基本组件(类)
开局一张图,内容全靠编(bushi)
先以一张图,整体展示一下重要组件以及其作用:
ServerList
/ServerListUpdater
定义初始化,更新服务器列表的接口;更新服务端列表策略。
获取服务端列表的来源可以是 配置文件/注册中心 等。
ILoadBalancer
定义了负载均衡器操作的接口。包括保存一组服务器、标记服务端状态以及选择服务器等方法。
ServerListFilter
用于候选服务器过滤器。
说人话,就是执行一些规则挑选出一部分服务器后,再给负载均衡器挑选结果。
例如服务端可能在多区域部署,可以通过此 Filter 过滤出在同区域的服务器后,再交给负载均衡器。
个人觉得与 IRule
的定义有重叠。
IClient
Ribbon 定义的客户端,定义了执行请求的方法。
具体发送请求的方法,由实现类自行定义。
-
IRule
定义负载均衡策略。
下表是 Spring Cloud Netflix 默认情况下为 Ribbon 提供的 Bean:
Bean Type | Bean Name | Class Name |
---|---|---|
IClientConfig | ribbonClientConfig | DefaultClientConfigImpl |
IRule | ribbonRule | ZoneAvoidanceRule |
IPing | ribbonPing | DummyPing |
ServerList<Server> | ribbonServerList | ConfigurationBasedServerList |
ServerListFilter<Server> | ribbonServerListFilter | ZonePreferenceServerListFilter |
ILoadBalancer | ribbonLoadBalancer | ZoneAwareLoadBalancer |
ServerListUpdater | ribbonServerListUpdater | PollingServerListUpdater |
以上类,均在配置类RibbonClientConfiguration
中加载。
重要代码分析
PollingServerListUpdater
PollingServerListUpdater 是 Ribbon 接入注册中心的核心类。此类负责同步拉取各服务的IP地址集合。
public class PollingServerListUpdater implements ServerListUpdater {
// ...
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
// 一个定时任务,每隔30s拉取一次服务侧信息
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs, // 1000
refreshIntervalMs, // 30 * 1000
TimeUnit.MILLISECONDS
);
} else {
...
}
}
// ...
}
ZoneAwareLoadBalancer
ZoneAwareLoadBalancer 定义了区的概念,可以是物理区,也可以是逻辑区。
LoadBalancer 将计算并检查所有可用区域的区域统计信息。
如果任何区域的平均活动请求数达到配置的阈值,则该区域将从活动服务器列表中删除。
如果多个区域达到阈值,则每台服务器具有最活跃请求的区域将被删除。
一旦最差的区域被删除,就会以与其实例数量成比例的概率从其余区域中选择一个区域。
这个是与注册中心通信的核心类,该类的主要继承/实现关系如下:
其中,DynamicServerListLoadBalancer
实现了从远端获取服务器列表。
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
// ...
volatile ServerList<T> serverListImpl;
volatile ServerListFilter<T> filter;
protected final ServerListUpdater.UpdateAction updateAction = ()-> {updateListOfServers()};
protected volatile ServerListUpdater serverListUpdater;
// ...
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList<T> serverList, ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
// ...
// 这里的逻辑,调用了 serverListUpdater.start(updateAction);
restOfInit(clientConfig);
}
// ...
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
// 拉取新的服务端信息
servers = serverListImpl.getUpdatedListOfServers();
// ...
}
updateAllServerList(servers);
}
/**
* Update the AllServer list in the LoadBalancer if necessary and enabled
*
* @param ls
*/
protected void updateAllServerList(List<T> ls) {
// other threads might be doing this - in which case, we pass
if (serverListUpdateInProgress.compareAndSet(false, true)) {
try {
// 把所有服务端信息都设置为存活
for (T s : ls) {
s.setAlive(true);
}
// 更新服务端信息
setServersList(ls);
//...
} finally {
serverListUpdateInProgress.set(false);
}
}
}
}
使用方式
-
直接使用
不搭配
Feign
,直接使用。直接在代码中注入SpringClientFactory
使用。配置文件:
damai: ribbon: listOfServers: 127.0.0.1:8080,127.0.0.1:8081
代码示例:
@Resource SpringClientFactory springClientFactory; ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer("damai"); Server server = loadBalancer.chooseServer(null);
-
搭配
Feign
,不带注册中心使用增加 Feign 依赖,配置如上。
代码示例:
@FeignClient(name = "damai") public interface FooRibbonClient { @GetMapping("/printLog/{log}") String homePage(@PathVariable String log); } @Resource FooRibbonClient fooRibbonClient; @GetMapping("/test/{log}") public String homePage(@PathVariable String log) { return fooRibbonClient.homePage(log); }
-
结合注册中心使用
使用最多的方式,集成注册中心依赖即可。
不再赘述。
Ribbon 与 Feign 结合点分析
Feign 与 Ribbon 是两个组件。Feign 负责服务间通信;Ribbon 负责负载均衡。
那么这两个组件是如何结合起来使用的呢?
想吐槽的一点是,如果你只引进
spring-cloud-starter-openfeign
,代码是跑不起来的。No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon or spring-cloud-starter-loadbalancer
关键点在于 Feign 包下的一个文件夹中 org.springframework.cloud.openfeign.ribbon
,显然是 Feign 对 Ribbon 做了特殊支持。
Ribbon 开发人员贴心的为你准备了 DefaultFeignLoadBalancedConfiguration
\ OkHttpFeignLoadBalancedConfiguration
\ HttpClientFeignLoadBalancedConfiguration
... 供你选择。
查看这几个类后,发现里面只是创建了 LoadBalancerFeignClient
,以默认配置为例:
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory, HttpClient httpClient) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
LoadBalancerFeignClient extend Client
,到这里就已经与我们曾经学习过的『OpenFeign』原理篇 # 执行请求与负载均衡
联系上了。
即,请求会被代理到 LoadBalancerFeignClient#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);
// 注意这里将 this.delegate 设置到 Request 中
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
// 从 SpringClientFactory 中获取 FeignLoadBalancer
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
但是我们需要注意 LoadBalancerFeignClient 又再次讲请求转发到 Ribbon 的 Client
上,Ribbon Client 又通过 Feign Client 透传的 this.delegate
,最终执行了 HTTP 请求:
FeignLoadBalancer#execute
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride) throws IOException {
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(override.connectTimeout(connectTimeout),
TimeUnit.MILLISECONDS, override.readTimeout(readTimeout),
TimeUnit.MILLISECONDS, override.isFollowRedirects(followRedirects));
}
else {
options = new Request.Options(connectTimeout, TimeUnit.MILLISECONDS,
readTimeout, TimeUnit.MILLISECONDS, followRedirects);
}
Response response = request.client().execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
... 烦死了😡,参数传来传去的,找都找不见!!!
总结来说,就是 Feign 实现了 Ribbon 提供的接口,来使用 Ribbon 负载均衡的能力
IClient
-->AbstractLoadBalancerAwareClient
-->FeignLoadBalancer
小结
这篇通过 图解 Ribbon 基础组件入手,介绍了 Ribbon 负载均衡的主要流程及其对应的类;并分析了 Feign 与 Ribbon 的结合实现点。
在文章开头已经说过,Ribbon 已经不再更新新特性,只做重大 BUG维护。
Spring Cloud 团队在又推出了新一代负载均衡组件 LoadBlancer ,预计下一篇文章,我们继续来学习该组件。
转载自:https://juejin.cn/post/7270870135228678204