Elasticsearch High Level Rest Client偶现访问集群超时的问题定位与解决

2022-02-14 17:04:31 浏览数 (3)

Elasticsearch High Level Rest Client偶现访问集群超时的问题定位与解决

背景

某个客户计划使用云上的es集群,在前期准备工作做完之后,在某天半夜进行切割,切割之后的几个小时内,客户反馈客户端访问ES集群会出现Connection reset by peer 或者 listener timeout after waiting for 30000 ms。

问题定位

客户端使用的是es的High Level Rest Client, es集群和客户端版本都是6.8, 客户反馈在切割之前没有问题,切割之后才出现了超时。客户觉得这一定跟es集群有关系。通过一番排查,发现集群的cpu使用率、load都比较低,不可能因为集群负载高而出现超时。并且,connection reset by peer 这种错误,是因为tcp连接被对端关闭才触发的,所以可能是es服务端主动关闭了连接?

之后在es集群的所有的节点上部署抓包,因为节点比较多,每个节点上的流量也比较大,计划对每个节点抓2个小时的包,看看能不能有什么发现?使用Wireshark对抓包得到pcap文件进行分析,发现抓到的tcp报文都是正常的,没什么异常,这就比较奇怪了。

因为es集群对客户只暴露了一个vip, 这个vip是通过vpc gateway实现的负载均衡功能,因此联系了网络侧的同事,协助排查这个问题。网络侧的同事反馈这种客户端报的connection reset by peer 错误,只可能是后端RS也就是es集群的节点返回的,作为一个网关,vpc gateway是不会主动返回给客户端RST回包的,建议我们在es集群的所有节点上部署抓RST回包,来进行确认。

因此,在所有的es集群的节点上重新部署抓包,只抓RST回包:

代码语言:txt复制
tcpdump -i eth0  host x.x.x.x and port 9200 and 'tcp[tcpflags] & (tcp-rst) != 0' -vvv -w 1_rst.pcap

跑了一晚上,终于抓到了RST回包:

从上图中可以看到,某一时刻es节点开始对网关ip进行了tcp keepalive的探活,在连续发送了9次的探活报文没有得到响应后直接回复了RST报文给网关,从而断掉了tcp连接。而对于这种回包,网络侧的同事表示这是不正常的,因为客户端的请求链路是客户端->vpc gateway->es节点,而在es节点上看到的tcp报文源ip是vpc gateway的,es节点作为服务端,不应该对直接对网关进行tcp 连接保活的。通过查看es集群节点(centos7)的tcp keepalive配置,发现默认的选项是:

代码语言:txt复制
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200

也就是说如果一条tcp连接超过2小时没有流量的话,系统就会主动去探测该条连接进行保活,但是保活的请求直接发送到网关,网关是不会直接回复的,所以可以抓到上面的在连续发送了9次探活报文没有得到响应后直接回复了RST报文给网关,从而断掉了tcp连接。

而客户的业务是在一直运行的,为什么一条连接会有超过2小时没有流量呢?通过查阅es的High Level Rest Client的代码,发现该客户端会使用到client连接池,默认有30个实例,每个client持有一个http连接,并且开启http的keep-alive策略复用连接。但是问题是该客户端是不会对连接进行探测保活的,也就是连接池里可能会存在2小时没有流量的连接;并且客户端也不会主动剔除连接池里实际已经不可用的连接,例如本例中出现的被服务端主动回复RST断掉的连接,因此在客户端如果使用了连接池里已经不可用的连接的时候,会出现connection reset by peer的报错。

而另外一种直接报请求超时的错误又是怎么一回事呢,经过网络侧同事的解释,vpc gateway会有默认的清理过期session的策略,如果一条tcp连接超过2小时没有流量,网关侧是会把这个连接给清理掉的,因为网关也是一个分布式集群,本身的负载和内存容量有限,不能无限制的保持连接。

问题的原因已经清楚了,该如何解决?实际上是需要客户端主动的开启tcp keepalive, 进行连接保活,使得连接池里的不会出现超过2小时没有流量的连接,也使得服务端不会再显式的对与网关之间的tcp请求进行探测保活。而经过google发现es开源社区也有针对类似问题的讨论:在经过网关或者负载均衡器访问集群时,会偶现SocketTimeoutException或者connection reset by peer (https://github.com/elastic/elasticsearch/issues/59261), 而经过讨论后,社区里的研发决定给es的High Level Rest client默认开启tcp keepalive策略来解决这类问题(https://github.com/elastic/elasticsearch/issues/65213),而在实现这个功能之前,临时的解决办法是:

  1. 第一步,在客户端代码中显式的开启tcp keepalive选项: setSoKeepAlive(true)
代码语言:txt复制
	final RestClientBuilder restClientBuilder = RestClient.builder(/* fill in arguments here */);
    // optionally perform some other configuration of restClientBuilder here if needed
    restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
            /* optionally perform some other configuration of httpClientBuilder here if needed */
            .setDefaultIOReactorConfig(IOReactorConfig.custom()
                    /* optionally perform some other configuration of IOReactorConfig here if needed */
                    .setSoKeepAlive(true)
                    .build()));
    final RestHighLevelClient restHighLevelClient = new RestHighLevelClient(restClientBuilder);
  1. 第二步,设置系统层面的tcp keepalive探测保活时间为300s, 也就是每隔5分钟发送一次探活报文,因为默认的7200s时间太长了,有可能会被网关主动断掉连接。

采用上述临时的解决办法,客户进行了灰度测试,果然不会再出现客户端超时或者connection reset by peer的错误了。

值得一提的是,开源社区里也有人提问是否可以在客户端显式的设置setKeepAliveStrategy,然后把keep-alive时间小一些,比如3分钟:

代码语言:txt复制
    RestClientBuilder builder = RestClient.builder(httpHosts.toArray(new HttpHost[0]));

    builder.setRequestConfigCallback(
            new RestClientBuilder.RequestConfigCallback() {
                @Override
                public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) {
                    return requestConfigBuilder.setConnectTimeout(elasticsearchProperties.getConnectTimeout())
                            .setSocketTimeout(elasticsearchProperties.getSocketTimeout());
                }
            });
    //
    builder.setHttpClientConfigCallback(requestConfig -> requestConfig.setKeepAliveStrategy(
            (response, context) -> TimeUnit.MINUTES.toMillis(3)));
    RestHighLevelClient client = new RestHighLevelClient(builder);

实际上,这种方式和tcp的keepalive没有关系,是指的连接池里http连接复用的最大时间,如果超过这个时间,那么http连接将不再复用,就会被服务端关闭,后续创建新的http连接。这种方式实际上也能够解决上述被网关断开连接的问题,但是因为频繁的创建和关闭连接,效率比较低,会损耗客户端性能,不推荐使用。

1 人点赞