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