最近一年使用 Elasticsearch 完成億級(jí)別日志搜索平臺(tái)「ELK」,億級(jí)別的分布式跟蹤系統(tǒng)。在設(shè)計(jì)這些系統(tǒng)的過程中,底層都是采用 Elasticsearch 來做數(shù)據(jù)的存儲(chǔ),并且數(shù)據(jù)量都超過億級(jí)別,甚至達(dá)到百億級(jí)別。
所以趁著有空,就花點(diǎn)時(shí)間整理一下具體怎么做 Elasticsearch 性能優(yōu)化,希望能對(duì) Elasticsearch 感興趣的同學(xué)有所幫助。
背景Elasticsearch 是一個(gè)基于 Lucene 的搜索服務(wù)器。它提供了一個(gè)分布式多用戶能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 開發(fā)的,并作為 Apache 許可條款下的開放源碼發(fā)布,是當(dāng)前流行的企業(yè)級(jí)搜索引擎。設(shè)計(jì)用于云計(jì)算中,能夠達(dá)到實(shí)時(shí)搜索,穩(wěn)定,可靠,快速,安裝使用方便。
作為一個(gè)開箱即用的產(chǎn)品,在生產(chǎn)環(huán)境上線之后,我們其實(shí)不一定能確保其的性能和穩(wěn)定性。如何根據(jù)實(shí)際情況提高服務(wù)的性能,其實(shí)有很多技巧。
下面我就從三個(gè)方面分別來講解下優(yōu)化服務(wù)的性能:
索引優(yōu)化主要是在 Elasticsearch 插入層面優(yōu)化,如果瓶頸不在這塊,而是在產(chǎn)生數(shù)據(jù)部分,比如 DB 或者 Hadoop 上,那么優(yōu)化方向就需要改變下。同時(shí),Elasticsearch 本身索引速度其實(shí)還是蠻快的,具體數(shù)據(jù),我們可以參考官方的 benchmark 數(shù)據(jù)。
批量提交當(dāng)有大量數(shù)據(jù)提交的時(shí)候,建議采用批量提交。
比如在做 ELK 過程中 ,Logstash indexer 提交數(shù)據(jù)到 Elasticsearch 中 ,batch size 就可以作為一個(gè)優(yōu)化功能點(diǎn)。但是優(yōu)化 size 大小需要根據(jù)文檔大小和服務(wù)器性能而定。
像 Logstash 中提交文檔大小超過 20MB ,Logstash 會(huì)請(qǐng)一個(gè)批量請(qǐng)求切分為多個(gè)批量請(qǐng)求。
如果在提交過程中,遇到 EsRejectedExecutionException 異常的話,則說明集群的索引性能已經(jīng)達(dá)到極限了。這種情況,要么提高服務(wù)器集群的資源,要么根據(jù)業(yè)務(wù)規(guī)則,減少數(shù)據(jù)收集速度,比如只收集 Warn、Error 級(jí)別以上的日志。
優(yōu)化硬件優(yōu)化硬件設(shè)備一直是最快速有效的手段。
為了提高索引性能,Elasticsearch 在寫入數(shù)據(jù)時(shí)候,采用延遲寫入的策略,即數(shù)據(jù)先寫到內(nèi)存中,當(dāng)超過默認(rèn) 1 秒 (index.refresh_interval)會(huì)進(jìn)行一次寫入操作,就是將內(nèi)存中 segment 數(shù)據(jù)刷新到操作系統(tǒng)中,此時(shí)我們才能將數(shù)據(jù)搜索出來,所以這就是為什么 Elasticsearch 提供的是近實(shí)時(shí)搜索功能,而不是實(shí)時(shí)搜索功能。
當(dāng)然像我們的內(nèi)部系統(tǒng)對(duì)數(shù)據(jù)延遲要求不高的話,我們可以通過延長 refresh 時(shí)間間隔,可以有效的減少 segment 合并壓力,提供索引速度。在做全鏈路跟蹤的過程中,我們就將 index.refresh_interval 設(shè)置為 30s,減少 refresh 次數(shù)。
同時(shí),在進(jìn)行全量索引時(shí),可以將 refresh 次數(shù)臨時(shí)關(guān)閉,即 index.refresh_interval 設(shè)置為 -1,數(shù)據(jù)導(dǎo)入成功后再打開到正常模式,比如 30s。
減少副本數(shù)量Elasticsearch 默認(rèn)副本數(shù)量為 3 個(gè),雖然這樣會(huì)提高集群的可用性,增加搜索的并發(fā)數(shù),但是同時(shí)也會(huì)影響寫入索引的效率。
在索引過程中,需要把更新的文檔發(fā)到副本節(jié)點(diǎn)上,等副本節(jié)點(diǎn)生效后在進(jìn)行返回結(jié)束。使用 Elasticsearch 做業(yè)務(wù)搜索的時(shí)候,建議副本數(shù)目還是設(shè)置為 3 個(gè),但是像內(nèi)部 ELK 日志系統(tǒng)、分布式跟蹤系統(tǒng)中,完全可以將副本數(shù)目設(shè)置為 1 個(gè)。
查詢效率優(yōu)化 路由當(dāng)我們查詢文檔的時(shí)候,Elasticsearch 如何知道一個(gè)文檔應(yīng)該存放到哪個(gè)分片中呢?它其實(shí)是通過下面這個(gè)公式來計(jì)算出來
shard = hash(routing) % number_of_primary_shards
routing 默認(rèn)值是文檔的 id,也可以采用自定義值,比如用戶 id。
不帶 routing 查詢在查詢的時(shí)候因?yàn)椴恢酪樵兊臄?shù)據(jù)具體在哪個(gè)分片上,所以整個(gè)過程分為 2 個(gè)步驟
查詢的時(shí)候,可以直接根據(jù) routing 信息定位到某個(gè)分配查詢,不需要查詢所有的分配,經(jīng)過協(xié)調(diào)節(jié)點(diǎn)排序。
向上面自定義的用戶查詢,如果 routing 設(shè)置為 userid 的話,就可以直接查詢出數(shù)據(jù)來,效率提升很多。
Filter VS QueryEbay 曾經(jīng)分享過他們使用 Elasticsearch 的經(jīng)驗(yàn)中說到:
Use filter context instead of query context if possible.
盡可能使用過濾器上下文(Filter)替代查詢上下文(Query
Elasticsearch 針對(duì) Filter 查詢只需要回答「是」或者「否」,不需要像 Query 查詢一下計(jì)算相關(guān)性分?jǐn)?shù),同時(shí) Filter 結(jié)果可以緩存。
大翻頁在使用 Elasticsearch 過程中,應(yīng)盡量避免大翻頁的出現(xiàn)。
正常翻頁查詢都是從 From 開始 Size 條數(shù)據(jù),這樣就需要在每個(gè)分片中查詢打分排名在前面的 From + Size 條數(shù)據(jù)。協(xié)同節(jié)點(diǎn)收集每個(gè)分配的前 From + Size 條數(shù)據(jù)。協(xié)同節(jié)點(diǎn)一共會(huì)受到 N * ( From + Size )條數(shù)據(jù),然后進(jìn)行排序,再將其中 From 到 From + Size 條數(shù)據(jù)返回出去。
如果 From 或者 Size 很大的話,導(dǎo)致參加排序的數(shù)量會(huì)同步擴(kuò)大很多,最終會(huì)導(dǎo)致 CPU 資源消耗增大。
可以通過使用 Elasticsearch scroll 和 scroll-scan 高效滾動(dòng)的方式來解決這樣的問題。具體寫法,可以參考 Elasticsearch: 權(quán)威指南 - scroll 查詢
JVM 設(shè)置 32G 現(xiàn)象Elasticsearch 默認(rèn)安裝后設(shè)置的堆內(nèi)存是 1 GB。 對(duì)于任何一個(gè)業(yè)務(wù)部署來說, 這個(gè)設(shè)置都太小了。
比如機(jī)器有 64G 內(nèi)存,那么我們是不是設(shè)置的越大越好呢?
其實(shí)不是的。
主要 Elasticsearch 底層使用 Lucene。Lucene 被設(shè)計(jì)為可以利用操作系統(tǒng)底層機(jī)制來緩存內(nèi)存數(shù)據(jù)結(jié)構(gòu)。 Lucene 的段是分別存儲(chǔ)到單個(gè)文件中的。因?yàn)槎问遣豢勺兊?,這些文件也都不會(huì)變化,這是對(duì)緩存友好的,同時(shí)操作系統(tǒng)也會(huì)把這些段文件緩存起來,以便更快的訪問。
如果你把所有的內(nèi)存都分配給 Elasticsearch 的堆內(nèi)存,那將不會(huì)有剩余的內(nèi)存交給 Lucene。 這將嚴(yán)重地影響全文檢索的性能。
標(biāo)準(zhǔn)的建議是把 50% 的可用內(nèi)存作為 Elasticsearch 的堆內(nèi)存,保留剩下的 50%。當(dāng)然它也不會(huì)被浪費(fèi),Lucene 會(huì)很樂意利用起余下的內(nèi)存。
同時(shí)了解過 ES 的同學(xué)都聽過過「不要超過 32G」的說法吧。
其實(shí)主要原因是 :JVM 在內(nèi)存小于 32 GB 的時(shí)候會(huì)采用一個(gè)內(nèi)存對(duì)象指針壓縮技術(shù)。
在 Java 中,所有的對(duì)象都分配在堆上,并通過一個(gè)指針進(jìn)行引用。 普通對(duì)象指針(OOP)指向這些對(duì)象,通常為 CPU 字長 的大小:32 位或 64 位,取決于你的處理器。指針引用的就是這個(gè) OOP 值的字節(jié)位置。
對(duì)于 32 位的系統(tǒng),意味著堆內(nèi)存大小大為 4 GB。對(duì)于 64 位的系統(tǒng), 可以使用更大的內(nèi)存,但是 64 位的指針意味著更大的浪費(fèi),因?yàn)槟愕闹羔槺旧泶罅?。更糟糕的是?更大的指針在主內(nèi)存和各級(jí)緩存(例如 LLC,L1 等)之間移動(dòng)數(shù)據(jù)的時(shí)候,會(huì)占用更多的帶寬.
所以最終我們都會(huì)采用 31 G 設(shè)置
-Xms 31g
-Xmx 31g
假設(shè)你有個(gè)機(jī)器有 128 GB 的內(nèi)存,你可以創(chuàng)建兩個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)內(nèi)存分配不超過 32 GB。 也就是說不超過 64 GB 內(nèi)存給 ES 的堆內(nèi)存,剩下的超過 64 GB 的內(nèi)存給 Lucene
參考我在51CTO博客開了一個(gè)專欄——《帶你玩轉(zhuǎn)高可用》希望可以幫助有一定分布式系統(tǒng)架構(gòu)知識(shí),在架構(gòu)方面力求進(jìn)階的分布式系統(tǒng)架構(gòu)從業(yè)人員,提高架構(gòu)可用性,實(shí)現(xiàn)高可用目標(biāo)。
《帶你玩轉(zhuǎn)高可用帶你玩轉(zhuǎn)高可用》