2011年3月網站Lucene調整優化手記

壹.起因

    自網站重構以來,我們加入了Apache Lucene,用來輔助mysql數據庫存儲查詢,以減少對DB的負擔,網站的大部分數據共有的特點是不需要即時更新,數據量較大,這正是Lucene擅長解決的問題領域,起始版本是2.4,開始效果不賴,當然也遇到了一些問題,例如判斷索引文件合理的大小值問題,分詞器的選擇問題,對於一個完整的存儲查詢解決方案來說是不言而喻的,Lucene的學習成本相對而言也較高,理論和內容都比較多,需要花時間和精力來研究。
    09年底,Lucene推出了3.0版,自從2.9版開始,內部結構發生了不小的變化,同時根據官方文檔的提示,Lucene在自身性能上有了明顯的提高,隨後我們做出:升級到3.0的決定。
    2010年,網站各個部分的數據訪問基本都是基於Lucene,各類索引的建立訪問和管理,成爲了新的問題,如何組織索引文件,如何更有效的訪問,這類問題不斷浮現,2010年底,網站的訪問量有了新的提升,如何應對激增的訪問,成爲了首要的問題。
    2011年開始,訪問量最大的www系統顯現出了問題,在高訪問量下,出現了OutOfMemoryError,當虛擬機可用內存不足2%,且不能正常回收時,會throw出這個Error,一個良好運行的系統,是不應出現這樣明顯的性能問題的,我們決定:必須要解決這個問題了。

貳.經過

    提到應對訪問量,有經驗的人員首先想到的是擴展,垂直擴展是我們首先嚐試的,最直接的做法是:增大每個tomcat的JVM內存上限,提高tomcat的訪問線程上限,研究apache和tomcat之間,如何更有效的轉發請求等等,而隨後的水平擴展,最直接的做法是,增加tomcat數量,更多的分散負載請求,這些我們都有嘗試,效果卻不明顯,問題何在?最終我們把目光移到了Lucene本身,是不是我們使用它的方式有問題?
    隨後展開的查詢搜索研究過程暫且不表,我們最終發現了一處地方,很有可能會影響性能:在我們的索引數據查詢方法裏,都是通過類似這樣的方式來獲得IndexSearcher對象的

Directory directory = new SimpleFSDirectory(new File(indexDir));
IndexSearcher searcher = new IndexSearcher(directory);
//查詢過程
directory.close();
searcher.close();
	

    這看上去好像沒有什麼問題,類似於JDBC獲取Connection對象並最終在finally確保關閉一樣,是很標準的做法,但當我們再深入一點,會發現IndexSearcher類還有另外一種構造方法

Directory directory = new SimpleFSDirectory(new File(indexDir));
IndexSearcher searcher = new IndexSearcher(IndexReader.open(directory));
//查詢過程
directory.close();
searcher.close();


    這有什麼不同?其實第一種構造方法,會在內部調用第二種構造方法,也就是說,IndexSearcher需要一個IndexReader,如果我們用的是第一種,則每次都要重建一個IndexReader對象,並在IndexSearch.close()後一起關閉,而第二種我們手動傳入IndexReader對象的方式,則可以保留IndexReader而不關閉,這樣就可以重用該對象,上面提到,我們的數據量大部分是不需要即時更新的,也就是說,對於一種類型的索引文件,其實我們只需要打開一個IndexReader對象就可以了。
    在Lucene官方的API說明,提到了IndexSearcher的使用,原文爲:For performance reasons it is recommended to open only one IndexSearcher and use it for all of your searches. 說白了,只有一個IndexSearcher,自然也只會有一個IndexReader了,而IndexReader.open()方法的調用次數,正是影響性能的關鍵所在。
    我們最終選用了保守一點的方法,改變IndexSearcher的構造方式,將IndexReader單例化,並將改變應用到各個主要系統中。

叄.結果
   
    改變之後,性能發生了很大的變化,系統方面,例如原先35機器的cpu使用率在30%-70%之間,1,5,10分鐘平均負載都在5.X左右,修改後cpu使用率基本在10%以下,1,5,10分鐘平均負載則只有0.X左右,在虛擬機方面,效果也很明顯,修改前GC大小回收的兩個總時間基本相同,加在一起要佔到系統運行時間的近1/6,修改後小GC次數降低了近20倍,而大GC則穩定的在2小時左右才運行一次,每個JVM內存最大上限爲1.5G,實際只用到了600M左右,完全消除了OutOfMemoryError出錯的可能。

肆.補充
  
    實際在這個過程裏,我們也嘗試研究了很多種方式,最後簡單說明如下:
    1.http://wiki.apache.org/jakarta-lucene/ImproveIndexingSpeed裏面說明了建索引時要注意的地方,要優先閱讀。
    2.http://wiki.apache.org/jakarta-lucene/ImproveSearchingSpeed裏面說明了查詢索引時要注意的地方,更要優先閱讀。
    3.建立索引時,IndexWriter.MAXBufferedDocs最好不要設置,它默認是關閉的,而IndexWriter.RAMBufferSize屬性默認爲16M,即內存Document對象達到了16M纔會刷新至磁盤,推薦優先應用這個設置。
    4.構造Field對象時,需要傳入Field.Index index參數,如不需要boost功能,儘量使用ANALYZED_NO_NORMS而不是ANALYZED,儘量使用NOT_ANALYZED_NO_NORMS而不是NOT_ANALYZED,修改之後,發現並不能有效地節省磁盤空間,但是會影響內存使用。
    5.構造Field對象時,可以設置omitTermFreqAndPositions屬性,來不保存詞條的位置等信息,API原文說明爲:While this option reduces storage space required in the index, it also means any query requiring positional information, such as PhraseQuery or SpanQuery subclasses will silently fail to find results. 修改之後,可以明顯地節省10%左右的磁盤空間,但在隨後測試發現,雖然程序裏沒有用到PhraseQuery和SpanQuery查詢,但是查詢條件也會受到影響而不能分詞,初步推斷和使用的IK分詞器有關,所以這個屬性,我們實踐過後認爲不要隨意設置。
    6.通過Eclipse Memory Analyzer軟件分析www的heap快照文件,發現Lucene中的FieldCache類比較多,在網上搜索得知,該緩存類和IndexSearcher類的數目有關,但在我們上面的解決方案中,我們的觀點更傾向於,該類和IndexReader類的數目有關,此處有待以後驗證,現在的系統,並沒有像API說明提示的那樣,把IndexSearcher也搞成單例,因爲現在的內存狀況很好,如以後再遇到擴展的性能問題,可以再回到這裏,考慮和研究IndexSearcher單例的進一步做法。
    7.再次強調,主要的優化方式和提示,優先查看第1,2條裏的官方說明,裏面包含了很有價值的信息,因此在本篇不再贅述。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章