高并發(fā)場景下的httpClient優(yōu)化使用

1.背景
我們有個業(yè)務(wù)变屁,會調(diào)用其他部門提供的一個基于http的服務(wù)眼俊,日調(diào)用量在千萬級別。使用了httpclient來完成業(yè)務(wù)粟关。之前因為qps上不去疮胖,就看了一下業(yè)務(wù)代碼,并做了一些優(yōu)化闷板,記錄在這里澎灸。

先對比前后:優(yōu)化之前,平均執(zhí)行時間是250ms遮晚;優(yōu)化之后性昭,平均執(zhí)行時間是80ms,降低了三分之二的消耗县遣,容器不再動不動就報警線程耗盡了糜颠,清爽~

2.分析
項目的原實現(xiàn)比較粗略,就是每次請求時初始化一個httpclient萧求,生成一個httpPost對象其兴,執(zhí)行,然后從返回結(jié)果取出entity夸政,保存成一個字符串元旬,最后顯式關(guān)閉response和client。我們一點點分析和優(yōu)化:

2.1 httpclient反復(fù)創(chuàng)建開銷
httpclient是一個線程安全的類守问,沒有必要由每個線程在每次使用時創(chuàng)建匀归,全局保留一個即可。

2.2 反復(fù)創(chuàng)建tcp連接的開銷
tcp的三次握手與四次揮手兩大裹腳布過程耗帕,對于高頻次的請求來說朋譬,消耗實在太大。試想如果每次請求我們需要花費5ms用于協(xié)商過程兴垦,那么對于qps為100的單系統(tǒng)徙赢,1秒鐘我們就要花500ms用于握手和揮手字柠。又不是高級領(lǐng)導(dǎo),我們程序員就不要搞這么大做派了狡赐,改成keep alive方式以實現(xiàn)連接復(fù)用窑业!

2.3 重復(fù)緩存entity的開銷
原本的邏輯里,使用了如下代碼:

HttpEntity entity = httpResponse.getEntity();
String response = EntityUtils.toString(entity);

這里我們相當(dāng)于額外復(fù)制了一份content到一個字符串里枕屉,而原本的httpResponse仍然保留了一份content常柄,需要被consume掉,在高并發(fā)且content非常大的情況下搀擂,會消耗大量內(nèi)存西潘。并且,我們需要顯式的關(guān)閉連接哨颂,ugly喷市。

3.實現(xiàn)
按上面的分析,我們主要要做三件事:一是單例的client威恼,二是緩存的逼沸眨活連接,三是更好的處理返回結(jié)果箫措。一就不說了腹备,來說說二。

提到連接緩存斤蔓,很容易聯(lián)想到數(shù)據(jù)庫連接池植酥。httpclient4提供了一個PoolingHttpClientConnectionManager 作為連接池。接下來我們通過以下步驟來優(yōu)化:

3.1 定義一個keep alive strategy
關(guān)于keep-alive弦牡,本文不展開說明友驮,只提一點,是否使用keep-alive要根據(jù)業(yè)務(wù)情況來定喇伯,它并不是靈丹妙藥喊儡。還有一點,keep-alive和time_wait/close_wait之間也有不少故事稻据。

在本業(yè)務(wù)場景里艾猜,我們相當(dāng)于有少數(shù)固定客戶端,長時間極高頻次的訪問服務(wù)器捻悯,啟用keep-alive非常合適

再多提一嘴匆赃,http的keep-alive 和tcp的KEEPALIVE不是一個東西〗窀浚回到正文算柳,定義一個strategy如下:

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        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")) {
                return Long.parseLong(value) * 1000;
            }
        }
        return 60 * 1000;//如果沒有約定,則默認(rèn)定義時長為60s
    }
};

3.2 配置一個

PoolingHttpClientConnectionManager
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(500);
connectionManager.setDefaultMaxPerRoute(50);//

例如默認(rèn)每路由最高50并發(fā)姓言,具體依據(jù)業(yè)務(wù)來定
也可以針對每個路由設(shè)置并發(fā)數(shù)瞬项。

3.3 生成

httpclient
httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setKeepAliveStrategy(kaStrategy)
                .setDefaultRequestConfig(RequestConfig.custom().setStaleConnectionCheckEnabled(true).build())
                .build();

注意:使用setStaleConnectionCheckEnabled方法來逐出已被關(guān)閉的鏈接不被推薦蔗蹋。更好的方式是手動啟用一個線程,定時運行closeExpiredConnections 和closeIdleConnections方法囱淋,如下所示猪杭。

public static class IdleConnectionMonitorThread extends Thread {
    
    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;
    
    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }
    
    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }
    
}

3.4 使用httpclient執(zhí)行method時降低開銷
這里要注意的是,不要關(guān)閉connection妥衣。

一種可行的獲取內(nèi)容的方式類似于皂吮,把entity里的東西復(fù)制一份:

res = EntityUtils.toString(response.getEntity(),"UTF-8");
EntityUtils.consume(response1.getEntity());

但是,更推薦的方式是定義一個ResponseHandler税手,方便你我他蜂筹,不再自己catch異常和關(guān)閉流。在此我們可以看一下相關(guān)的源碼:

public <T> T execute(final HttpHost target, final HttpRequest request,
            final ResponseHandler<? extends T> responseHandler, final HttpContext context)
            throws IOException, ClientProtocolException {
        Args.notNull(responseHandler, "Response handler");

        final HttpResponse response = execute(target, request, context);

        final T result;
        try {
            result = responseHandler.handleResponse(response);
        } catch (final Exception t) {
            final HttpEntity entity = response.getEntity();
            try {
                EntityUtils.consume(entity);
            } catch (final Exception t2) {
                // Log this exception. The original exception is more
                // important and will be thrown to the caller.
                this.log.warn("Error consuming content after an exception.", t2);
            }
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            }
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            throw new UndeclaredThrowableException(t);
        }

        // Handling the response was successful. Ensure that the content has
        // been fully consumed.
        final HttpEntity entity = response.getEntity();
        EntityUtils.consume(entity);//看這里看這里
        return result;
    }

可以看到芦倒,如果我們使用resultHandler執(zhí)行execute方法艺挪,會最終自動調(diào)用consume方法,而這個consume方法如下所示:

public static void consume(final HttpEntity entity) throws IOException {
        if (entity == null) {
            return;
        }
        if (entity.isStreaming()) {
            final InputStream instream = entity.getContent();
            if (instream != null) {
                instream.close();
            }
        }
    }

可以看到最終它關(guān)閉了輸入流熙暴。

4.其他
通過以上步驟闺属,基本就完成了一個支持高并發(fā)的httpclient的寫法慌盯,下面是一些額外的配置和提醒:

4.1 httpclient的一些超時配置
CONNECTION_TIMEOUT是連接超時時間周霉,SO_TIMEOUT是socket超時時間,這兩者是不同的亚皂。連接超時時間是發(fā)起請求前的等待時間俱箱;socket超時時間是等待數(shù)據(jù)的超時時間。

HttpParams params = new BasicHttpParams();
//設(shè)置連接超時時間
Integer CONNECTION_TIMEOUT = 2 * 1000; //設(shè)置請求超時2秒鐘 根據(jù)業(yè)務(wù)調(diào)整
Integer SO_TIMEOUT = 2 * 1000; //設(shè)置等待數(shù)據(jù)超時時間2秒鐘 根據(jù)業(yè)務(wù)調(diào)整

//定義了當(dāng)從ClientConnectionManager中檢索ManagedClientConnection實例時使用的毫秒級的超時時間
//這個參數(shù)期望得到一個java.lang.Long類型的值灭必。如果這個參數(shù)沒有被設(shè)置狞谱,默認(rèn)等于CONNECTION_TIMEOUT,因此一定要設(shè)置禁漓。
Long CONN_MANAGER_TIMEOUT = 500L; //在httpclient4.2.3中我記得它被改成了一個對象導(dǎo)致直接用long會報錯跟衅,后來又改回來了
 
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SO_TIMEOUT);
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, CONN_MANAGER_TIMEOUT);
//在提交請求之前 測試連接是否可用
params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);
 
//另外設(shè)置http client的重試次數(shù),默認(rèn)是3次播歼;當(dāng)前是禁用掉(如果項目量不到伶跷,這個默認(rèn)即可)
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));

4.2 如果配置了nginx的話,nginx也要設(shè)置面向兩端的keep-alive
現(xiàn)在的業(yè)務(wù)里秘狞,沒有nginx的情況反而比較稀少叭莫。nginx默認(rèn)和client端打開長連接而和server端使用短鏈接。注意client端的keepalive_timeout和keepalive_requests參數(shù)烁试,以及upstream端的keepalive參數(shù)設(shè)置雇初,這三個參數(shù)的意義在此也不再贅述。

以上就是我的全部設(shè)置减响。通過這些設(shè)置靖诗,成功地將原本每次請求250ms的耗時降低到了80左右郭怪,效果顯著。

4.3
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刊橘,一起剝皮案震驚了整個濱河市移盆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伤为,老刑警劉巖咒循,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绞愚,居然都是意外死亡叙甸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進(jìn)店門位衩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裆蒸,“玉大人,你說我怎么就攤上這事糖驴×诺唬” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵贮缕,是天一觀的道長辙谜。 經(jīng)常有香客問我,道長感昼,這世上最難降的妖魔是什么装哆? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮定嗓,結(jié)果婚禮上蜕琴,老公的妹妹穿的比我還像新娘。我一直安慰自己宵溅,他們只是感情好凌简,可當(dāng)我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恃逻,像睡著了一般雏搂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辛块,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天畔派,我揣著相機(jī)與錄音,去河邊找鬼润绵。 笑死线椰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尘盼。 我是一名探鬼主播憨愉,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼烦绳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了配紫?” 一聲冷哼從身側(cè)響起径密,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躺孝,沒想到半個月后享扔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡植袍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年惧眠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片于个。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡氛魁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出厅篓,到底是詐尸還是另有隱情秀存,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布羽氮,位于F島的核電站或链,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乏苦。R本人自食惡果不足惜株扛,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一尤筐、第九天 我趴在偏房一處隱蔽的房頂上張望汇荐。 院中可真熱鬧,春花似錦盆繁、人聲如沸掀淘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽革娄。三九已至,卻和暖如春冕碟,著一層夾襖步出監(jiān)牢的瞬間拦惋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工安寺, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留厕妖,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓挑庶,卻偏偏與公主長得像言秸,于是被迫代替她去往敵國和親软能。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內(nèi)容