简体   繁体   English

无法在Camel HTTP组件中配置“Keep Alive”

[英]Unable to configure “Keep Alive” in Camel HTTP component

I'm having some troubles with the right setup of the HTTP component. 我正在为HTTP组件的正确设置遇到一些麻烦。 Currently a microservice pulls JSON Content from a provider, process it and send it to the next service for further processes. 目前,微服务从提供商处提取JSON内容,处理它并将其发送到下一个服务以进行进一步处理。 The main problem is that this microservice create a ton of CLOSE_WAIT socket connections. 主要问题是这个微服务创建了大量的CLOSE_WAIT套接字连接。 I understand that the whole concept of "KEEP-ALIVE" shall keep the connection open until I close it, but it's possible that the server will drop the connection for some reasons and creates this CLOSE_WAIT socket. 我知道“KEEP-ALIVE”的整个概念应保持连接打开,直到我关闭它,但服务器可能会因某些原因丢弃连接并创建此CLOSE_WAIT套接字。

I've created a small service for debugging / testing purposes which make a GET Call to Google, but even this connection stays open until i close the program. 我已经创建了一个用于调试/测试目的的小型服务,它可以向Google发送GET调用,但即使这个连接保持打开状态,直到我关闭程序。 I've tried many different solutions: 我尝试了很多不同的解决方案:

  • .setHeader("Connection", constant("Close")) .setHeader(“连接”,常量(“关闭”))
  • -Dhttp.keepAlive=false as VM argument -Dhttp.keepAlive = false作为VM参数
  • Switching from Camel-Http to Camel-Http4 从Camel-Http切换到Camel-Http4
  • httpClient.soTimeout=500 (Camel-HTTP), httpClient.socketTimeout=500 and connectionTimeToLive=500 (Camel-HTTP4) httpClient.soTimeout = 500(Camel-HTTP),httpClient.socketTimeout = 500和connectionTimeToLive = 500(Camel-HTTP4)
  • .setHeader("Connection", simple("Keep-Alive")) and .setHeader("Keep-Alive", simple("timeout=10")) (Camel-HTTP4) .setHeader(“Connection”,simple(“Keep-Alive”))和.setHeader(“Keep-Alive”,simple(“timeout = 10”))(Camel-HTTP4)
  • Setting via debugging the response of DefaultConnectionKeepAliveStrategy from -1 (never ending) to a specific value in Camel-HTTP4 - that works but I was not able to inject my own strategy. 设置通过调试DefaultConnectionKeepAliveStrategy的响应从-1(永不结束)到Camel-HTTP4中的特定值 - 这有效,但我无法注入自己的策略。

but i had no success. 但我没有成功。 So maybe one of you can help me: 所以也许你们中的一个可以帮助我:

  • How can i tell the Camel-HTTP that it should close a connection when a specific time is passed? 我怎么能告诉Camel-HTTP它应该在特定时间过后关闭连接? For example, the service pulls every hour from the content provider. 例如,该服务每小时从内容提供商处提取。 After 3-4 hours the HttpComponent should close the connection after the pull and reopen it when the next pull is there. 3-4小时后,HttpComponent应在拉动后关闭连接,并在下一次拉动时重新打开。 Currently every connection would be put back into the MultiThreadedHttpConnectionManager and the socket is still open. 目前,每个连接都将被放回MultiThreadedHttpConnectionManager,并且套接字仍处于打开状态。
  • If it's not possible to do that with Camel-HTTP: How can i inject a HttpClientBuilder into the Creation of my route? 如果使用Camel-HTTP无法做到这一点:我如何在创建路线时注入HttpClientBuilder? I know that it should be possible via httpClient option but I don't understand that specific part of the documentation. 我知道应该可以通过httpClient选项,但我不明白文档的特定部分。

Thank you all for your help 谢谢大家的帮助

You can provide your own clientConnectionManager to HTTP4. 您可以将自己的clientConnectionManager提供给HTTP4。 Generally you should use an instance of org.apache.http.impl.conn.PoolingHttpClientConnectionManager , which you'd configure with your own org.apache.http.config.SocketConfig by passing it to setDefaultSocketConfig method of the connection manager. 通常,您应该使用org.apache.http.impl.conn.PoolingHttpClientConnectionManager的实例,您可以使用自己的org.apache.http.config.SocketConfig将其配置为连接管理器的setDefaultSocketConfig方法。

If you're using Spring with Java config, you would have a method: 如果您使用Spring with Java config,那么您将拥有一个方法:

@Bean
PoolingHttpClientConnectionManager connectionManager() {
    SocketConfig socketConfig = SocketConfig.custom()
            .setSoKeepAlive(false)
            .setSoReuseAddress(true)
            .build();
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
    connectionManager.setDefaultSocketConfig(socketConfig);
    return connectionManager;
}

and then you'd just use it in your endpoint definition like so: clientConnectionManager=#connectionManager 然后你只需在端点定义中使用它,如下所示: clientConnectionManager=#connectionManager

It can be done by closing idle connections if they are idle for configured time. 如果空闲连接在配置的时间内空闲,则可以通过关闭空闲连接来完成。 You can achieve same by configuring idle connection timeout for Camel Http Component. 您可以通过为Camel Http Component配置空闲连接超时来实现相同目的。 Camel Http provide interface to do so. Camel Http提供了这样做的界面。

Cast org.apache.camel.component.http4.HttpComponent to PoolingHttpClientConnectionManager 将org.apache.camel.component.http4.HttpComponent转换为PoolingHttpClientConnectionManager

        PoolingHttpClientConnectionManager poolingClientConnectionManager = (PoolingHttpClientConnectionManager) httpComponent
                .getClientConnectionManager();

        poolingClientConnectionManager.closeIdleConnections(5000, TimeUnit.MILLISECONDS);

Visit Here [ http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.html#closeIdleConnections(long , java.util.concurrent.TimeUnit)] 访问此处[ http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.html#closeIdleConnections(long,java.util.concurrent.TimeUnit )]

Unfortunately none of the proposed answers solved the CLOSE_WAIT connection status on my side until the application finally was closed. 不幸的是,在应用程序最终关闭之前,所提出的答案都没有解决我身边的CLOSE_WAIT连接状态。

I reproduced this problem with the following test case: 我用以下测试用例重现了这个问题:

public class HttpInvokationTest extends CamelSpringTestSupport {

  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  @EndpointInject(uri = "mock:success")
  private MockEndpoint successEndpoint;
  @EndpointInject(uri = "mock:failure")
  private MockEndpoint failureEndpoint;

  @Override
  protected AbstractApplicationContext createApplicationContext() {
    return new AnnotationConfigApplicationContext(ContextConfig.class);
  }

  @Configuration
  @Import(HttpClientSpringTestConfig.class)
  public static class ContextConfig extends CamelConfiguration {

    @Override
    public List<RouteBuilder> routes() {
      List<RouteBuilder> routes = new ArrayList<>(1);
      routes.add(new RouteBuilder() {
        @Override
        public void configure() {
          from("direct:start")
            .log(LoggingLevel.INFO, LOG, CONFIDENTIAL, "Invoking external URL: ${header[ERPEL_URL]}")
            .setHeader("Connection", constant("close"))
            .recipientList(header("TEST_URL"))
            .log(LoggingLevel.DEBUG, "HTTP response code: ${header["+Exchange.HTTP_RESPONSE_CODE+"]}")
            .bean(CopyBodyToHeaders.class)
            .choice()
              .when(header(Exchange.HTTP_RESPONSE_CODE).isGreaterThanOrEqualTo(300))
                .to("mock:failure")
              .otherwise()
                .to("mock:success");
        }
      });
      return routes;
    }
  }

  @Test
  public void testHttpInvocation() throws Exception {
    successEndpoint.expectedMessageCount(1);
    failureEndpoint.expectedMessageCount(0);

    ProducerTemplate template = context.createProducerTemplate();

    template.sendBodyAndHeader("direct:start", null, "TEST_URL", "http4://meta.stackoverflow.com");

    successEndpoint.assertIsSatisfied();
    failureEndpoint.assertIsSatisfied();

    Exchange exchange = successEndpoint.getExchanges().get(0);
    Map<String, Object> headers = exchange.getIn().getHeaders();
    String body = exchange.getIn().getBody(String.class);
    for (String key : headers.keySet()) {
      LOG.info("Header: {} -> {}", key, headers.get(key));
    }
    LOG.info("Body: {}", body);

    Thread.sleep(120000);
  }
}

and issuing netstat -ab -p tcp | grep 151.101.129.69 并发布netstat -ab -p tcp | grep 151.101.129.69 netstat -ab -p tcp | grep 151.101.129.69 requests, where the IP is the one of meta.stackoverflow.com . netstat -ab -p tcp | grep 151.101.129.69请求,其中IP是meta.stackoverflow.com

This gave responses like: 这给出了如下答复:

tcp4       0      0  192.168.0.10.52183     151.101.129.69.https   ESTABLISHED      37562       2118
tcp4       0      0  192.168.0.10.52182     151.101.129.69.http    ESTABLISHED        885        523

right after the invocation followeb by 在调用之后跟随

tcp4       0      0  192.168.0.10.52183     151.101.129.69.https   CLOSE_WAIT       37562       2118
tcp4       0      0  192.168.0.10.52182     151.101.129.69.http    CLOSE_WAIT         885        523

responses until the application was closed due to the Connection: keep-alive header even with a configuration like the one below: 应用程序关闭之前的响应由于Connection: keep-alive标头,即使配置如下所示:

@Configuration
@EnableConfigurationProperties(HttpClientSettings.class)
public class HttpClientSpringTestConfig {

  private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  @Resource
  private HttpClientSettings httpClientSettings;

  @Resource
  private CamelContext camelContext;

  private SocketConfig httpClientSocketConfig() {
    /*
      socket timeout:
      Monitors the time passed between two consecutive incoming messages over the connection and
      raises a SocketTimeoutException if no message was received within the given timeout interval
     */
    LOG.info("Creating a SocketConfig with a socket timeout of {} seconds", httpClientSettings.getSoTimeout());
    return SocketConfig.custom()
        .setSoTimeout(httpClientSettings.getSoTimeout() * 1000)
        .setSoKeepAlive(false)
        .setSoReuseAddress(false)
        .build();
  }

  private RequestConfig httpClientRequestConfig() {
    /*
      connection timeout:
      The time span the application will wait for a connection to get established. If the connection
      is not established within the given amount of time a ConnectionTimeoutException will be raised.
     */
    LOG.info("Creating a RequestConfig with a socket timeout of {} seconds and a connection timeout of {} seconds",
             httpClientSettings.getSoTimeout(), httpClientSettings.getConTimeout());
    return RequestConfig.custom()
        .setConnectTimeout(httpClientSettings.getConTimeout() * 1000)
        .setSocketTimeout(httpClientSettings.getSoTimeout() * 1000)
        .build();
  }

  @Bean(name = "httpClientConfigurer")
  public HttpClientConfigurer httpConfiguration() {
    ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
      @Override
      public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        return 5 * 1000;
      }
    };

    PoolingHttpClientConnectionManager conMgr =
        new PoolingHttpClientConnectionManager();
    conMgr.closeIdleConnections(5, TimeUnit.SECONDS);

    return builder -> builder.setDefaultSocketConfig(httpClientSocketConfig())
                             .setDefaultRequestConfig(httpClientRequestConfig())
                             .setConnectionTimeToLive(5, TimeUnit.SECONDS)
                             .setKeepAliveStrategy(myStrategy)
                             .setConnectionManager(conMgr);
  }

  @PostConstruct
  public void init() {
    LOG.debug("Initializing HTTP clients");
    HttpComponent httpComponent = camelContext.getComponent("http4", HttpComponent.class);
    httpComponent.setHttpClientConfigurer(httpConfiguration());
    HttpComponent httpsComponent = camelContext.getComponent("https4", HttpComponent.class);
    httpsComponent.setHttpClientConfigurer(httpConfiguration());
  }
}

or defining the settings directly on the respective HttpComponent. 或直接在相应的HttpComponent上定义设置。

On examining the respective proposed methods in the HttpClient code it gets obvious that these methods are single-shot operations and not configurations that HttpClient internally will check every few milliseconds itself. 在检查HttpClient代码中各自提出的方法时,很明显这些方法是单次操作,而不是HttpClient内部每隔几毫秒检查一次的配置。

PoolingHttpClientConnectionManager states further that: PoolingHttpClientConnectionManager进一步说明:

The handling of stale connections was changed in version 4.4. 版本4.4中更改了过时连接的处理。 Previously, the code would check every connection by default before re-using it. 以前,代码会在重新使用之前默认检查每个连接。 The code now only checks the connection if the elapsed time since the last use of the connection exceeds the timeout that has been set. 现在,代码仅检查连接,如果自上次使用连接以来经过的时间超过已设置的超时。 The default timeout is set to 2000ms 默认超时设置为2000毫秒

which only occurs if an attempt is done on re-using a connection, which makes sense for a connection pool, especially if multiple messages are exchanged via the same connection. 只有在重新使用连接时才会发生这种情况,这对连接池有意义,特别是如果通过同一连接交换多个消息。 For single-shot invocations, that should more behave like a Connection: close there might not be a reuse of that connection for some time, leaving the connection open or half-closed as no further attempt is done to read from that connection and therefore recognizing itself that the connection could be closed. 对于单次调用,它应该更像是一个Connection: close可能在一段时间内没有重用该连接,使连接保持打开或半关闭,因为没有进一步尝试从该连接读取并因此识别本身可以关闭连接。

I noticed that I already solved such an issue a while back with traditional HttpClients and started to port this solution to Camel, which worked out quite easily. 我注意到我已经用传统的HttpClients解决了这个问题,并开始将这个解决方案移植到Camel,这很容易解决。

The solution basically consists of registering HttpClients with a service and then periodically (5 seconds in my case) call closeExpiredConnections() and closeIdleConnections(...) . 该解决方案基本上包括使用服务注册HttpClients,然后定期(在我的情况下为5秒)调用closeExpiredConnections()closeIdleConnections(...)

This logic is kept in a singleton enum, as this is actually in a library that a couple of applications use, each running in their own JVM. 这个逻辑保存在单例枚举中,因为它实际上位于一个应用程序使用的库中,每个应用程序都在自己的JVM中运行。

/**
 * This singleton monitor will check every few seconds for idle and stale connections and perform
 * a cleanup on the connections using the registered connection managers.
 */
public enum IdleConnectionMonitor {

  INSTANCE;

  private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  /** The execution service which runs the cleanup every 5 seconds **/
  private ScheduledExecutorService executorService =
      Executors.newScheduledThreadPool(1, new NamingThreadFactory());
  /** The actual thread which performs the monitoring **/
  private IdleConnectionMonitorThread monitorThread = new IdleConnectionMonitorThread();

  IdleConnectionMonitor() {
    // execute the thread every 5 seconds till the application is shutdown (or the shutdown method
    // is invoked)
    executorService.scheduleAtFixedRate(monitorThread, 5, 5, TimeUnit.SECONDS);
  }

  /**
   * Registers a {@link HttpClientConnectionManager} to monitor for stale connections
   */
  public void registerConnectionManager(HttpClientConnectionManager connMgr) {
    monitorThread.registerConnectionManager(connMgr);
  }

  /**
   * Request to stop the monitoring for stale HTTP connections.
   */
  public void shutdown() {
    executorService.shutdown();
    try {
      if (!executorService.awaitTermination(3, TimeUnit.SECONDS)) {
        LOG.warn("Connection monitor shutdown not finished after 3 seconds!");
      }
    } catch (InterruptedException iEx) {
      LOG.warn("Execution service was interrupted while waiting for graceful shutdown");
    }
  }

  /**
   * Upon invocation, the list of registered connection managers will be iterated through and if a
   * referenced object is still reachable {@link HttpClientConnectionManager#closeExpiredConnections()}
   * and {@link HttpClientConnectionManager#closeIdleConnections(long, TimeUnit)} will be invoked
   * in order to cleanup stale connections.
   * <p/>
   * This runnable implementation holds a weakly referable list of {@link
   * HttpClientConnectionManager} objects. If a connection manager is only reachable by {@link
   * WeakReference}s or {@link PhantomReference}s it gets eligible for garbage collection and thus
   * may return null values. If this is the case, the connection manager will be removed from the
   * internal list of registered connection managers to monitor.
   */
  private static class IdleConnectionMonitorThread implements Runnable {

    // we store only weak-references to connection managers in the list, as the lifetime of the
    // thread may extend the lifespan of a connection manager and thus allowing the garbage
    // collector to collect unused objects as soon as possible
    private List<WeakReference<HttpClientConnectionManager>> registeredConnectionManagers =
        Collections.synchronizedList(new ArrayList<>());

    @Override
    public void run() {

      LOG.trace("Executing connection cleanup");
      Iterator<WeakReference<HttpClientConnectionManager>> conMgrs =
          registeredConnectionManagers.iterator();
      while (conMgrs.hasNext()) {
        WeakReference<HttpClientConnectionManager> weakConMgr = conMgrs.next();
        HttpClientConnectionManager conMgr = weakConMgr.get();
        if (conMgr != null) {
          LOG.trace("Found connection manager: {}", conMgr);
          conMgr.closeExpiredConnections();
          conMgr.closeIdleConnections(30, TimeUnit.SECONDS);
        } else {
          conMgrs.remove();
        }
      }
    }

    void registerConnectionManager(HttpClientConnectionManager connMgr) {
      registeredConnectionManagers.add(new WeakReference<>(connMgr));
    }
  }

  private static class NamingThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
      Thread t = new Thread(r);
      t.setName("Connection Manager Monitor");
      return t;
    }
  }
}

As mentioned, this singleton service spawns an own thread that invokes the two, above mentioned methods every 5 seconds. 如上所述,这个单例服务产生一个自己的线程,每隔5秒调用上面提到的两个方法。 These invocations take care of closing connections that are either unused for a certain amount of time or that are IDLE for the stated amount of time. 这些调用负责关闭在一段时间内未使用的连接或在规定的时间内闲置的连接。

In order to camelize this service EventNotifierSupport can be utilized in order to let Camel take care of shutting down the monitor thread once it is closing down. 为了使用这个服务,可以使用EventNotifierSupport ,以便让Camel在关闭后关闭监视器线程。

/**
 * This Camel service with take care of the lifecycle management of {@link IdleConnectionMonitor} 
 * and invoke {@link IdleConnectionMonitor#shutdown()} once Camel is closing down in order to stop
 * listening for stale connetions.
 */
public class IdleConnectionMonitorService extends EventNotifierSupport {

  private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  private IdleConnectionMonitor connectionMonitor;

  @Override
  public void notify(EventObject event) {
    if (event instanceof CamelContextStartedEvent) {
      LOG.info("Start listening for closable HTTP connections");
      connectionMonitor = IdleConnectionMonitor.INSTANCE;
    } else if (event instanceof CamelContextStoppingEvent){
      LOG.info("Shutting down listener for open HTTP connections");
      connectionMonitor.shutdown();
    }
  }

  @Override
  public boolean isEnabled(EventObject event) {
    return event instanceof CamelContextStartedEvent || event instanceof CamelContextStoppingEvent;
  }

  public IdleConnectionMonitor getConnectionMonitor() {
    return this.connectionMonitor;
  }
}

In order to take advantage of that service, the connection manager that is used by the HttpClient Camel uses internally needs to be registered with the service, which is done in the code block below: 为了利用该服务,HttpClient Camel使用的内部连接管理器需要在服务中注册,这在下面的代码块中完成:

private void registerHttpClientConnectionManager(HttpClientConnectionManager conMgr) {
  if (!getIdleConnectionMonitorService().isPresent()) {
    // register the service with Camel so that on a shutdown the monitoring thread will be stopped
    camelContext.getManagementStrategy().addEventNotifier(new IdleConnectionMonitorService());
  }
  IdleConnectionMonitor.INSTANCE.registerConnectionManager(conMgr);
}

private Optional<IdleConnectionMonitorService> getIdleConnectionMonitorService() {
  for (EventNotifier eventNotifier : camelContext.getManagementStrategy().getEventNotifiers()) {
    if (eventNotifier instanceof IdleConnectionMonitorService) {
      return Optional.of((IdleConnectionMonitorService) eventNotifier);
    }
  }
  return Optional.empty();
}

Last but not least the connection manager defined in httpConfiguration inside the HttpClientSpringTestConfig in my case needed to be past to the introduced register function 最后但并非最不重要的是,在我的情况下, HttpClientSpringTestConfig中的httpConfiguration定义的连接管理器需要转到引入的寄存器函数

PoolingHttpClientConnectionManager conMgr = new PoolingHttpClientConnectionManager();
registerHttpClientConnectionManager(conMgr);

This might not be the prettiest solution, but it does close the half-closed connections on my machine. 这可能不是最漂亮的解决方案,但它会关闭我机器上的半封闭连接。


@edit @编辑

I just learned that you can use a NoConnectionReuseStrategy which changes the connection state to TIME_WAIT rather than CLOSE_WAIT and therefore removes the connection after a short moment. 我刚刚了解到你可以使用NoConnectionReuseStrategy将连接状态更改为TIME_WAIT而不是CLOSE_WAIT ,因此会在短时间后删除连接。 Unfortunately, the request is still issued with a Connection: keep-alive header. 不幸的是,该请求仍然发出了一个Connection: keep-alive头。 This strategy will create a new connection per request, ie if you've got a 301 Moved Permanently redirect response the redirect would occur on a new connection. 此策略将为每个请求创建一个新连接,即如果您有301 Moved Permanently重定向响应,则重定向将在新连接上发生。

The httpClientConfigurer bean would need to change to the following in order to make use of the above mentioned strategy: httpClientConfigurer bean需要更改为以下内容才能使用上述策略:

@Bean(name = "httpClientConfigurer")
public HttpClientConfigurer httpConfiguration() {
    return builder -> builder.setDefaultSocketConfig(socketConfig)
        .setDefaultRequestConfig(requestConfig)
        .setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE);
}

Firstly Roman Vottner , your answer and just your sheer dedication to finding the issue helped me a truckload. 首先是罗曼沃特纳 ,你的回答以及你找到问题的纯粹奉献精神帮助了我一辆卡车。 I have been struggling with the CLOSE_WAIT for 2 days now and your answer was what helped. 我一直在与CLOSE_WAIT斗争2天,你的回答是有帮助的。 Here is what I did. 这就是我做的。 Added the following code in my CamelConfiguration class which essentially tampers with CamelContext at startup. 在我的CamelConfiguration类中添加了以下代码,该类在启动时基本上篡改了CamelContext。

    HttpComponent http4 = camelContext.getComponent("https4", HttpComponent.class);
    http4.setHttpClientConfigurer(new HttpClientConfigurer() {

        @Override
        public void configureHttpClient(HttpClientBuilder builder) {
            builder.setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE);
        }
    });

Worked like a charm. 工作就像一个魅力。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM