搜尋引擎怎麼選?攜程酒店訂單Elasticsearch實戰

隨著訂單量的日益增長,單個數據庫的讀寫能力開始捉襟見肘。這種情況下,對數據庫進行分片變得順理成章。

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

分片之後的寫,只要根據分片的維度進行取模即可。可是多維度的查詢應該如何處理呢?

一片一片的查詢,然後在內存裡面聚合是一種方式,可是缺點顯而易見。

對於那些無數據返回分片的查詢,不僅對應用服務器是一種額外的性能消耗,對寶貴的數據庫資源也是一種不必要的負擔。

至於查詢性能,雖然可以通過開線程併發查詢進行改善,但是多線程編程以及對數據庫返回結果的聚合,增加了編程的複雜性和易錯性。可以試想一下分片後的分頁查詢如何實現,便可有所體會。

所以我們選擇對分片後的數據庫建立實時索引,把查詢收口到一個獨立的 Web Service,在保證性能的前提下,提升業務應用查詢時的便捷性。那問題就來了,如何建立高效的分片索引呢?

索引技術的選型

實時索引的數據會包含常見查詢中所用到的列,例如用戶 ID,用戶電話,用戶地址等,實時複製分發一份到一個獨立的存儲介質上。

查詢時,會先查索引,如果索引中已經包含所需要的列,直接返回數據即可。

如果需要額外的數據,可以根據分片維度進行二次查詢。因為已經能確定具體的分片,所以查詢也會高效。

為什麼沒有使用數據庫索引

數據庫索引是一張表的所選列的數據備份。

由於得益於包含了低級別的磁盤塊地址或者直接鏈接到原始數據的行,查詢效率非常高效。

優點是數據庫自帶的索引機制是比較穩定可靠且高效的;缺陷是隨著查詢場景的增多,索引的量會隨之上升。

訂單自身的屬性隨著業務的發展已經達到上千,高頻率查詢的維度可多達幾十種,組合之後的變形形態可達上百種。

而索引本身並不是沒有代價的,每次增刪改都會有額外的寫操作,同時佔用額外的物理存儲空間。

索引越多,數據庫索引維護的成本越大。所以還有其他選擇麼?

開源搜索引擎的選擇

當時閃現在我們腦中的是開源搜索引擎 Apache Solr 和 Elastic Search。

Solr 是一個建立在 Java 類庫 Lucene 之上的開源搜索平臺,以一種更友好的方式提供 Lucene 的搜索能力。

它已經存在十年之久,是一款非常成熟的產品,提供分佈式索引、複製分發、負載均衡查詢,自動故障轉移和恢復功能。

Elastic Search 也是一個建立在 Lucene 之上的分佈式 RESTful 搜索引擎。

通過 RESTful 接口和 Schema Fee JSON 文檔,提供分佈式全文搜索引擎。每個索引可以被分成多個分片,每個分片可以有多個備份。

兩者對比各有優劣:

  • 在安裝和配置方面,得益於產品較新,Elastic Search 更輕量級以及易於安裝使用。
  • 在搜索方面,撇開大家都有的全文搜索功能,Elastic Search 在分析性查詢中有更好的性能。
  • 在分佈式方面,Elastic Search 支持在一個服務器上存在多個分片,並且隨著服務器的增加,自動平衡分片到所有的機器。
  • 社區與文檔方面,Solr 得益於其資歷,有更多的積累。

根據 Google Trends 的統計,Elastic Search 比 Solr 有更廣泛的關注度。

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

最終我們選擇了 Elastic Search,看中的是它的輕量級、易用和對分佈式更好的支持,整個安裝包也只有幾十兆。

複製分發的實現

為了避免重複造輪子,我們嘗試尋找現存組件。由於數據庫是 SQL Server 的,所以沒有找到合適的開源組件。

SQL Server 本身有實時監控增刪改的功能,把更新後的數據寫到單獨的一張表。

但是它並不能自動把數據寫到 Elastic Search,也沒有提供相關的 API 與指定的應用進行通訊,所以我們開始嘗試從應用層面去實現複製分發。

為什麼沒有使用數據訪問層複製分發

首先進入我們視線的是數據訪問層,它可能是一個突破口。每當應用對數據庫進行增刪改時,實時寫一條數據到 Elastic Search。

但是考慮到以下情況後,我們決定另闢蹊徑:

有幾十個應用在訪問數據庫,有幾十個開發都在改動數據訪問層的代碼。如果要實現數據層的複製分發,必須對現有十幾年的代碼進行肉眼掃描,然後進行修改。開發的成本和易錯性都很高。

每次增刪改時都寫 Elastic Search,意味著業務處理邏輯與複製分發強耦合。

Elastic Search 或相關其他因素的不穩定,會直接導致業務處理的不穩定。

異步開線程寫 Elastic Search?那如何處理應用發佈重啟的場景?加入大量異常處理和重試的邏輯?然後以 JAR 的形式引用到幾十個應用?一個小 Bug 引起所有相關應用的不穩定?

實時掃描數據庫

初看這是一種很低效的方案,但是在結合以下實際場景後,它卻是一種簡單、穩定、高效的方案:

  • 零耦合。相關應用無需做任何改動,不會影響業務處理效率和穩定性。
  • 批量寫 Elastic Search。由於掃描出來的都是成批的數據,可以批量寫入 Elastic Search,避免 Elastic Search 由於過多單個請求,頻繁刷新緩存。
  • 存在大量毫秒級併發的寫。掃描數據庫時無返回數據意味著額外的數據庫性能消耗,我們的場景寫的併發和量都非常大,所以這種額外消耗可以接受。
  • 不刪除數據。掃描數據庫無法掃描出刪除的記錄,但是訂單相關的記錄都需要保留,所以不存在刪除數據的場景。

提高 Elastic Search 寫的吞吐量

由於是對數據庫的實時複製分發,效率和併發量要求都會較高。以下是我們對 Elastic Search 的寫所採用的一些優化方案:

使用 upsert 替代 select + insert/update

類似於 MySQL 的 replace into,避免多次請求,成倍節省多次請求帶來的性能消耗。

使用 bulkrequest,把多個請求合併在一個請求裡面

Elastic Search 的工作機制對批量請求有較好的性能,例如 translog 的持久化默認是 request 級別的,這樣寫硬盤的次數就會大大降低,提高寫的性能。

至於具體一個批次多少個請求,這個與服務器配置、索引結構、數據量都有關係。可以使用動態配置的方式,在生產上面調試。

對於實時性要求不高的索引

把 index.refresh_interval 設置為 30 秒(默認是 1 秒),這樣可以讓 Elastic Search 每 30 秒創建一個新的 segment,減輕後面的 flush 和 merge 壓力。

提前設置索引的 schema,去除不需要的功能

例如默認對 string 類型的映射會同時建立 keyword 和 text 索引,前者適合於完全匹配的短信息,例如郵寄地址、服務器名稱,標籤等。

而後者適合於一片文章中的某個部分的查詢,例如郵件內容、產品說明等。

根據具體查詢的場景,選擇其中一個即可,如下圖:

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

對於不關心查詢結果評分的字段,可以設置為 norms:false。

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

對於不會使用 phrase query 的字段,設置 index_options:freqs。

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

對於能接受數據丟失的索引或者有災備服務器的場景

把 index.translog.durability 設置成 async(默認是 request)。把對 lucene 的寫持久化到硬盤是一個相對昂貴的操作,所以會有 translog 先持久化到硬盤,然後批量寫入 lucene。

異步寫 translog 意味著無需每個請求都去寫硬盤,能提高寫的性能。在數據初始化的時候效果比較明顯,後期實時寫入使用 bulkrequest 能滿足大部分的場景。

提高 Elastic Search 讀的性能

為了提高查詢的性能,我們做了以下優化:寫的時候指定查詢場景最高的字段為 _routing 的值。

由於 Elastic Search 的分佈式分區原則默認是對文檔 id 進行哈希和取模決定分片,所以如果把查詢場景最高的字段設為 _routing 的值就能保證在對該字段查詢時,只要查一個分片即可返回結果。

寫:

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

查:

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

對於日期類型,在業務能夠接受的範圍內,儘可能降低精確度。能只包含年月日,就不要包含時分秒。

當數據量較大時,這個優化的效果會特別的明顯。因為精度越低意味著緩存的命中率越高,查詢的速度就會越快。

同時內存的重複利用也會提升 Elastic Search 服務器的性能,降低 CPU 的使用率,減少 GC 的次數。

系統監控的實現

技術中心專門為業務部門開發了一套監控系統。它會週期性的調用所有服務器的 Elastic Search CAT API,把性能數據保存在單獨的 Elastic Search 服務器中,同時提供一個網頁給應用負責人進行數據的監控。

搜索引擎怎麼選?攜程酒店訂單Elasticsearch實戰

災備的實現

Elastic Search 本身是分佈式的。在創建索引時,我們根據未來幾年的數據總量進行了分片,確保單片數據總量在一個健康的範圍內。

為了在寫入速度和災備之間找到一個平衡點,把備份節點設置為 2。

所以數據分佈在不同的服務器上,如果集群中的一個服務器宕機,另外一個備份服務器會直接進行服務。

同時為了防止一個機房發生斷網或者斷電等突發情況,而導致整個集群不能正常工作,我們專門在不同地區的另一個機房部署了一套完全一樣的 Elastic Search 集群。

日常數據在複製分發的時候,會同時寫一份到災備機房以防不時之需。

總結

整個項目的開發是一個逐步演進的過程,實現過程中也遇到了大量問題。

項目上線後,應用服務器的 CPU 與內存都有大幅下降,同時查詢速度與沒有分片之前基本持平。在此分享遇到的問題和解決問題的思路,供大家參考。

參考:

  • Elastic Search官方文檔;
  • https://en.wikipedia.org/wiki/Database_index
  • https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B4%A2%E5%BC%95
  • https://logz.io/blog/solr-vs-elasticsearch/


分享到:


相關文章: