X

从一个生产的问题分析ElasticSearch负载均衡算法

背景

负载均衡是分布式系统里最常用的能力,他的实现方式有很多,轮询,随机,加权轮询,一致性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.为什么轮询的负载均衡算法里,坏节点的下一台机器流量会明显高?

    1. 为什么会超时?为什么超时最开始一波波的,重启后超时会打散了?
    2. 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>

龙安_任天兵: 不忘初心,方得始终!