客户端负载均衡Ribbon之源码解析
什么是负载均衡器?
假设有一个分布式系统,该系统由在不同计算机上运行的许多服务组成。但是,当用户数量很大时,通常会为服务创建多个副本。每个副本都在另一台计算机上运行。此时,出现 “load balancer(负载均衡器)”。它有助于在服务器之间平均分配传入流量。
服务器端负载均衡器
传统上,load balancers(例如nginx、f5)是放置在服务器端的组件。当请求来自 客户端 时,它们将转到负载均衡器,负载均衡器将为请求指定 服务器。负载均衡器使用的最简单的算法是随机指定。在这种情况下,大多数负载平衡器是用于控制负载平衡的硬件集成软件。
重点:
- 对客户端不透明,客户端不知道服务器端的服务列表,甚至不知道自己发送请求的目标地址存在负载均衡器。
- 服务器端维护负载均衡服务器,控制负载均衡策略和算法。
客户端负载均衡器
当负载均衡器位于 客户端 时,客户端得到可用的服务器列表然后按照特定的负载均衡策略,分发请求到不同的 服务器 。
重点:
- 对客户端透明,客户端需要知道服务器端的服务列表,需要自行决定请求要发送的目标地址。
- 客户端维护负载均衡服务器,控制负载均衡策略和算法。
- 目前单独提供的客户端实现比较少( 我用过的只有ribbon),大部分都是在框架内部自行实现。
ribbon
简介
ribbon是netflix公司开源的一个客户单负载均衡的项目,可以自动与 eureka 进行交互。它提供下列特性:
- 负载均衡
- 容错
- 以异步和反应式模型执行多协议 (http, tcp, udp)
- 缓存和批量
ribbon中的关键组件
- serverlist:可以响应客户端的特定服务的服务器列表。
- serverlistfilter:可以动态获得的具有所需特征的候选服务器列表的过滤器。
- serverlistupdater:用于执行动态服务器列表更新。
- rule:负载均衡策略,用于确定从服务器列表返回哪个服务器。
- ping:客户端用于快速检查服务器当时是否处于活动状态。
- loadbalancer:负载均衡器,负责负载均衡调度的管理。
源码分析
loadbalancerclient
实际应用中,通常将 resttemplate 和 ribbon 结合使用,例如:
@configuration public class ribbonconfig { @bean @loadbalanced resttemplate resttemplate() { return new resttemplate(); } }
消费者调用服务接口:
@service public class ribbonservice { @autowired private resttemplate resttemplate; public string hi(string name) { return resttemplate.getforobject("http://service-hi/hi?name="+name,string.class); } }
@loadbalanced,通过源码可以发现这是一个标记注解:
/** * annotation to mark a resttemplate bean to be configured to use a loadbalancerclient * @author spencer gibb */ @target({ elementtype.field, elementtype.parameter, elementtype.method }) @retention(retentionpolicy.runtime) @documented @inherited @qualifier public @interface loadbalanced { }
通过注释可以知道@loadbalanced注解是用来给resttemplate做标记,方便我们对resttemplate添加一个loadbalancerclient,以实现客户端负载均衡。
根据spring boot的自动配置原理,可以知道同包下的loadbalancerautoconfiguration,应该是实现客户端负载均衡器的自动化配置类。代码如下:
@configuration @conditionalonclass(resttemplate.class) @conditionalonbean(loadbalancerclient.class) @enableconfigurationproperties(loadbalancerretryproperties.class) public class loadbalancerautoconfiguration { @loadbalanced @autowired(required = false) private list<resttemplate> resttemplates = collections.emptylist(); @bean public smartinitializingsingleton loadbalancedresttemplateinitializerdeprecated( final objectprovider<list<resttemplatecustomizer>> resttemplatecustomizers) { return () -> resttemplatecustomizers.ifavailable(customizers -> { for (resttemplate resttemplate : loadbalancerautoconfiguration.this.resttemplates) { for (resttemplatecustomizer customizer : customizers) { customizer.customize(resttemplate); } } }); } @autowired(required = false) private list<loadbalancerrequesttransformer> transformers = collections.emptylist(); @bean @conditionalonmissingbean public loadbalancerrequestfactory loadbalancerrequestfactory( loadbalancerclient loadbalancerclient) { return new loadbalancerrequestfactory(loadbalancerclient, transformers); } @configuration @conditionalonmissingclass("org.springframework.retry.support.retrytemplate") static class loadbalancerinterceptorconfig { @bean public loadbalancerinterceptor ribboninterceptor( loadbalancerclient loadbalancerclient, loadbalancerrequestfactory requestfactory) { return new loadbalancerinterceptor(loadbalancerclient, requestfactory); } @bean @conditionalonmissingbean public resttemplatecustomizer resttemplatecustomizer( final loadbalancerinterceptor loadbalancerinterceptor) { return resttemplate -> { list<clienthttprequestinterceptor> list = new arraylist<>( resttemplate.getinterceptors()); list.add(loadbalancerinterceptor); resttemplate.setinterceptors(list); }; } } @configuration @conditionalonclass(retrytemplate.class) public static class retryautoconfiguration { @bean @conditionalonmissingbean public loadbalancedretryfactory loadbalancedretryfactory() { return new loadbalancedretryfactory() {}; } } @configuration @conditionalonclass(retrytemplate.class) public static class retryinterceptorautoconfiguration { @bean @conditionalonmissingbean public retryloadbalancerinterceptor ribboninterceptor( loadbalancerclient loadbalancerclient, loadbalancerretryproperties properties, loadbalancerrequestfactory requestfactory, loadbalancedretryfactory loadbalancedretryfactory) { return new retryloadbalancerinterceptor(loadbalancerclient, properties, requestfactory, loadbalancedretryfactory); } @bean @conditionalonmissingbean public resttemplatecustomizer resttemplatecustomizer( final retryloadbalancerinterceptor loadbalancerinterceptor) { return resttemplate -> { list<clienthttprequestinterceptor> list = new arraylist<>( resttemplate.getinterceptors()); list.add(loadbalancerinterceptor); resttemplate.setinterceptors(list); }; } } }
从代码可以看出,这个类作用主要是使用resttemplatecustomizer对所有标注了@loadbalanced的resttemplate bean添加了一个loadbalancerinterceptor拦截器,而这个拦截器的作用就是对请求的uri进行转换获取到具体应该请求哪个服务实例。
那再看看添加的拦截器loadbalancerinterceptor的代码,如下:
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, requestfactory.createrequest(request, body, execution)); } }
从代码可以看出 loadbalancerinterceptor 拦截了请求后,通过loadbalancerclient执行具体的请求发送。
打开loadbalancerclient,发现它是一个接口:
public interface loadbalancerclient { serviceinstance choose(string serviceid); <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):根据传入的服务id,从负载均衡器中为指定的服务选择一个服务实例。
t execute(string serviceid, loadbalancerrequest request):根据传入的服务id,指定的负载均衡器中的服务实例执行请求。 t execute(string serviceid, serviceinstance serviceinstance, loadbalancerrequest request):根据传入的服务实例,执行请求。
loadbalancerclient 有一个唯一的实现类 ribbonloadbalancerclient,关键代码如下:
public class ribbonloadbalancerclient implements loadbalancerclient { public serviceinstance choose(string serviceid) { server server = this.getserver(serviceid); return server == null ? null : new ribbonloadbalancerclient.ribbonserver(serviceid, server, this.issecure(server, serviceid), this.serverintrospector(serviceid).getmetadata(server)); } public <t> t execute(string serviceid, loadbalancerrequest<t> request) throws ioexception { iloadbalancer loadbalancer = this.getloadbalancer(serviceid); server server = this.getserver(loadbalancer); if (server == null) { throw new illegalstateexception("no instances available for " + serviceid); } else { ribbonloadbalancerclient.ribbonserver ribbonserver = new ribbonloadbalancerclient.ribbonserver(serviceid, server, this.issecure(server, serviceid), this.serverintrospector(serviceid).getmetadata(server)); return this.execute(serviceid, ribbonserver, request); } } protected server getserver(string serviceid) { return this.getserver(this.getloadbalancer(serviceid)); } protected server getserver(iloadbalancer loadbalancer) { return loadbalancer == null ? null : loadbalancer.chooseserver("default"); } protected iloadbalancer getloadbalancer(string serviceid) { return this.clientfactory.getloadbalancer(serviceid); } //省略... }
负载均衡器
从 ribbonloadbalancerclient 代码可以看出,实际负载均衡的是通过 iloadbalancer 来实现的。
iloadbalancer 接口代码如下:
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:跟据key,从负载均衡器获取服务实例。
- markserverdown:用来标记某个服务实例下线。
- getreachableservers:获取可用的服务实例集合。
- getallservers():获取所有服务实例集合,包括下线的服务实例。
iloadbalancer 的实现 依赖关系示意图如下:
- nooploadbalancer:啥都不做
- baseloadbalancer:
- 一个负载均衡器的基本实现,其中有一个任意列表,可以将服务器设置为服务器池。
- 可以设置一个ping来确定服务器的活力。
- 在内部,该类维护一个“all”服务器列表,以及一个“up”服务器列表,并根据调用者的要求使用它们。
- dynamicserverlistloadbalancer:
- 通过动态的获取服务器的候选列表的负载平衡器。
- 可以通过筛选标准来传递服务器列表,以过滤不符合所需条件的服务器。
- zoneawareloadbalancer:
- 用于测量区域条件的关键指标是平均活动请求,它根据每个rest客户机和每个区域聚合。这是区域内未完成的请求总数除以可用目标实例的数量(不包括断路器跳闸实例)。当在坏区上缓慢发生超时时,此度量非常有效。
- 该负载均衡器将计算并检查所有可用区域的区域状态。如果任何区域的平均活动请求已达到配置的阈值,则该区域将从活动服务器列表中删除。如果超过一个区域达到阈值,则将删除每个服务器上活动请求最多的区域。一旦去掉最坏的区域,将在其余区域中选择一个区域,其概率与其实例数成正比。服务器将使用给定的规则从所选区域返回。对于每个请求,将重复上述步骤。也就是说,每个与区域相关的负载平衡决策都是实时做出的,最新的统计数据可以帮助进行选择。
那么在整合ribbon的时候spring cloud默认采用了哪个具体实现呢?我们通过ribbonclientconfiguration配置类,可以知道在整合时默认采用了zoneawareloadbalancer来实现负载均衡器。
@bean @conditionalonmissingbean public iloadbalancer ribbonloadbalancer(iclientconfig config, serverlist<server> serverlist, serverlistfilter<server> serverlistfilter, irule rule, iping ping, serverlistupdater serverlistupdater) { return (iloadbalancer)(this.propertiesfactory .isset(iloadbalancer.class, this.name) ? (iloadbalancer)this.propertiesfactory .get(iloadbalancer.class, config, this.name) : new zoneawareloadbalancer(config, rule, ping, serverlist, serverlistfilter, serverlistupdater)); }
从这段代码 ,也可以看出,负载均衡器所需的主要配置项是iclientconfig, serverlist, serverlistfilter, irule, iping, serverlistupdater。下面逐一分析他们。
iclientconfig
iclientconfig 用于对客户端或者负载均衡的配置,它的默认实现类为 defaultclientconfigimpl。
irule
为loadbalancer定义“负载均衡策略”的接口。
public interface irule{ public server choose(object key); public void setloadbalancer(iloadbalancer lb); public iloadbalancer getloadbalancer(); }
irule 的实现 依赖关系示意图如下:
- bestavailablerule:选择具有最低并发请求的服务器。
- clientconfigenabledroundrobinrule:轮询。
- randomrule:随机选择一个服务器。
- roundrobinrule:轮询选择服务器。
- retryrule:具备重试机制的轮询。
- weightedresponsetimerule:根据使用平均响应时间去分配一个weight(权重) ,weight越低,被选择的可能性就越低。
- zoneavoidancerule:根据区域和可用性筛选,再轮询选择服务器。
iping
定义如何 “ping” 服务器以检查其是否存活。
public interface iping { public boolean isalive(server server); }
iping 的实现 依赖关系示意图如下:
- pingurl:真实的去ping 某个url,判断其是否alive。
- pingconstant:固定返回某服务是否可用,默认返回true,即可用
- noopping:不去ping,直接返回true,即可用。
- dummyping:继承抽象类abstractloadbalancerping,认为所以服务都是存活状态,返回true,即可用。
- niwsdiscoveryping:结合eureka使用时,如果discovery client在线,则认为心跳检测通过。
serverlist
定义获取所有的服务实例清单。
public interface serverlist<t extends server> { public list<t> getinitiallistofservers(); public list<t> getupdatedlistofservers(); }
serverlist 的实现 依赖关系示意图如下:
- domainextractingserverlist:代理类,根据传入的serverlist的值,实现具体的逻辑。
- configurationbasedserverlist:从配置文件中加载服务器列表。
- discoveryenabledniwsserverlist:从eureka注册中心中获取服务器列表。
- staticserverlist:通过静态配置来维护服务器列表。
serverlistfilter
允许根据过滤配置动态获得的具有所需特性的候选服务器列表。
public interface serverlistfilter<t extends server> { public list<t> getfilteredlistofservers(list<t> servers); }
serverlistfilter 的实现 依赖关系示意图如下:
- defaultniwsserverlistfilter:完全继承自zoneaffinityserverlistfilter。
- zonepreferenceserverlistfilter:enablezoneaffinity 或 enablezoneexclusivity 开启状态使用,默认关闭。处理基于区域感知的过滤服务器,过滤掉不和客户端在相同zone的服务,若不存在相同zone,则不进行过滤。
- serverlistsubsetfilter:服务器列表筛选器,它将负载平衡器使用的服务器数量限制为所有服务器的子集。如果服务器机群很大(例如数百个),并且不需要使用每一个机群并将连接保存在http客户机的连接池中,那么这是非常有用的。它还可以通过比较总的网络故障和并发连接来驱逐相对不健康的服务器。
serverlistupdater
用于执行动态服务器列表更新。
public interface serverlistupdater { public interface updateaction { void doupdate(); } void start(updateaction updateaction); void stop(); string getlastupdate(); long getdurationsincelastupdatems(); int getnumbermissedcycles(); int getcorethreads(); }
serverlistupdater 的实现 依赖关系示意图如下:
- pollingserverlistupdater:默认的实现策略,会启动一个定时线程池,定时执行更新策略。
- eurekanotificationserverlistupdater:利用eureka的事件监听器来驱动服务列表的更新操作。
参考资料
https://github.com/netflix/ribbon/wiki
http://tech.lede.com/2018/01/11/rd/server/netflixribbon/
http://blog.didispace.com/springcloud-sourcecode-ribbon/
https://www.fangzhipeng.com/springcloud/2017/08/11/ribbon-resources.html
https://blog.csdn.net/tincox/article/details/79210309
欢迎扫码或微信搜索公众号《程序员果果》关注我,关注有惊喜~