Search results for 'PoolingHttpClientConnectionManager'

PoolingHttpClientConnectionManager 를 xml기반 Spring bean으로 등록하기.

개발한 서비스 중 commons httpclient 로 서버 <-> 서버 간 rest api 호출하는 부분에  부분에서 간헐적으로 NoHttpResponseException : The target server failed to respond 예외가 발생하기 시작했다. 이를 해결하기위해 자료를 좀 찾아봤더니 HttpClient 4.4에서 존재하던 버그였고 4.4.1에서 해결된 문제( https://issues.apache.org/jira/browse/HTTPCLIENT-1610 )라고 하는데... 개발한 서비스에는 4.5.1을 쓰는데?

현상의 이유는 HTTP/1.1의 Keep-Alive로 인해 httpclient는 통신이 끝난 connection을 종료하지 않고 동일host:port에 대해 동일한 커넥션을 이용하려하기 때문이다.

비록 서버측은 통신이 완료되어 해당 연결을 close 할지라도 client 측은 커넥션 객체가 여전히 열여있고 데이터가 인입되길 기다리고 있게된다.  ( close()의 실제 의미는 소켓의 단절이 아닌 "나는 더 이상 보낼 데이터가 없습니다."로 상대측에서는 해당 커넥션을 단절하지 않는 이상 여전히 데이터가 인입될 수 있음을 의미한다. )
이때를 half-closed connection 으로 표현하며 이는 TCP가 그렇게 동작하게끔 설계되었기 때문으로 버그가 아니다. 이런 상황이 되면 JVM상의 connection 객체는 당연히 살아있지만 내부 소켓은 CLOSE_WAIT 상태가 된다.

문제는 httpclient가 이 CLOSE_WAIT 상태에있는 connection 객체를 다시 사용하려고 할때 앞에서 설명한것과 같이 서버 측은 이미 연결을 끊어버렸기 때문에 NoHttpResponseException - The target server failed to respond 예외를 발생한다.
이를 해결하기위해서는 httpclient의 connectionManager에서 통신이 완료된 connection을 적절하게 제거할 필요가 있다.

Spring 4.X 환경에서 commons HttpClient 4.5.x 를 소켓 설정과 KeepAlive 설정등을 포함하여 xml로 설정하는 방법.

PoolingHttpClientConnectionManager를 통하여 CloseableHttpClient 를 사용하는 과정에 ConnectionManager의 closeIdleConnections 설정 통해 특정시간 idle인 커넥션을 종료하고 싶은 경우 java 코드가 아닌 spring xml 설정정으로 bean을 등록하려 할 때. 

사용은 당연히
@Autowired
private CloseableHttpClient httpClient;

 <!-- ===================================================================== -->
 <!-- =======================   HttpClient 4.5.X   ======================== -->
 <!-- ===================================================================== -->        
 <bean id="requestConfigBuilder" class="org.apache.http.client.config.RequestConfig" factory-method="custom">
         <property name="socketTimeout" value="10000" /> 
         <property name="connectTimeout" value="12000" /> 
         <property name="connectionRequestTimeout" value="12000" />
 </bean>
 
 <bean id="requestConfig" factory-bean="requestConfigBuilder" factory-method="build" />
 
 <bean id="socketConfigBuilder" class="org.apache.http.config.SocketConfig" factory-method="custom">
     <!-- 소켓이 연결된후 InputStream에서 읽을때 timeout -->
     <property name="soTimeout" value="10000" /> 
     <!-- SO_KEEPALIVE를 활성화 할 경우 소켓 내부적으로 일정시간 간격으로 heartbeat을 전송하여, 비정상적인 세션 종료에 대해 감지.
     unix 계열 : /etc/sysctl.conf
     windows : \HKEY_LOCAL_MACHINE\SystemCurrentControlSet\Services\TCPIP\Parameters
     -->
     <property name="soKeepAlive" value="true" /> 
     <!-- 비정상종료된 상태에서 아직 커널이 소켓의 bind정보를 유지하고 있을 때 해당 소켓을 재사용 할 수 있도록 -->
     <property name="soReuseAddress" value="true" /> 
     <!-- nagle 알고리즘 적용 여부 -->
     <property name="tcpNoDelay" value="true" /> 
     <!-- socket이 close 될 때 버퍼에 남아 있는 데이터를 보내는데 기다려주는 시간(blocked)-->
     <property name="soLinger" value="2000" /> 
 </bean>
 
 <bean id="poolingHttpClientConnectionManager" class="org.apache.http.impl.conn.PoolingHttpClientConnectionManager" destroy-method="shutdown">
     <constructor-arg value="2000" type="long" index="0" /> <!-- pool에 있는 커넥션 제거 idle time -->
     <constructor-arg value="MILLISECONDS" type="java.util.concurrent.TimeUnit" index="1" />
     <property name="maxTotal" value="60" />
     <property name="defaultMaxPerRoute" value="15" />
     <property name="defaultSocketConfig"><bean factory-bean="socketConfigBuilder" factory-method="build" /></property>
 </bean>
 
 <bean id="connectionKeepAliveStrategy" class="com.http.client.HttpShortKeepAliveStrategy" />
 
 <bean id="httpClientBuilder" class="org.apache.http.impl.client.HttpClientBuilder" factory-method="create">            
     <property name="defaultRequestConfig" ref="requestConfig" />
     <property name="connectionManager" ref="poolingHttpClientConnectionManager" />
     <property name="userAgent" value="Mozilla/5.0 (Windows NT 6.1; WOW64) CUSTOM-CLIENT" />
     <property name="keepAliveStrategy" ref="connectionKeepAliveStrategy" />
 </bean>

 <bean id="httpClient" factory-bean="httpClientBuilder" factory-method="build" destroy-method="close" />
 


HttpShortKeepAliveStrategy 클래스는...
 package com.http.client;
 
 import org.apache.http.HeaderElement;
 import org.apache.http.HeaderElementIterator;
 import org.apache.http.HttpResponse;
 import org.apache.http.conn.ConnectionKeepAliveStrategy;
 import org.apache.http.message.BasicHeaderElementIterator;
 import org.apache.http.protocol.HTTP;
 import org.apache.http.protocol.HttpContext;
 
 /**
  * HttpShortKeepAliveStrategy (UTF-8) created : 2016. 5. 25
  *
 
  */
 public class HttpShortKeepAliveStrategy implements ConnectionKeepAliveStrategy {
 
     /**
      *
      * @param response
      * @param context
      * @return
      */
     @Override
     public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
         // Honor 'keep-alive' header
         HeaderElementIterator it = new BasicHeaderElementIterator(
                 response.headerIterator(HTTP.CONN_KEEP_ALIVE));
         while (it.hasNext()) {
             HeaderElement he = it.nextElement();
             String param = he.getName();
             String value = he.getValue();
             if (value != null && param.equalsIgnoreCase("timeout")) {
                 try {
                     return Long.parseLong(value) * 100;
                 } catch (NumberFormatException ignore) {
                 }
             }
         }
 
 
         HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
         if ("www.mydomain.com".equalsIgnoreCase(target.getHostName())) {
             // Keep alive for 5 seconds only
             return 5 * 1000;
         } else {
             // otherwise keep alive for 1 seconds
             return 1 * 1000;
         }
     }
 
 }
 


코드로 표현하면 대강 다음과 같음.
static final CloseableHttpClient httpClient;
 static {
         PoolingHttpClientConnectionManager pooledManager = new PoolingHttpClientConnectionManager(20L,java.util.concurrent.TimeUnit.MILLISECONDS);
         pooledManager.setMaxTotal(15);
         pooledManager.setDefaultMaxPerRoute(5);
         pooledManager.closeIdleConnections(20L, TimeUnit.MILLISECONDS);
         pooledManager.setDefaultSocketConfig(SocketConfig.custom()
                 // nagle 알고리즘 적용 여부
                 .setTcpNoDelay(true)
                 // SO_KEEPALIVE를 활성화 할 경우 소켓 내부적으로 일정시간 간격으로 heartbeat을 전송하여, 비정상적인 세션 종료에 대해 감지.
                 // unix 계열 : /etc/sysctl.conf
                 // windows : \HKEY_LOCAL_MACHINE\SystemCurrentControlSet\Services\TCPIP\Parameters
                 .setSoKeepAlive(true)
                 // socket이 close 될 때 버퍼에 남아 있는 데이터를 보내는데 기다려주는 시간(blocked)
                 .setSoLinger(200)
                 // 비정상종료된 상태에서 아직 커널이 소켓의 bind정보를 유지하고 있을 때 해당 소켓을 재사용 할 수 있도록
                 .setSoReuseAddress(true)
                 //소켓이 연결된후 InputStream에서 읽을때 timeout
                 .setSoTimeout(10000)
                 .build()
         );
         httpClient = HttpClients.custom()
                         .setConnectionManager(pooledManager)
                         .setUserAgent("Mozilla/5.0 (Windows NT 6.1; WOW64) API-CLIENT")
                         .setRedirectStrategy(new DefaultRedirectStrategy() {
                                 @Override
                                 public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) {
                                         boolean isRedirect = false;
                                         try {
                                                 isRedirect = super.isRedirected(request, response, context);
                                         } catch (ProtocolException e) {
                                                 logger.error(null, e);
                                         }
                                         if (!isRedirect) {
                                                 int responseCode = response.getStatusLine().getStatusCode();
                                                 if (responseCode == 301 || responseCode == 302) {
                                                         return true;
                                                 }
                                         }
                                         return false;
                                 }
                         })
                         .setKeepAliveStrategy(new ConnectionKeepAliveStrategy() {
                                 @Override
                                 public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                                         // Honor 'keep-alive' header
                                         HeaderElementIterator it = new BasicHeaderElementIterator(
                                                         response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                                         while (it.hasNext()) {
                                                 HeaderElement he = it.nextElement();
                                                 String param = he.getName();
                                                 String value = he.getValue();
                                                 if (value != null && param.equalsIgnoreCase("timeout")) {
                                                         try {
                                                                 return Long.parseLong(value) * 100;
                                                         } catch (NumberFormatException ignore) {
                                                         }
                                                 }
                                         }
                                         HttpHost target = (HttpHost) context.getAttribute(
                                                         HttpClientContext.HTTP_TARGET_HOST);
                                         if ("www.mydomain.com".equalsIgnoreCase(target.getHostName())) {
                                                 // Keep alive for 5 seconds only
                                                 return 5 * 1000;
                                         } else {
                                                 // otherwise keep alive for 0.1 seconds
                                                 return 1 * 100;
                                         }
                                 }
                         })
                         .build();
 }
 


2016/05/25 15:28 2016/05/25 15:28
이 글의 관련글
    이글의 태그와 관련된 글이 없습니다.
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다