昨天的搜索系統又出情況了,幾個庫同時重建索引變得死慢。通過一個上午的復現分析,肯定問題出現httpclient的使用上(我使用的是3.1這個被普遍使用的遺留版本)。搜索系統在重建索引時,是併發多個線程(默認是8個)不停的從PHP客戶端取數據(固然,從另外一個角度來講,搜索系統是客戶端,PHP端是服務端),取回後放到一個隊列裏由單獨的一個或多個線程更新索引。在測試環境復現發現,對於一個請求,PHP端打印耗時是1-2秒,但搜索端打印在4-6秒。這種耗時差異也就兩種可能性,一個是PHP端返回到搜索端接受完耗時太長,另外一個就是搜索端在真正發給PHP端數據前等待了好久。由於有了以前的jetty7的困頓,起初我懷疑是傳輸數據的問題。由於請求數據的代碼部分我只是簡單的使用了httpclient,因此只能從httpclient着手分析。我想到把PHP端和搜索端的請求起始和結束時間都打出來對照一下,不過在這樣作以前我把搜索端的併發請求線程數調到了1,看下單線程狀況下效果如何,結果驚奇地發現PHP端和搜索端的耗時相近。因此,能夠肯定,是httpclient的併發鏈接處理上可能存在問題。不消說,翻開httpclient API中和配置相關的接口,結果找到HttpConnectionManagerParams類中下面兩個函數:html
public void setDefaultMaxConnectionsPerHost(int maxHostConnections); public void setMaxTotalConnections(int maxTotalConnections);
httpclient在處理請求鏈接方面使用了鏈接池,它內部實際上有兩種鏈接池,一種是全局的ConnectionPool,一種是每主機(per-host)HostConnectionPool。參數maxHostConnections就HostConnectionPool中表示每主機可保持鏈接的鏈接數,maxTotalConnections是ConnectionPool中可最多保持的鏈接數。每主機的配置類是HostConfiguration,HttpClient有個int executeMethod(final HostConfiguration hostConfiguration, final HttpMethod method)方法能夠指定使用哪一個HostConfiguration,不過多數狀況都是不顯示指定HostConfiguration,這樣httpclient就用了默認的HostConfiguration=null,也就是說全部的請求能夠認爲指自同一個主機。若是不設置這兩個參數,httpclient天然會用默認的配置,也就是MultiThreadedHttpConnectionManager中的:程序員
public static final int DEFAULT_MAX_HOST_CONNECTIONS = 2; // Per RFC 2616 sec 8.1.4 ? public static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 20;
默認的maxHostConnections大小隻有2,也就是說,在我併發8個線程請求數據時,實際上會有6個線程處於等待被調度,這也就解釋上面的現象了。再看看上面的註釋,我從http://www.faqs.org/rfcs/rfc2616.html找到從了RFC 2616 sec 8.1.4 Practical Considerations段落的最後一段:
Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion.
看這敘述,也就代表人家httpclient設置maxHostConnections爲2是有根有據的。不過,這種設置顯然適合的是瀏覽器這種客戶端,但我相信,多數使用httpclient並不但願有這種默認的限制。而它默認的只有20的maxTotalConnections也太有些吝嗇了。我後來瀏覽solr的客戶端server類CommonsHttpSolrServer的代碼發現了下面的段落,solr要比我更瞭解httpclient:瀏覽器
_httpClient = (client == null) ? new HttpClient(new MultiThreadedHttpConnectionManager()) : client; if (client == null) { // set some better defaults if we created a new connection manager and client // increase the default connections this.setDefaultMaxConnectionsPerHost( 32 ); // 2 this.setMaxTotalConnections( 128 ); // 20 }
對於httpclient,特別指出的是它的MultiThreadedHttpConnectionManager,看名字好像是多線程併發請求似的,其實不是,但它也確實用到了多線程,那是在發現鏈接不夠用時起個等待線程wait信號,這個名稱的含義應該是多線程狀況線程安全的HttpConnectionManager。
使用httpclient還有兩點經驗,一個是建立的MultiThreadedHttpConnectionManager 實例最好是全局的,不然會有多個鏈接池,而HttpClient是線程安全的,能夠多個實例。另外一個是,在處理請求的最後,也就是finnaly裏中,要調用method.releaseConnection();回收鏈接,不然鏈接池就可能會爆了。安全
補充:寫完以後倒在牀上,我又想起了幾個問題,這裏補充上:
一、系統原先重建索引隱約記得速度仍是能夠的,爲何如今變慢得如此明顯?這有兩方面的緣由,一個是原來系統取數據是用的單線程(我後來發現單取數據取數據速度跟不上更新索引的速度因此改爲多線程),另外一個是,當時重建沒有一會兒同時開啓數個庫。因此,即使是一樣的代碼,環境變了,效果也可能變了。當這種改變悄然發生了,程序員卻沒有捕捉到,纔會第一直覺感到問題的詭異。
二、對於長時間不能得到鏈接的狀況,httpclient是否有warn日誌報出來?由於我使用了httpclient的getResponseBodyAsStream方法,而它會打出warn日誌,因此我是關掉了httpclient的warn級別的。因此,我又檢查了httpclient的代碼,惋惜沒看到相關warning log,這點httpclient能夠改進下。不過httpclient如今都是4時代了,而我使用的仍是3.1的,而3.x已經被中止更新了,因此再採用httpclient能夠考慮4版本的,儘管如今能見到的代碼幾乎都是用的3.x系列的。
三、httpclient的文檔是否有特別提到鏈接數配置的狀況?我翻看了一下,確實在關於threading一頁中有提到。不過,我等使用它時顯然沒有完整閱畢它的文檔。或許,httpclient給出個明顯的最佳實踐到能引發使用者的注意,不然誤用的狀況仍是會時有發生。不信,google之。多線程