【轉】一種基於Lucene的實時搜索方案 - From 淘寶技術部

http://www.tuicool.com/articles/NZ7v6b數據庫

 

背景

阿里集團各大業務快速發展過程當中都對搜索服務不少剛性的須要,而這樣的搜索需求有着很是明顯的特徵:快速支持、低成本、實時性和穩定性。緩存

快速支持:性能優化

業務需求急迫、須要一週甚至幾天內完成索引服務搭建、測試、上線環節。服務器

低成本:架構

搜索需求方要求接入便捷,低成本的機器和運維成本。框架

實時性:運維

搜索需求方的業務數據發生變化,須要實時在索引中進行更新可見,而這個過程一般須要穩定的保證在100ms內。異步

穩定性:分佈式

搜索服務集羣不會由於升級、運維操做或者若干臺機器的宕機致使搜索服務不穩定。oop

面臨的挑戰

海量數據

阿里的業務基本動輒就是上10億上百億規模,那麼如何能行之有效的管理索引和保障搜索性能將是一個很是具備挑戰性的工做。

業務複雜

互聯網業務負責,那麼涉及到須要進行檢索的源數據可能來自多個數據庫多個表,也就意味着索引數據源可能來源於多庫多表,而且表和表之間還有1:N,N:N的關係。而使用過Lucene的同窗們都知道,對於基於Lucene爲引擎內核的更新都是完整記錄更新,因此對於咱們產品來講構建索引時候避免不了多表join湊整更新問題。

實時性

在阿里的衆多搜索需求場景下,不少時候是把搜索服務當作數據庫使用,也就意味着一個頁面上產生的更新操做,頁面跳轉後就能看到最新更新的結果,那麼對於咱們搜索服務來講須要在毫秒級別將更新結果可見。

高可用性

阿里的業務對穩定性的要求是很是高的,因此對於升級、擴容、機器宕機等狀況都不能影響正常的搜索和更新服務。

可伸縮性

電商業務一年下來有很是多的大促活動,而一旦有大促活動,搜索的QPS將會比日常翻幾倍。因此若是以大促的成本去衡量支持服務,整個服務所須要的機器在平時將很是空閒,從而形成了成本極大浪費。那麼針對這種狀況,就須要咱們提供一種靈活的能夠在線伸縮服務吞吐量的技術:即大促前臨時加機器來支持突增的流量,大促後隨時下線擴容機器,而整個過程不影響正常的搜索服務。

由於本文篇幅有限,在這裏我只會着重介紹:實時性、高可用性在咱們產品中的一些技術實踐。

實時解決方案

在介紹咱們產品方案以前,首先介紹下業內常見的實時解決方案,見圖1-1實時架構圖:

圖1-1

圖1-1

該方案通常是由:

  • 內存索引(Ram-IndexA)負責數據更新。
  • 內存索引(Ram-IndexA)達到閥值,角色轉換成待合併內存索引(Ram-IndexB),同時從新開闢一塊新的內存索引(Ram-IndexA)負責新的更新寫入,老的內存索引(Ram-IndexB)合而且優化到主索引中。
  • 內存索引(Ram-IndexA)+磁盤索引(Full-IndexA)提供檢索服務。

基於該方案能夠帶來的最大的優點是內存索引能夠合併到主索引,避免了索引碎片,從而能夠屏蔽全量索引從新構建和保障搜索服務的性能穩定性。可是這種方案仍然會存在如下問題:

  • 生產者(寫入量)遠遠大於消費能力(內存索引構建),影響上游系統(數據生成方)的穩定性。
  • 內存索引合併磁盤主索引並執行優化過程時間較長,若是該過程出現宕機,重啓機器後存在丟失數據的可能性。
  • 內存索引和主磁盤索引合併後,主索引是須要從新打開才能讓更新可見,而對於大的磁盤索引從新打開一次耗時是比較長,由於須要從新預熱數據到內存中。那麼對實時性要求很高的須要明顯是不合理的。
  • 另外爲了保證從新打開主索引視圖期間查詢是不中斷,也就意味着一份大的磁盤索引的資源視圖須要被同時打開2份。那麼就意味着承載該主索引的機器資源是須要實際承載主索引2倍以上資源才能知足。
  •  大索引的合併並優化的過程對機器IO資源佔用較大,而自己搜索服務自己就是IO密集型的應用,因此合併主索引並優化一定對搜索服務穩定性帶來影響。
  • 由於要數據更新可見,因此須要頻繁的從新打開合併後的磁盤大索引,這樣會致使大索引對應的優化Cache出現頻繁清空,從新加載的問題。從而使得爲性能優化而設置的Cache訪問命中率將很是低,這樣對於一些複雜的統計查詢帶來性能上極大不穩定性,同時Cache若是自己所佔內存過大,還會帶來JVM頻繁FullGC的影響。

因此爲了單純追求系統某個指標值(如永遠不須要作全量),而犧牲掉系統穩定性是得不償失的。固然上述某些緣由可能在一些好的硬件配置機器下並不會暴露的特別明顯,可是從技術架構的機器成本上考慮的話,上述設計方案就不是一種特別合適的方案。那麼咱們產品平臺便採起了一種更低成本更穩定的實時架構方案來解決上述問題,其主要思路:

  • 採用WAL機制保證上游系統寫入磁盤的源數據不丟失,機器宕機重啓保證讓機器數據快速恢復到宕機前一致。
  • 全部的更新操做只會發生在內存索引,但內存索引不會無限擴大,知足系統設置閥值後就會刷入磁盤,一旦刷入磁盤的索引將不會發生從新打開(只存在標記刪除操做),經過這種屏蔽從新打開磁盤索引的操做,也就解決了前面提到因爲主索引頻繁從新打開致使的查詢實時性、資源佔用峯值、Cache的命中率、FullGC的問題。
  • 內存索引直接刷磁盤生成子索引,再也不去合併主索引,避免大的主索引須要從新打開,同時子索引的大小可控,基本在100MB以內,避免從新打開子索引很是慢。
  • 子索引數目增多,也就意味着冗餘數據變多、查詢遍歷更多的文件,那麼性能一定會形成影響,因此子索引必須經過一種合併策略進行合併優化。咱們產品採起的策略由合併因子和閥值影響,例如:當相同大小的100MB子索引達到10個的時候,觸發合併。雖然觸發合併後,仍然會帶來IO爭用,但由於子索引體積小,因此合併優化時間較快,那麼對搜索服務的影響基本不存在。另外當子索引合併生成的新子索引達到必定大小時候,合併策略將不會將其歸入下次待合併列表中,即永遠不會再參與合併。

基於上述的思路,我將着重在實時更新處理、實時索引體系兩個方面來跟你們介紹下咱們產品。

實時更新處理

WAL日誌

大型分佈式系統中故障很常見,設想一下,若是內存索引沒有刷寫,服務器就宕機了。內存中沒有寫於硬盤的數據就會丟失。因此咱們的分佈式實時搜索產品應對的辦法是在寫內存索引以前先寫入WAL(Write-Ahead Logging,預寫式日誌)。其寫入流程以下:

  • 將WAL日誌以追加寫的方式寫入磁盤日誌文件中
  • 將WAL日誌的修改操做做用到內存索引中
  •  返回操做成功或者失敗

如上所示,在修改內存索引元素以前,要確保與這一個修改相關的操做日誌必需要刷入磁盤中。若是檢索服務器宕機,沒有從RamIndex刷寫入Disk的數據將能夠經過回放WAL來恢復。而這個過程並不須要人爲參與,檢索節點內部機制中有恢復流程來處理。

批提交

通常而言搜索系統是須要將WAL日誌刷入磁盤才能夠構建內存索引的,可是若是每一個事務都要求將日誌當即刷入磁盤,系統的吞吐量將會不好。所以,對一致性要求很高的應用,須要當即刷入;相應地,對一致性要求不高的應用,能夠考慮不要求當即刷入,首先將WAL日誌緩存到內存緩存區中,按期刷入磁盤。可是這種作法有一個問題,若是搜索應用系統意外故障,可能丟失最後一部分更新操做。

批提交(Group Commit,如圖1-2批處理流程圖)技術是一種有效的優化手段。WAL日誌首先寫入到系統內容緩存區中:

  •  日誌緩存區的數據量超過必定大小,好比128KB;
  • 距離上次刷入磁盤超過必定時間,好比10ms。

當知足以上兩個條件中的某一個時,將日誌緩存區中多個事務的操做一次性刷入磁盤,接着一次性將多個事務的修改操做逐個返回客戶端操做結果。批提交技術保證了WAL日誌成功刷入磁盤後,才返回操做結果保障數據的不丟失,雖然犧牲了寫事務延時,但大大提升了系統吞吐量。

圖1-2

圖1-2

CheckPoint檢查點

考慮數據寫入須要實時可查,那麼更新的數據都是在內存索引中,那麼可能出現一些問題:

故障恢復時須要回放全部WAL,效率較低。若是WAL超過100GB,那麼,故障恢復時間根本沒法接受。另外內存有限,內存索引須要達到閥值後轉儲到磁盤。因此,咱們須要在內存索引轉儲到磁盤的時候,記錄checkpoint時刻的日誌回放點,之後故障恢復只須要回放checkpoint時刻日誌以後的WAL日誌,如圖1-3檢查點方案流程圖所示:

圖1-3

圖1-3

當機器發送重啓,只須要從新加載subindexA、subindexB、subindexC的索引,並重放checkpointC以後的WAL日誌,變可以讓數據恢復到宕機前一致。

實時索引體系

圖1-4

圖1-4

根據圖1-4實時方案架構圖咱們詳細說明下實時模式實現流程:

  • 更新操做都會在服務端以WAL落地磁盤
  •  服務端異步線程順序消費WAL構建成內存索引(Ram-IndexA)
  •  內存索引(Ram-IndexA)大小達到內存閥值將轉換角色爲Ram-IndexB
  •  從新開闢新的內存索引(Ram-IndexA)負責當前WAL的消費
  •   Ram-IndexB內存索引直接刷入磁盤生成以Index前綴的子索引,如:Index_0,Index_1,Index_2….。
  •  防止索引碎片(Index_0,Index_1…)會愈來愈多從而影響性能,咱們採起一種合併策略能夠經過合併因子和索引大小來選出能夠合併的小索引進行合併。
  •   達到閥值的子索引將不會在參與合併,那麼系統運行一段較長時間後,索引碎片(子索引)也將會愈來愈多,那麼系統能夠經過從新作一次全量的方式來消除索引碎片帶來的影響。

子索引合併策略

前面咱們說到內存索引一旦達到閥值,將被刷入到磁盤,那麼磁盤將會存在不少相似index_0、index_一、index2,index_3的索引碎片。若是不對這些索引碎片進行合併,那麼隨着這些索引碎片的增長,會致使搜索服務性能下降。因此咱們的產品對索引碎片採起了一種合併策略對其進行按期合併。

圖1-5-1

圖1-5

如圖1-5子索引合併流程圖所示,內存索引刷入磁盤,將會依次遞增的生成index_0,index_1,index_2的磁盤索引碎片。假設當前的合併因子是2,當合並管理器發現存在2個大小一致索引index_0,index_1的時候,變會觸發合併操做:index_0和index_1合併成index_3,合併過程當中index_0、index_1依然提供正常服務,當合並操做成功完成,即index_3生成完畢,並對外提供服務。接下來將index_0和index_1的資源引用計數減1,即當基於index_0、index1的查詢訪問線程結束時,index_0,index_1的資源引用計數爲0、索引將正常關閉,這樣一個索引碎片合併操做正常結束。可是若是合併過程出現宕機或者異常狀況,即當前合併事務未正常結束,那麼整個合併過程將會回滾,即index_3被清理,index_0,index_1正常提供服務。固然若是內存索引繼續刷到磁盤生成了index_四、index_5,經過合併策略生成了index_6,這個時候發現index_3和index_6又知足了合併條件,那麼index_3和index_6又會合並生成index_7。因此經過這種合併策略,小索引碎片逐步會被合併成大索引碎片,可是若是索引碎片越大,那麼帶來的合併代價也越大,咱們須要設置一個合併閥值,凡是索引碎片達到指定文件大小閥值後,將不會進一步再參與合併,這樣就很好的屏蔽了大索引碎片合併代價過大的問題。

全量索引構建

由於前面說到咱們產生索引碎片,而這些索引碎片即便進行了碎片合併而減小碎片數,可是一旦當碎片達到必定大小後就不適合繼續進行合併,不然合併代價很大,因此咱們沒法避免的會由於碎片問題而致使更新實時性和查詢QPS性能損耗問題。因此咱們的解決的辦法就是經過一段時間對具體業務所有源數據進行一次構建全量索引DUMP工做,用構建好的新的全量主索引去替換原來老的主索引和磁盤索引,從而讓實時更新、搜索服務性能恢復到最佳。

阿里的業務數據規模都很龐大,動輒就上10億到百億,那麼咱們若是使用Solr原生的基於檢索服務節點的索引構建模式會帶來2個很大問題:

  • 構建索引就是一個IO密集型的任務,而搜索服務也是IO密集型,那麼兩個任務若是在一臺機器上並存,將會致使雙方服務變得都不穩定。
  • 搜索業務數據規模大,致使傳統原生構建索引的方式在幾十億數據量規模下所須要的時間特別長,即便深夜訪問低峯期開始全量任務,也須要延續到白天甚至是訪問高峯期還未結束,從而使得搜索服務出現頻繁超時現象。

因此基於上述緣由咱們的搜索平臺實現一個分佈式全量索引任務調度框架來解決搜索業務全量索引構建的問題。

圖1-6

圖1-6

如圖1-6 DUMP中心架構圖所示簡單描述下一個業務全量索引構建的流程:

  •  將一個具體業務相關的上下文信息以全量索引構建任務的形式提交給JobNode
  • JobNode根據TaskNode空閒程度,選擇好若干個TaskNode並將全量任務下發到具體的TaskNode。JobNode根據了這些上下文信息把一個全量任務分解成若干個TaskNode進行,這樣有效的用到分佈式並行任務的優點來加速索引構建。
  • 被選擇出來的若干個TaskNode根據授予任務的上下文信息,獲取業務數據的來源類型和存儲地址,(如數據庫、Hadoop雲梯),而後經過流的方式消費源數據內容並構建成索引。
  • 一個索引任務執行完畢後,將完成的全量索引根據指定的目標存儲源進行迴流(通常仍是HDFS)。

那麼經過這種離線的分佈式索引構建中心具體爲搜索業務帶來什麼呢,主要在如下幾個方面體現:

  • 完全隔離全量構建索引和搜索服務的耦合性,杜絕資源搶用狀況。
  • 實現與業務細節無關的全量構建任務集羣,能爲接入搜索平臺的全部業務進行全量索引構建服務,意味着極大提升機器綜合利用率,不用爲具體業務搭建具體的全量DUMP集羣。
  • 索引構建和檢索服務隔離後,DUMP任務節點(TaskNode)能夠在最大化利用機器資源,即對底層索引構建細節深刻優化,因此極大提高了海量數據索引構建速度。
  • DUMP中心快速構建海量數據索引並回流索引文件到存儲中心的過程,爲在線搜索服務無縫擴容和線上故障快速恢復提供了數據來源基礎。

其餘優化

咱們產品在solr和Lucene上作了不少優化來適應一些業務的需求,本文篇幅有限,因此在這裏我主要挑出一個比較有表明性的優化實踐:Cache改造。

Cache改造

用過solr的同窗們都知道全部的Cache都是由SolrIndexSearcher來管理的,如圖1-7 Searcher結構圖所示:

圖1-7

圖1-7

而在咱們的實時模式下須要讓更新的數據實時可見,那麼必須近實時的用新的SolrIndexSearcher-new去替換SolrIndexSearcher-old。(如圖1-7)而這樣實時的替換 也就引起以下問題:

  • solrIndexSearcher的替換,意味着基於solrIndexSearcher層的Cache(如圖1-7所示的4種Cache)所有失效,那麼意味着毫秒級別會頻繁有大空間的內存須要被垃圾回收,最終會觸發頻繁的FullGC。
  • 若是Cache配置還打開了預熱功能(warm),那麼新的SolrIndexSearcher在替換以前須要將其管理的新Cache進行預熱。數據量若是較大,那麼預熱時間會較長,從而引起數據實時性可見問題。

因此基於如上的問題,終搜產品從新設計了一些Cache,將Cache的管理由SolrIndexSearcher遷移到IndexReader層中來,如圖1-8  Cache結構圖所示:

圖1-8

圖1-8

首先,先闡述下咱們這種優化思路的前置條件,前文中提到咱們內存索引會直接刷磁盤而不用合併到主索引中,這樣在磁盤存在的主索引、子索引對應的內存視圖對象IndexReader在任什麼時候候都不須要從新打開,而以IndexReader管理的Cache一旦建立後將不會被失效,而須要涉及到預加載Cache的過程只是在刷入磁盤或者系統從新啓動過程當中一次將配置涉及到的Cache都預加載到內存中,那麼以前存在的頻繁失效致使GC、預加載慢引發實時性的若干問題都將不復存在。

因此經過將Cache從Searcher層遷移到IndexReader層的設計使得實時模式下的引擎在複雜的統計查詢下性能也能獲得很好的保證。

總結與展望

本文中咱們深刻的探討了一種高穩定性實時搜索引擎系統實踐,這些實踐內容也依託於咱們的產品服務於阿里衆多業務線。而目前咱們的產品搜索服務集羣已經將近700臺,接入業務範圍也涵蓋整個阿里集團。而這些業務特別是在數據量和訪問量的成倍增加的狀況下,咱們產品更加須要關注

  • 再也不須要爲數據規模和訪問規模增加而提心吊膽。
  • 更加合理利用機器資源,搜索服務集羣吞吐量能夠根據業務實際狀況來動態調整。

歸根結底其實這些要求是對搜索服務系統的易擴展提出了更高的要求,即如何提供一種無縫的在線擴容方案達到搜索服務吞吐量無上限的目標,而這個目標也正是咱們產品目前正在重點關注的方向,而關於這塊的內容但願有機會在新的文章中跟你們作深刻探討。

做者簡介

柳明(花名:洪震),阿里技術專家,阿里一站式搜索服務平臺TSearcher的負責人。目前關注於分佈式、高性能、高穩定性的搜索服務領域。

相關文章
相關標籤/搜索