簡體   English   中英

無法在Camel HTTP組件中配置“Keep Alive”

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

我正在為HTTP組件的正確設置遇到一些麻煩。 目前,微服務從提供商處提取JSON內容,處理它並將其發送到下一個服務以進行進一步處理。 主要問題是這個微服務創建了大量的CLOSE_WAIT套接字連接。 我知道“KEEP-ALIVE”的整個概念應保持連接打開,直到我關閉它,但服務器可能會因某些原因丟棄連接並創建此CLOSE_WAIT套接字。

我已經創建了一個用於調試/測試目的的小型服務,它可以向Google發送GET調用,但即使這個連接保持打開狀態,直到我關閉程序。 我嘗試了很多不同的解決方案:

  • .setHeader(“連接”,常量(“關閉”))
  • -Dhttp.keepAlive = false作為VM參數
  • 從Camel-Http切換到Camel-Http4
  • httpClient.soTimeout = 500(Camel-HTTP),httpClient.socketTimeout = 500和connectionTimeToLive = 500(Camel-HTTP4)
  • .setHeader(“Connection”,simple(“Keep-Alive”))和.setHeader(“Keep-Alive”,simple(“timeout = 10”))(Camel-HTTP4)
  • 設置通過調試DefaultConnectionKeepAliveStrategy的響應從-1(永不結束)到Camel-HTTP4中的特定值 - 這有效,但我無法注入自己的策略。

但我沒有成功。 所以也許你們中的一個可以幫助我:

  • 我怎么能告訴Camel-HTTP它應該在特定時間過后關閉連接? 例如,該服務每小時從內容提供商處提取。 3-4小時后,HttpComponent應在拉動后關閉連接,並在下一次拉動時重新打開。 目前,每個連接都將被放回MultiThreadedHttpConnectionManager,並且套接字仍處於打開狀態。
  • 如果使用Camel-HTTP無法做到這一點:我如何在創建路線時注入HttpClientBuilder? 我知道應該可以通過httpClient選項,但我不明白文檔的特定部分。

謝謝大家的幫助

您可以將自己的clientConnectionManager提供給HTTP4。 通常,您應該使用org.apache.http.impl.conn.PoolingHttpClientConnectionManager的實例,您可以使用自己的org.apache.http.config.SocketConfig將其配置為連接管理器的setDefaultSocketConfig方法。

如果您使用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;
}

然后你只需在端點定義中使用它,如下所示: clientConnectionManager=#connectionManager

如果空閑連接在配置的時間內空閑,則可以通過關閉空閑連接來完成。 您可以通過為Camel Http Component配置空閑連接超時來實現相同目的。 Camel Http提供了這樣做的界面。

將org.apache.camel.component.http4.HttpComponent轉換為PoolingHttpClientConnectionManager

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

        poolingClientConnectionManager.closeIdleConnections(5000, TimeUnit.MILLISECONDS);

訪問此處[ http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.html#closeIdleConnections(long,java.util.concurrent.TimeUnit )]

不幸的是,在應用程序最終關閉之前,所提出的答案都沒有解決我身邊的CLOSE_WAIT連接狀態。

我用以下測試用例重現了這個問題:

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);
  }
}

並發布netstat -ab -p tcp | grep 151.101.129.69 netstat -ab -p tcp | grep 151.101.129.69請求,其中IP是meta.stackoverflow.com

這給出了如下答復:

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

在調用之后跟隨

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

應用程序關閉之前的響應由於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());
  }
}

或直接在相應的HttpComponent上定義設置。

在檢查HttpClient代碼中各自提出的方法時,很明顯這些方法是單次操作,而不是HttpClient內部每隔幾毫秒檢查一次的配置。

PoolingHttpClientConnectionManager進一步說明:

版本4.4中更改了過時連接的處理。 以前,代碼會在重新使用之前默認檢查每個連接。 現在,代碼僅檢查連接,如果自上次使用連接以來經過的時間超過已設置的超時。 默認超時設置為2000毫秒

只有在重新使用連接時才會發生這種情況,這對連接池有意義,特別是如果通過同一連接交換多個消息。 對於單次調用,它應該更像是一個Connection: close可能在一段時間內沒有重用該連接,使連接保持打開或半關閉,因為沒有進一步嘗試從該連接讀取並因此識別本身可以關閉連接。

我注意到我已經用傳統的HttpClients解決了這個問題,並開始將這個解決方案移植到Camel,這很容易解決。

該解決方案基本上包括使用服務注冊HttpClients,然后定期(在我的情況下為5秒)調用closeExpiredConnections()closeIdleConnections(...)

這個邏輯保存在單例枚舉中,因為它實際上位於一個應用程序使用的庫中,每個應用程序都在自己的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;
    }
  }
}

如上所述,這個單例服務產生一個自己的線程,每隔5秒調用上面提到的兩個方法。 這些調用負責關閉在一段時間內未使用的連接或在規定的時間內閑置的連接。

為了使用這個服務,可以使用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;
  }
}

為了利用該服務,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();
}

最后但並非最不重要的是,在我的情況下, HttpClientSpringTestConfig中的httpConfiguration定義的連接管理器需要轉到引入的寄存器函數

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

這可能不是最漂亮的解決方案,但它會關閉我機器上的半封閉連接。


@編輯

我剛剛了解到你可以使用NoConnectionReuseStrategy將連接狀態更改為TIME_WAIT而不是CLOSE_WAIT ,因此會在短時間后刪除連接。 不幸的是,該請求仍然發出了一個Connection: keep-alive頭。 此策略將為每個請求創建一個新連接,即如果您有301 Moved Permanently重定向響應,則重定向將在新連接上發生。

httpClientConfigurer bean需要更改為以下內容才能使用上述策略:

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

首先是羅曼沃特納 ,你的回答以及你找到問題的純粹奉獻精神幫助了我一輛卡車。 我一直在與CLOSE_WAIT斗爭2天,你的回答是有幫助的。 這就是我做的。 在我的CamelConfiguration類中添加了以下代碼,該類在啟動時基本上篡改了CamelContext。

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

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

工作就像一個魅力。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM