【问题标题】:Elasticsearch Rest Client Still Giving IOException : Too Many Open FilesElasticsearch Rest 客户端仍然给出 IOException:打开的文件太多
【发布时间】:2017-10-11 07:30:34
【问题描述】:

这是对上一篇文章中提供给我的解决方案的跟进:

How to Properly Close Raw RestClient When Using Elastic Search 5.5.0 for Optimal Performance?

同样的错误信息又回来了!

2017-09-29 18:50:22.497 ERROR 11099 --- [8080-Acceptor-0] org.apache.tomcat.util.net.NioEndpoint   : Socket accept failed

java.io.IOException: Too many open files
    at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) ~[na:1.8.0_141]
    at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422) ~[na:1.8.0_141]
    at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250) ~[na:1.8.0_141]
    at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:453) ~[tomcat-embed-core-8.5.15.jar!/:8.5.15]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_141]

2017-09-29 18:50:23.885  INFO 11099 --- [Thread-3] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5387f9e0: startup date [Wed Sep 27 03:14:35 UTC 2017]; root of context hierarchy
2017-09-29 18:50:23.890  INFO 11099 --- [Thread-3] o.s.c.support.DefaultLifecycleProcessor  : Stopping beans in phase 2147483647
2017-09-29 18:50:23.891  WARN 11099 --- [Thread-3] o.s.c.support.DefaultLifecycleProcessor  : Failed to stop bean 'documentationPluginsBootstrapper'

    ... 7 common frames omitted

2017-09-29 18:50:53.891  WARN 11099 --- [Thread-3] o.s.c.support.DefaultLifecycleProcessor  : Failed to shut down 1 bean with phase value 2147483647 within timeout of 30000: [documentationPluginsBootstrapper]
2017-09-29 18:50:53.891  INFO 11099 --- [Thread-3] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2017-09-29 18:50:53.894  INFO 11099 --- [Thread-3] com.app.controller.SearchController  : Closing the ES REST client

我尝试使用上一篇文章中的解决方案。

ElasticsearchConfig:

@Configuration
public class ElasticsearchConfig {

@Value("${elasticsearch.host}")
private String host;

@Value("${elasticsearch.port}")
private int port;

@Bean
public RestClient restClient() {
    return RestClient.builder(new HttpHost(host, port))
    .setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() {
        @Override
        public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) {
            return requestConfigBuilder.setConnectTimeout(5000).setSocketTimeout(60000);
        }
    }).setMaxRetryTimeoutMillis(60000).build();
}

搜索控制器:

@RestController
@RequestMapping("/api/v1")
public class SearchController {

    @Autowired
    private RestClient restClient;

    @RequestMapping(value = "/search", method = RequestMethod.GET, produces="application/json" )
    public ResponseEntity<Object> getSearchQueryResults(@RequestParam(value = "criteria") String criteria) throws IOException {

        // Setup HTTP Headers
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");

        // Setup query and send and return ResponseEntity...

        Response response = this.restClient.performRequest(...);
    }

    @PreDestroy
    public void cleanup() {
        try {
            logger.info("Closing the ES REST client");
            this.restClient.close();
        } 
        catch (IOException ioe) {
            logger.error("Problem occurred when closing the ES REST client", ioe);
        }
    }
}    

pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.4.RELEASE</version>
</parent>

<dependencies>
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Elasticsearch -->
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>5.5.0</version>
    </dependency>

    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>transport</artifactId>
        <version>5.5.0</version>
    </dependency>

    <!-- Apache Commons -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.6</version>
    </dependency>

    <!-- Log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>

这让我觉得 RestClient 从来没有明确地关闭连接,首先......

这令人惊讶,因为我的基于 Elasticsearch Spring Boot 的微服务在两个不同的 AWS EC-2 服务器上进行了负载平衡。

日志文件报告的异常发生了 2000 次,最后 preDestroy() 才关闭客户端。查看 StackTrace 末尾记录的 @PreDestroy() 清理方法中的信息。

是否需要在 SearchController 中显式放置 finally 子句并显式关闭 RestClient 连接?

此 IOException 不会再次发生非常重要,因为此搜索微服务依赖于许多不同的移动客户端(iOS 和 Android)。

需要它具有容错性和可扩展性......或者,至少不要破坏。

这是在日志文件底部的唯一原因:

2017-09-29 18:50:53.894  INFO 11099 --- [Thread-3] com.app.controller.SearchController : Closing the ES REST client

是因为我这样做了:

kill -3 jvm_pid

我是否应该保留 @PreDestory cleanup() 方法但更改 SearchController.getSearchResults() 方法的内容以反映如下内容:

@RequestMapping(value = "/search", method = RequestMethod.GET, produces="application/json" )
public ResponseEntity<Object> getSearchQueryResults(@RequestParam(value = "criteria") String criteria) throws IOException {
    // Setup HTTP Headers
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "application/json");

    // Setup query and send and return ResponseEntity...
    Response response = null;

    try {
        // Submit Query and Obtain Response
        response = this.restClient.performRequest("POST", endPoint, Collections.singletonMap("pretty", "true"), entity);
    } 
    catch(IOException ioe) {
        logger.error("Exception when performing POST request " + ioe);
    }
    finally {
        this.restClient.close();
    }
    // return response as EsResponse();
}

这样,RestClient 连接总是关闭...

如果有人可以帮助我,将不胜感激。

【问题讨论】:

  • @PacificNW_Lover 请注意,您的异常是由sun.nio.ch.ServerSocketChannelImpl.accept0() 引发的,这表明 - 即使异常消息与您的第一篇文章中的相同 - 异常的原因是不同的,可能没什么使用 Elasticsearch 的 RestClient。也许您的服务传入的流量过多,或者可能就像在 ECS 实例中检查和升级 ulimit 一样简单。
  • 我同意@diginoise,问题似乎不是来自 RestClient。

标签: java elasticsearch rest-client


【解决方案1】:

在我看来,您做错的事情很少,但我会直接解决。

我不会写完整的解决方案(事实上,我没有执行或测试任何东西),但重要的是理解它。此外,最好将所有与数据访问相关的内容移至另一层。 无论如何,这只是一个例子,所以设计并不完美

第 1 步:导入正确的库。

与您的示例几乎相同。 我更新了示例以使用版本 5.6.2 中推荐的最后一个客户端库

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.acervera</groupId>
  <artifactId>elastic-example</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>elastic-example</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <es.version>5.6.2</es.version>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.4.RELEASE</version>
  </parent>

  <dependencies>
    <!-- Spring -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <!-- Elasticsearch -->
    <dependency>
      <groupId>org.elasticsearch</groupId>
      <artifactId>elasticsearch</artifactId>
      <version>${es.version}</version>
    </dependency>

<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>${es.version}</version>
</dependency>

    <!-- Log4j -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
  </dependencies>
</project>

第 2 步:在 bean 工厂中创建和关闭客户端。

在 bean factory 中,创建和销毁它。您可以重复使用同一个客户端。

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;

@Configuration
public class ElasticsearchConfig {


    // Here all init stuff with @Value(....)


    RestClient lowLevelRestClient;
    RestHighLevelClient client;

    @PostConstruct
    public void init() {
        lowLevelRestClient = RestClient.builder(new HttpHost("host", 9200, "http")).build();
        client = new RestHighLevelClient(lowLevelRestClient);
    }

    @PreDestroy
    public void destroy() throws IOException {
        lowLevelRestClient.close();
    }

    @Bean
    public RestHighLevelClient getClient() {
        return client;
    }

}

第 3 步:使用 Java 传输客户端执行查询。

使用 Java 传输客户端执行查询。

import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequestMapping("/api/v1")
public class SearchController {

    @Autowired
    private RestHighLevelClient client;

    @RequestMapping(value = "/search", method = RequestMethod.GET, produces="application/json" )
    public ResponseEntity<Tweet> getSearchQueryResults(@RequestParam(value = "criteria") String criteria) throws IOException {

        // This is only one example. Of course, this logic make non sense and you are going to put it in a DAO
        // layer with more logical stuff
        SearchRequest searchRequest = new SearchRequest();
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        SearchResponse searchResponse = client.search(searchRequest);

        if(searchResponse.getHits().totalHits > 0) {
            SearchHit searchHit = searchResponse.getHits().iterator().next();

            // Deserialize to Java. The best option is to use response.getSource() and Jackson
            // This is other option.
            Tweet tweet = new Tweet();
            tweet.setId(searchHit.getField("id").getValue().toString());
            tweet.setTittle(searchHit.getField("tittle").getValue().toString());
            return ResponseEntity.ok(tweet);
        } else {
            return ResponseEntity.notFound().build();
        }

    }

}

另外,使用 bean 来构建响应。

public class Tweet {

    private String id;

    private String tittle;

    public String getTittle() {
        return tittle;
    }

    public void setTittle(String tittle) {
        this.tittle = tittle;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    // Here rest of bean stuff (equal, hash, etc) or Lombok
}

步骤:4

享受 Elasticsearch!

备注:Java REST Client [5.6] » Java High Level REST Client

PS。有必要重构这个例子。只是为了了解方式。

【讨论】:

  • 在 5.6 中,他们引入了高级 REST 客户端,但从 5.0 开始提供低级 Java REST 客户端。低级 REST 客户端还可以平衡所有节点之间的查询。最后,我建议不要使用传输客户端,因为 Elastic 将弃用并最终删除传输客户端和 Java API:elastic.co/blog/…
  • @Val 反正结构是一样的。重用的线程安全连接器必须在应用程序启动或完成时仅打开和关闭一次。我将在今晚更新回复。谢谢你的观点!
  • 是的,但当时的想法是“不要在即将消失的事物上构建新事物”
  • @Val 100% 同意。我会在今晚更新回复。
  • @PacificNW_Lover cleanup() 方法只会在应用宕机并且 bean 被回收时调用。它不会在每个请求之后被调用......而且它不需要。我的应用程序中的设置完全相同,并且没有此类错误。您是否与/search API 端点的客户端保持长期连接?
【解决方案2】:

您确定不会在每个 HTTP 请求上都启动与 elasticsearch 服务器的新(线程池)连接吗?即,排队

        Response response = this.restClient.performRequest(...);

在单个 HTTP 请求后仔细检查 elasticsearch 服务器上的日志。您应该尝试在没有 @Autowired 注释的情况下实现单例模式,看看问题是否仍然存在。

【讨论】:

  • 根据 Elasticsearch 的文档,低级 RestClient 是线程安全的。我应该在日志中查找什么?
  • 他已经在使用@Autowired,并且在配置bean中创建了一次RestClient。
  • 那么,SearchController 是否只实例化了一次?如果没有,如果是单例,为什么不将restClient 设置为static
  • @PacificNW_Lover 在应用程序日志中,在一个 HTTP 请求之后,您应该看到启动了一个新的连接线程池。如果没有,那么您的应用程序可能会在其他地方泄漏资源。
【解决方案3】:

从您的堆栈跟踪来看,由于too many open files 错误,嵌入式 tomcat(您的应用程序容器)似乎不再能够接受新连接。从您的代码来看,elasticsearch rest client 似乎没有问题。

由于您在为您的搜索请求提供服务时重复使用 RestClient 的单个实例,因此与 ES 集群的打开连接可能不超过 30 个 (org.elasticsearch.client.RestClientBuilder.DEFAULT_MAX_CONN_TOTAL)。因此,RestClient 不太可能导致问题。

其他潜在的根本原因可能是您的服务使用者与您的 (tomcat) 服务器保持连接打开的时间更长,或者他们没有正确关闭连接。

我是否需要明确地将 finally 子句放在 SearchController 并显式关闭 RestClient 连接?

没有。你不应该。在关闭您的服务时应关闭休息客户端(在 @PreDestroy 方法中,因为您已经正确执行了)。

【讨论】:

    猜你喜欢
    • 2011-01-03
    • 2011-05-16
    • 1970-01-01
    • 2016-07-18
    • 2020-03-04
    • 2017-03-03
    • 2016-08-09
    • 2013-10-15
    相关资源
    最近更新 更多