【總結】阿里中介軟體效能大賽
抱大腿參加了一次中介軟體比賽,受益匪淺。學到了很多東西,更重要的是認識到了差距在哪。針對具體業務場景的優化就不提了,寫一下比較通用的優化策略。
1. Java
1.1 Split()
Java原生的split方法在此次使用中效能有很大的問題,主要在於兩點。首先Split中會建立一個arraylist,將切割後的子字串放入arraylist中,list的建立和擴容是一處開銷,尤其是在使用中並不需要切割後的所有子字串的時候。還有一點,split中使用的是正則去匹配多個char的regex,此處也會造成很大的效能問題。改進的方法是使用indexOf()和subString()結合的方式。
String splitted = line; p = splitted.indexOf(':'); key = splitted.substring(0, p);
1.2 BufferedReader
BufferedReader在初始化的時候可以指定緩衝區的大小,此處可根據頁的大小以及資料的實際排布更改。除此之外,還有一個預設引數 defaultExpectedLineLength。如果對於行的長度比較確定,可以自定義一個長度,減少readline() 內部StringBuffer擴容的開銷。不過BufferReader的構造器並不提供這個引數的設定,所以需要重寫一下。
2. 多執行緒
索引建立階段:
初始的策略是將源資料檔案平均分給3個執行緒去處理,讀取和寫入均不考慮位於哪個磁碟。由於比賽是三塊硬碟,8核CPU,後來考慮用3個執行緒的在分別去讀寫三塊硬碟,減少多執行緒訪問對磁碟IO的影響。本地測試沒有提升,這個是可以理解的。但是後來線上測試的時候,建索引的時間也沒有太大的提升。猜測的原因可能是原來的策略磁碟爭用比較有限,因此分開讀寫稍有改進但影響不大。
查詢階段:
查詢階段使用多執行緒效果很明顯,提升了大概80%。
最開始的策略是3個執行緒分別去三個硬碟去找,然後把結果合併。然而測試的結果並不好,提升比較有限。在測出查詢階段每部分的時間之後發現一個現象:每個執行緒搜尋的時間相比原來的單執行緒有大幅減小 -- 這樣很合理因為每個執行緒需要查詢的檔案少了;但是在合併結果,也就是等待所有執行緒結束的開銷是很高的;不論是CountLatchDown還是Future,都有相似的現象。
大賽在後半段明確CPU是8核,意識到對於多執行緒的利用比較有限之後,對執行緒的數量進行了調整。對於三種查詢設定三個執行緒池,每個執行緒池8個執行緒。對於多執行緒,坊間常見的一種說法是執行緒數最好不要超過核心數。在之前的一個專案中,使用OpenMP框架進行多執行緒程式設計,測試結果也佐證了這一說法。大賽規則中提到會有併發查詢,也就是同一時間最多會有24個執行緒在跑。但是因為併發查詢的密度是未知的,因此這種策略算是試出來的,假定併發是少數的。
多執行緒這種東西跟機器的關係很大,本地測試和線上測試的結果經常有很大差異,事情不能太想當然,還是要多試試。
3. Byte
位元組算是這次比賽的一個痛處。在比賽的前期,對於索引檔案的讀寫採用的都是字串。到了比賽末期,意識到位元組比字串是要快一些的,因為在讀取和寫入的時候省去了byte和string之間的轉換;單元測試中,byte使用BufferedInputStream(out),字串使用BufferedReader(writer)的情況下,byte的讀寫要比字串要快將近1倍,但是因為比賽後期時間不太夠了,只是把其中一級索引應用了位元組流。
3.1 ByteBuffer & ByteArrayOutputStream
ByteBuffer提供自身的put/getInt之類的方法;ByteArrayOutputStream搭配DataOutputStream也可以實現便捷的byte讀寫。對於二者的效能,下面的連結給了很好的解釋。點選開啟連結 下面的連結詳細比較ByteBuffer heap和direct的區別。點選開啟連結 在實際的應用中,ByteBuffer也確實是快的。
3.2 MappedByteBuffer
另一個針對大檔案,尤其位元組流讀寫的工具是記憶體對映。
<span> </span>FileInputStream fis = new FileInputStream("sdf");
MappedByteBuffer buffer = fis.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, 100);
byte[] b = new byte[5];
buffer.get(b, 0, 5);
單元測試中,使用記憶體對映的方式讀取位元組流檔案,要比BufferedInputStream快2倍。
檔案讀寫還是要儘量使用bytes,以後有時間要把原來的程式全部改為位元組 。
4. IO
4.1 RandomAccessFile
Java的RandomAccessFile可以從檔案特定位置開始訪問而無需讀取整個檔案。這對於大檔案的讀取是非常重要的。我們可以通過seek()在一個大檔案中跳躍。在實際使用中,我們發現:如果seek跳躍的距離很遠或者很隨機的話,讀的效率會大大降低。我們猜想這一現象是因為硬碟的磁頭需要通過大範圍的移動來讀取資料。因此,我們在讀取資料進行了分組和排序。我們首先將要讀取的資料根據檔案進行group;然後對於單個檔案,將要讀取的資料的offset進行排序。這樣,在假定源資料是順序寫的情況下,磁頭可以用最少的移動去讀取所需資料。
5. hash
這次比賽之後最大的感觸就是,hash才是終極方案,排序能不用就不用。在我們的解決方案中,排序也僅有很小的一部分應用。查詢效率的幾次質變,都是由於hash的應用。