背景
负载均衡是分布式系统里最常用的能力,他的实现方式有很多,轮询,随机,加权轮询,一致性hash等网上文章很多,可以自行查阅,今天要讲的是遇到的一个真实的生产的问题。
公司内部的ES访问架构一般是, Java应用--->SLB(域名)---->ES ingest node (no data) --> ES data node ,其中ingest节点是对外暴露的,供Java应用访问,承担了一个纯client角色,不提供数据存储和倒排索引检索服务。这其中SLB是为了方便起到一个域名和负载均衡的功能,绑定后端的n个client节点,并且做到对业务透明,但是毕竟还是有开销的,多了一次网络rpc的转发(尽管他很快),同时也是多花了一份钱。所以在930的时候我们把SLB去掉了,并且进行了验证完全没有问题,这其中还要得益于es本身就支持ip配置列表,并且自身实现了负载均衡的功能。 更改之后的访问链路,Java应用--->ES ingest node -->ES data node
就在缩容的时候,我们遇到了问题,我们更改了es client里面配置的ip列表,结果出现了超时,同时观察到一个现象,每次更新ip 列表的时候,总有一台机器的连接数明显高于其他机器,这是为何呢?关键节点如下
- step1 下午16:xx 更新了es client里的机器列表,系统表现正常
- step2 晚上20:5x 开始下线数据节点,系统出现了少量超时和报错,并且观察到有一台es的client节点流量明显高于其他机器,出现了负载不均衡
- step3 晚上21:1x 以为es 流量高的节点有问题,所以进行了下线
- step4 晚上21:2x 随着那台client节点下线,另外一台新的client节点又出现了流量过高的情况,并且超时一波一波,像是定时发生的
- step5 怀疑是不是es client sdk 初始化有问题,代码里创建了新的es client的时候,老的未正常销毁,于是开始分批重启Java应用,让他重新初始化es client,而不是做热替换,分6批,每批大概10台机器
- step6 观察到超时依然持续,负载不均衡的问题,依然没有解决,同时超时从一波波变成了持续但是少量,相当于原来超时的波峰被均匀打散到各个时间段了
- step7 随后发现,es client里的ip列表配错了,里面配置了data node 数据节点,而正好20:5x下线了这几台机器,这几台已经不可用了
- step8 修正es client连接的ip列表,系统报错消失,负载又均衡了,系统恢复正常
问题
从负载不均衡的表现上来看,应该是配置的ip列表了,有机器不可用了,那么这台机器的下一台可用的机器,流量就会比别的机器明显高,出现了负载不均衡的问题,应该是es的负载均衡的逻辑导致的,因此决定翻一翻es的负载均衡的算法,详细的看看。
先抽象一下问题,在RR的负载均衡算法下,
有 ServerA, ServerB,ServerC,ServerD,ServerE 5台机器,当ServerD不可用的时候,ServerE的流量会明显增高,当ServerE不可用的时候,ServerA的流量会明显增高。
1.为什么轮询的负载均衡算法里,坏节点的下一台机器流量会明显高?- 为什么会超时?为什么超时最开始一波波的,重启后超时会打散了?
- ES是如何处理es client里的坏节点的? 如果是加黑名单,为什么还会出现负载不均衡和超时问题?
源码分析
整体流程
抽茧剥丝,去掉所有干扰因素,今天就主要看看es的负载均衡的实现,过程不细说,直接上答案
/** * Sends a request to the Elasticsearch cluster that the client points to. * Blocks until the request is completed and returns its response or fails * by throwing an exception. Selects a host out of the provided ones in a * round-robin fashion. Failing hosts are marked dead and retried after a * certain amount of time (minimum 1 minute, maximum 30 minutes), depending * on how many times they previously failed (the more failures, the later * they will be retried). In case of failures all of the alive nodes (or * dead nodes that deserve a retry) are retried until one responds or none * of them does, in which case an {@link IOException} will be thrown. * * This method works by performing an asynchronous call and waiting * for the result. If the asynchronous call throws an exception we wrap * it and rethrow it so that the stack trace attached to the exception * contains the call site. While we attempt to preserve the original * exception this isn't always possible and likely haven't covered all of * the cases. You can get the original exception from * {@link Exception#getCause()}. * * @param request the request to perform * @return the response returned by Elasticsearch * @throws IOException in case of a problem or the connection was aborted * @throws ClientProtocolException in case of an http protocol error * @throws ResponseException in case Elasticsearch responded with a status code that indicated an error */ public Response performRequest(Request request) throws IOException { InternalRequest internalRequest = new InternalRequest(request); return performRequest(nextNodes(), internalRequest, null); }
这里有个nextNodes() ,返回值是一个NodeTuple<Iterator>