likes
comments
collection
share

『 LoadBalancer』Ribbon 负载均衡器原理解析

作者站长头像
站长
· 阅读数 49

前言

Ribbon 是 Spring Cloud Netflix 体系下,一个客户端侧的负载均衡器,在 Spring Cloud Netflix 体系中,一般结合 Feign 使用。

目前 Ribbon 已不做新特性开发,处于维护状态,Netflix 团队已经把精力转移到 gRPC上。

但 Ribbon 依然跑在很多公司的生产环境上,所以并不影响我们对它的学习。

学习一些框架的设计思想/实现方式,更有助于我们日常开发写出高质量代码。

基本组件(类)

开局一张图,内容全靠编(bushi)

  先以一张图,整体展示一下重要组件以及其作用:

『 LoadBalancer』Ribbon 负载均衡器原理解析

  • ServerList/ ServerListUpdater

   定义初始化,更新服务器列表的接口;更新服务端列表策略。

   获取服务端列表的来源可以是 配置文件/注册中心 等。

  • ILoadBalancer

   定义了负载均衡器操作的接口。包括保存一组服务器、标记服务端状态以及选择服务器等方法。

  • ServerListFilter

   用于候选服务器过滤器。

   说人话,就是执行一些规则挑选出一部分服务器后,再给负载均衡器挑选结果。

   例如服务端可能在多区域部署,可以通过此 Filter 过滤出在同区域的服务器后,再交给负载均衡器。

   个人觉得与 IRule 的定义有重叠。

  • IClient

   Ribbon 定义的客户端,定义了执行请求的方法。

   具体发送请求的方法,由实现类自行定义。

  • IRule

    定义负载均衡策略。

  下表是 Spring Cloud Netflix 默认情况下为 Ribbon 提供的 Bean:

Bean TypeBean NameClass Name
IClientConfigribbonClientConfigDefaultClientConfigImpl
IRuleribbonRuleZoneAvoidanceRule
IPingribbonPingDummyPing
ServerList<Server>ribbonServerListConfigurationBasedServerList
ServerListFilter<Server>ribbonServerListFilterZonePreferenceServerListFilter
ILoadBalancerribbonLoadBalancerZoneAwareLoadBalancer
ServerListUpdaterribbonServerListUpdaterPollingServerListUpdater

  以上类,均在配置类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 将计算并检查所有可用区域的区域统计信息。

如果任何区域的平均活动请求数达到配置的阈值,则该区域将从活动服务器列表中删除。

如果多个区域达到阈值,则每台服务器具有最活跃请求的区域将被删除。

一旦最差的区域被删除,就会以与其实例数量成比例的概率从其余区域中选择一个区域。

  这个是与注册中心通信的核心类,该类的主要继承/实现关系如下:

   『 LoadBalancer』Ribbon 负载均衡器原理解析

  其中,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 做了特殊支持

『 LoadBalancer』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
评论
请登录