一起學(xué)Elasticsearch的寫入和檢索調(diào)優(yōu)
當(dāng)涉及到大規(guī)模數(shù)據(jù)存儲和檢索時,Elasticsearch以其快速、高效和強(qiáng)大的搜索能力而聞名,并被廣泛應(yīng)用于各種場景,例如日志分析、全文搜索和實時數(shù)據(jù)分析。
然而,并不是只要將數(shù)據(jù)存入ES就可以立即獲得最佳性能和查詢效率。正如任何強(qiáng)大的工具一樣,ES也需要進(jìn)行調(diào)優(yōu),以充分發(fā)揮其潛力并滿足特定業(yè)務(wù)需求。
在這篇文章中,我們將探討ES寫入調(diào)優(yōu)和查詢調(diào)優(yōu)的關(guān)鍵方面,并提供一些實用的技巧和建議,幫助您優(yōu)化ES集群的性能和響應(yīng)速度。
寫入調(diào)優(yōu)
基本原則
寫入性能調(diào)優(yōu)是建立在 Elasticsearch 的寫入原理之上的。
ES 數(shù)據(jù)寫入具有一定的延時性,這是為了減少頻繁的索引文件產(chǎn)生。默認(rèn)情況下 ES 每秒生成一個 Segment 文件,當(dāng)達(dá)到一定閾值的時候會執(zhí)行merge,merge 過程發(fā)生在 JVM中,頻繁的生成 Segmen 文件可能會導(dǎo)致頻繁的觸發(fā) FGC,導(dǎo)致 OOM。
為了避免這種情況,通常采取的手段是降低 Segment 文件的生成頻率,辦法有兩個:一個是增加時間閾值,另一個是增大Buffer的空間閾值,因為緩沖區(qū)寫滿也會生成 Segment 文件。
生產(chǎn)經(jīng)常面臨的寫入可以分為兩種情況:
高頻低量:高頻的創(chuàng)建或更新索引或文檔,一般發(fā)生在 C 端業(yè)務(wù)場景下。
低頻高量:一般情況為定期重建索引或批量更新文檔數(shù)據(jù)。
在搜索引擎的業(yè)務(wù)場景下,用戶一般并不需要那么高的寫入實時性。比如你在網(wǎng)站發(fā)布一條征婚信息,或者二手交易平臺發(fā)布一個商品信息。其他人并不是馬上能搜索到的,這其實也是正常的處理邏輯。
這個延時的過程需要處理很多事情,比如:你的信息需要后臺審核。
你發(fā)布的內(nèi)容在搜索服務(wù)中需要建立索引,而且你的數(shù)據(jù)可能并不會馬上被寫入索引,而是等待要寫入的數(shù)據(jù)達(dá)到一定數(shù)量之后,批量寫入。
這種操作優(yōu)點(diǎn)類似于我們快遞物流的場景,只有當(dāng)快遞數(shù)量達(dá)到一定量級的時候,比如能裝滿整個車的時候,快遞車才會發(fā)車。因為反正是要跑一趟,裝的越多,平均成本越低。
這和我們數(shù)據(jù)寫入到磁盤的過程是非常相似的,我們可以把一條文檔數(shù)據(jù)看做是一個快遞,而快遞車每次發(fā)車就是向磁盤寫入數(shù)據(jù)的一個過程,這個過程不宜太多,太多只會降低性能,就是體現(xiàn)在運(yùn)輸成本上面,而對于我們數(shù)據(jù)寫入而言就是體現(xiàn)在我們硬件性能損耗上面。
優(yōu)化手段
以下為常見數(shù)據(jù)寫入的調(diào)優(yōu)手段,寫入調(diào)優(yōu)均以提升寫入吞吐量和并發(fā)能力為目標(biāo),而非提升寫入實時性。
增加 flush 時間間隔
flush的過程是非常消耗資源的。增加flush的時間間隔目的是減小數(shù)據(jù)寫入磁盤的頻率,降低磁盤IO頻率。
增加 refresh_interval 參數(shù)的值
增加 refresh_interval 參數(shù)的值,目的是減少segment文件的創(chuàng)建,降低merge次數(shù),因為merge是發(fā)生在jvm中的,有可能導(dǎo)致full GC。
ES的 refresh 行為非常昂貴,并且在正在進(jìn)行的索引活動時經(jīng)常調(diào)用,會降低索引速度。
默認(rèn)情況下,Elasticsearch 每秒定期刷新索引,如果沒有搜索流量或搜索流量很少(例如每 5 分鐘不到一個搜索請求),可以適當(dāng)調(diào)大此參數(shù)的值。
增加Buffer大小
本質(zhì)也是減小refresh的時間間隔,因為導(dǎo)致segment文件創(chuàng)建的原因不僅有時間閾值,還有buffer空間大小,寫滿了也會創(chuàng)建。默認(rèn)值為JVM 空間的10%。
關(guān)閉副本
當(dāng)需要單次寫入大量數(shù)據(jù)的時候,建議關(guān)閉副本,暫停搜索服務(wù),或選擇在檢索請求量谷值區(qū)間時間段來完成。
關(guān)閉副本可以帶來如下好處:
- 減小讀寫之間的資源搶占,讀寫分離。
- 當(dāng)檢索請求數(shù)量很少的時候,可以減少甚至完全刪除副本分片,關(guān)閉segment的自動創(chuàng)建以達(dá)到高效利用內(nèi)存的目的,因為副本的存在會導(dǎo)致主從之間頻繁的進(jìn)行數(shù)據(jù)同步,大大增加服務(wù)器的資源占用。
具體可通過設(shè)置index.number_of_replicas 為0以加快索引速度。沒有副本意味著丟失單個節(jié)點(diǎn)可能會導(dǎo)致數(shù)據(jù)丟失,因此數(shù)據(jù)保存在其他地方很重要,以便在出現(xiàn)問題時可以重試初始加載。初始加載完成后,可以設(shè)置index.number_of_replicas改回其原始值。
禁用swap
大多數(shù)操作系統(tǒng)嘗試將盡可能多的內(nèi)存用于文件系統(tǒng)緩存,并急切地?fù)Q掉未使用的應(yīng)用程序內(nèi)存。這可能導(dǎo)致部分 JVM 堆甚至其可執(zhí)行頁面被換出到磁盤。
交換對性能和節(jié)點(diǎn)穩(wěn)定性非常不利,應(yīng)該不惜一切代價避免。它可能導(dǎo)致垃圾收集持續(xù)幾分鐘而不是幾毫秒,并且可能導(dǎo)致節(jié)點(diǎn)響應(yīng)緩慢甚至與集群斷開連接。在Elastic分布式系統(tǒng)中,讓操作系統(tǒng)殺死節(jié)點(diǎn)更有效。
使用多個工作線程
發(fā)送批量請求的單個線程不太可能最大化 Elasticsearch 集群的索引容量。為了使用集群的所有資源,應(yīng)該從多個線程或進(jìn)程發(fā)送數(shù)據(jù)。除了更好地利用集群的資源外,還有助于降低每個 fsync 的成本。
確保注意 TOO_MANY_REQUESTS 響應(yīng)代碼:429。(EsRejectedExecutionException使用 Java 客戶端),這是 Elasticsearch 告訴我們它無法跟上當(dāng)前索引速度的方式。發(fā)生這種情況時,應(yīng)該在重試之前暫停索引,最好使用隨機(jī)指數(shù)退避。
與調(diào)整批量請求的大小類似,只有測試才能確定最佳工作線程數(shù)量是多少。這可以通過逐漸增加線程數(shù)量來測試,直到集群上的 I/O 或 CPU 飽和。
max_result_window參數(shù)
max_result_window是分頁返回的最大數(shù)值,默認(rèn)值為10000。max_result_window本身是對JVM的一種保護(hù)機(jī)制,通過設(shè)定一個合理的閾值,避免初學(xué)者分頁查詢時由于單頁數(shù)據(jù)過大而導(dǎo)致OOM。
設(shè)置一個合理的大小是需要通過你的各項指標(biāo)參數(shù)來衡量確定的,比如你用戶量、數(shù)據(jù)量、物理內(nèi)存的大小、分片的數(shù)量等等。通過監(jiān)控數(shù)據(jù)和分析各項指標(biāo)從而確定一個最佳值,并非越大越好。
查詢調(diào)優(yōu)
讀寫性能不可兼得
首先要明確一點(diǎn):魚和熊掌不可兼得。讀寫性能調(diào)優(yōu)在很多場景下是只能二選一的。犧牲 A 換 B 的行為非常常見。索引本質(zhì)上也是通過空間換取時間。犧牲寫入實時性就是為了提高檢索的性能。
當(dāng)你在二手平臺或者某垂直信息網(wǎng)站發(fā)布信息之后,是允許有信息寫入的延時性的。但是檢索不行,甚至 1 秒的等待時間對用戶來說都是無法接受的。滿足用戶的要求甚至必須做到10 ms以內(nèi)。
優(yōu)化手段
避免單次召回大量數(shù)據(jù)
搜索引擎最擅長的事情是從海量數(shù)據(jù)中查詢少量相關(guān)文檔,而非單次檢索大量文檔。非常不建議動輒查詢上萬數(shù)據(jù)。如果有這樣的需求,建議使用滾動查詢
避免單個文檔過大
鑒于默認(rèn)http.max_content_length設(shè)置為 100MB,Elasticsearch 將拒絕索引任何大于該值的文檔。您可能決定增加該特定設(shè)置,但 Lucene 仍然有大約 2GB 的限制。
即使不考慮硬性限制,大型文檔通常也不實用。大型文檔對網(wǎng)絡(luò)、內(nèi)存使用和磁盤造成了更大的壓力,即使對于不請求的搜索請求也是如此。
有時重新考慮信息單元應(yīng)該是什么是有用的。例如,您想讓書籍可搜索的事實并不一定意味著文檔應(yīng)該包含整本書。使用章節(jié)甚至段落作為文檔可能是一個更好的主意,然后在這些文檔中擁有一個屬性來標(biāo)識它們屬于哪本書。這不僅避免了大文檔的問題,還使搜索體驗更好。例如,如果用戶搜索兩個單詞 fooand bar,則不同章節(jié)之間的匹配可能很差,而同一段落中的匹配可能很好。
單次查詢10條文檔 好于 10次查詢每次一條
批量請求將產(chǎn)生比單文檔索引請求更好的性能。但是每次查詢多少文檔最佳,不同的集群最佳值可能不同,為了獲得批量請求的最佳閾值,建議在具有單個分片的單個節(jié)點(diǎn)上運(yùn)行基準(zhǔn)測試。
首先嘗試一次索引 100 個文檔,然后是 200 個,然后是 400 個等。在每次基準(zhǔn)測試運(yùn)行中,批量請求中的文檔數(shù)量翻倍。當(dāng)索引速度開始趨于平穩(wěn)時,就可以獲得已達(dá)到數(shù)據(jù)批量請求的最佳大小。在相同性能的情況下,當(dāng)大量請求同時發(fā)送時,太大的批量請求可能會使集群承受內(nèi)存壓力,因此建議避免每個請求超過幾十兆字節(jié)。
數(shù)據(jù)建模
很多人會忽略對 Elasticsearch 數(shù)據(jù)建模的重要性。
nested屬于object類型的一種,是Elasticsearch中用于復(fù)雜類型對象數(shù)組的索引操作。Elasticsearch沒有內(nèi)部對象的概念,因此,ES在存儲復(fù)雜類型的時候會把對象的復(fù)雜層次結(jié)果扁平化為一個鍵值對列表。
特別是,應(yīng)避免Join連接。Nested 可以使查詢慢幾倍,Join 會使查詢慢數(shù)百倍。兩種類型的使用場景應(yīng)該是:Nested針對字段值為非基本數(shù)據(jù)類型的時候,而Join則用于當(dāng)子文檔數(shù)量級非常大的時候。
給系統(tǒng)留足夠的內(nèi)存
Lucene的數(shù)據(jù)的fsync是發(fā)生在OS cache的,要給OS cache預(yù)留足夠的內(nèi)存大小。
預(yù)索引
利用查詢中的模式來優(yōu)化數(shù)據(jù)的索引方式。例如,如果所有文檔都有一個price字段,并且大多數(shù)查詢 range 在固定的范圍列表上運(yùn)行聚合,可以通過將范圍預(yù)先索引到索引中并使用聚合來加快聚合速度。
使用 filter 代替 query
query和filter的主要區(qū)別在:filter是結(jié)果導(dǎo)向的而query是過程導(dǎo)向。query傾向于“當(dāng)前文檔和查詢的語句的相關(guān)度”,而filter傾向于“當(dāng)前文檔和查詢的條件是不是相符”。即在查詢過程中,query是要對查詢的每個結(jié)果計算相關(guān)性得分的,而filter不會。另外filter有相應(yīng)的緩存機(jī)制,可以提高查詢效率。
避免深度分頁
避免單頁數(shù)據(jù)過大,可以參考百度或者淘寶的做法。es提供兩種解決方案 scroll search 和 search after。
使用 Keyword 類型
并非所有數(shù)值數(shù)據(jù)都應(yīng)映射為數(shù)值字段數(shù)據(jù)類型。Elasticsearch為查詢優(yōu)化數(shù)字字段,例如integeror long。如果不需要范圍查找,對于 term查詢而言,keyword 比 integer 性能更好。
避免使用腳本
Scripting是Elasticsearch支持的一種專門用于復(fù)雜場景下支持自定義編程的強(qiáng)大的腳本功能。相對于 DSL 而言,腳本的性能更差,DSL能解決 80% 以上的查詢需求,如非必須,盡量避免使用 Script。