Apache HttpClient是由Apache軟件基金會維護的一款開源HTTP客戶端庫,對比最基礎的 HttpURLConnection 而言,它的優勢時支持連接池管理,攔截器(Interceptor)機制,同步/異步請求支持等能力。
在使用這個組件時,需要格外注意連接池相關的配置,否則容易踩坑。
踩坑案例
問題
一個對外轉發請求的項目,部分渠道的對接使用了HttpClient來實現的,由于業務訪問量不大,上線后只部署了幾臺服務,前段時間三方平臺曝光量增加,導致業務量比平時多了一倍,隨后這個服務出現了問題:
從APM監控上看,上游調用該服務的請求有大量的超時,但是該服務只是請求轉發而已,從http組件監控看該服務調三方接口的請求的RT也有明顯增大,還有一部分請求出現了超時。但是該服務的CPU JVM資源指標都比較正常,而且項目中有多個平臺的對接業務,目前只有這個平臺的請求是有問題的。
排查過程
起初懷疑是網絡抖動,但是找運維看了說網絡延遲是正常的沒有抖動,即便如此,還是覺得是網絡不好導致請求hold住了(從apm看請求超時報錯時間都比較久應該是配置的不太合理),順著思路想著先擴容試試吧,擴容后發現起初是有效果的,但是過了一會兒又開始出現超時的請求了。
查到這感覺不像是網絡原因了,只能翻代碼了,隨后翻了一下代碼現狀和關于HttpClient的連接池配置資料,找到了問題的原因......
問題1:HttpClient的連接池只設置了全局最大連接MaxTotal,但是未設置單路由的最大連接defaultMaxPerRoute(默認只有2)。在并發情況下,同路由下的沒有空閑連接就會導致一直阻塞等待,直到獲取到連接才能進行請求,所以超時的請求其實是在等待獲取連接,并不是等待三方響應超時。
問題2:只有這個渠道的對接用了HttpClient,其他渠道直接用RestTemplate實現的。這就可以解釋為什么只有這個平臺的請求是有問題了
?
下面整理一下httpClient相關的配置,避免以后踩坑。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import tech.yummy.common.caja.tools.utils.BizThreadPoolUtils;import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;@Slf4j
public class HttpClientUtils {private static CloseableHttpClient httpClient;private static PoolingHttpClientConnectionManager clientConnectionManager;private static SSLConnectionSocketFactory sslConnectionSocketFactory;private static SSLContextBuilder sslContextBuilder;private static String encoding;private static final String HTTP = "http";private static final String HTTPS = "https";static {try {encoding = "UTF-8";Registry<ConnectionSocketFactory> registry = initConnectionSocketFactoryRegistry();clientConnectionManager = new PoolingHttpClientConnectionManager(registry);// 創建連接池(默認 maxTotal=20, defaultMaxPerRoute=2 validateAfterInactivity=2000)clientConnectionManager = new PoolingHttpClientConnectionManager(registry);//覆蓋默認配置 全局最大連接數 500clientConnectionManager.setMaxTotal(500);//覆蓋默認配置 每路由默認連接數 50clientConnectionManager.setDefaultMaxPerRoute(50);//覆蓋默認配置 連接在池中閑置多久后需要驗證其有效性 5秒clientConnectionManager.setValidateAfterInactivity(5000);RequestConfig requestConfig = RequestConfig.custom()//從連接池獲取連接的超時時間 - 連接池滿時會阻塞等待,超時拿不到鏈接會拋 ConnectionPoolTimeoutException.setConnectionRequestTimeout(2000)//建立TCP連接的超時時間(握手).setConnectTimeout(1000)//數據傳輸的間隔超時時間.setSocketTimeout(3000).build();httpClient = HttpClientBuilder.create().useSystemProperties().setConnectionManager(clientConnectionManager).setDefaultRequestConfig(requestConfig).build();} catch (Exception e) {log.error("初始化httpclient 配置執行異常", e);}}private static Registry<ConnectionSocketFactory> initConnectionSocketFactoryRegistry() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {sslContextBuilder = new SSLContextBuilder();// 全部信任 不做身份鑒定sslContextBuilder.loadTrustMaterial(null, new TrustStrategy() {@Overridepublic boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {return true;}});sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContextBuilder.build(),null,null,NoopHostnameVerifier.INSTANCE);Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register(HTTP, new PlainConnectionSocketFactory()).register(HTTPS, sslConnectionSocketFactory).build();return registry;}private static String request(HttpUriRequest request) throws IOException {ResponseHandler<String> responseHandler = response -> {int status = response.getStatusLine().getStatusCode();log.debug("response status:{}", status);HttpEntity entity = response.getEntity();return entity != null ? EntityUtils.toString(entity, encoding) : null;};log.debug("httpClient request:{} {}", request.getMethod(), request.getURI());String responseBody = httpClient.execute(request, responseHandler);log.debug("httpClient response:{}", responseBody);return responseBody;}//======================================================GET Start====================================================================public String get(String url, String params) throws IOException {if (!StringUtils.isBlank(params)) {url = url.concat("?").concat(params);}HttpGet httpGet = new HttpGet(url);return request(httpGet);}public static String get(String url, String params, Map<String, String> headers) throws IOException {if (!StringUtils.isBlank(params)) {url = url.concat("?").concat(params);}HttpGet httpGet = new HttpGet(url);if (headers != null) {headers.forEach(httpGet::setHeader);}return request(httpGet);}//======================================================GET End====================================================================//======================================================POST Start====================================================================/*** POST -> JSON 通用*/public static String post(String url, String rawContents, Map<String, String> headers) throws IOException {HttpPost httpPost = new HttpPost(url);HttpEntity entity = new StringEntity(rawContents, encoding);httpPost.setEntity(entity);if (headers != null) {headers.forEach(httpPost::setHeader);}return request(httpPost);}//======================================================POST End====================================================================public static void main(String[] args) throws IOException, InterruptedException {String url = "https://www.test.com";Map<String, String> headers = new HashMap<>();headers.put("Content-Type", "application/json");headers.put("charset", "UTF-8");headers.put("token", "ST-10384-3D0AQqHag-QqmKby4Upyu6YdB4f45fcc9-9qjdd");//參數String postParam = "{\"pageNum\":1,\"pageSize\":10}";for(int i = 0;i < 7;i++){String finalUrl = url;BizThreadPoolUtils.submit(() ->{long start = System.currentTimeMillis();try {String postResponse = HttpClientUtils.post(finalUrl, postParam, headers);log.info("請求耗時:{},返回值:{}",(System.currentTimeMillis() - start),postResponse);} catch (Exception e) {log.error("耗時:" + (System.currentTimeMillis() - start) + ",異常:" + e);}});}Thread.sleep(10000);String params = "page=1&size=10";String getResponse = HttpClientUtils.get(url, params, headers);System.out.println(getResponse);}}
?