#作者:獵人
文章目錄
- 前言
- 搜索慢查詢日志
- 索引慢寫入日志
- 性能調優之基本優化建議
- 性能調優之索引寫入性能優化
- 提升es集群寫入性能方法:
- 性能調優之集群讀性能優化
- 性能調優之搜索性能優化
- 性能調優之GC優化
- 性能調優之路由優化
- 性能調優之分片優化
前言
es里面的操作,主要分為兩種,一種寫入(增刪改),另一種是查詢(搜索)。分別要識別出來哪些寫入操作性能比較慢,哪些查詢操作性能比較慢,先要識別出來有性能問題的這些慢查詢,慢寫入,然后才能去考慮如何優化寫入性能,如何優化搜索性能。如果一次搜索,或者一次聚合,一下子就要10s,或者十幾秒,不能接受。
搜索慢查詢日志
無論是慢查詢日志,還是慢寫入日志,都是針對shard級別的,無論執行增刪改,還是執行搜索,都是對某個數據執行寫入或者是搜索,其實都是到某個shard上面去執行。 shard上面執行的慢的寫入或者是搜索,都會記錄在針對這個shard的日志中。比如設置一個閾值,5s就是搜索的閾值,5s就叫做慢,那么一旦一個搜索請求超過了5s之后,就會記錄一條慢搜索日志到日志文件中。
shard level的搜索慢查詢日志,輝將搜索性能較慢的查詢寫入一個專門的日志文件中。可以針對query phase和fetch phase單獨設置慢查詢的閾值,而具體的慢查詢閾值設置如下所示:
vim elasticsearch.yml
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.query.trace: 500msindex.search.slowlog.threshold.fetch.warn: 1s
index.search.slowlog.threshold.fetch.info: 800ms
index.search.slowlog.threshold.fetch.debug: 500ms
index.search.slowlog.threshold.fetch.trace: 200ms
而慢查詢日志具體的格式,都是在log4j2.properties中配置的,比如下面的配置:
appender.index_search_slowlog_rolling.type = RollingFile
appender.index_search_slowlog_rolling.name = index_search_slowlog_rolling
appender.index_search_slowlog_rolling.fileName = ${sys:es.logs}_index_search_slowlog.log
appender.index_search_slowlog_rolling.layout.type = PatternLayout
appender.index_search_slowlog_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %.10000m%n
appender.index_search_slowlog_rolling.filePattern = ${sys:es.logs}_index_search_slowlog-%d{yyyy-MM-dd}.log
appender.index_search_slowlog_rolling.policies.type = Policies
appender.index_search_slowlog_rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.index_search_slowlog_rolling.policies.time.interval = 1
appender.index_search_slowlog_rolling.policies.time.modulate = truelogger.index_search_slowlog_rolling.name = index.search.slowlog
logger.index_search_slowlog_rolling.level = trace
logger.index_search_slowlog_rolling.appenderRef.index_search_slowlog_rolling.ref = index_search_slowlog_rolling
logger.index_search_slowlog_rolling.additivity = false
索引慢寫入日志
可以用如下的配置來設置索引寫入慢日志的閾值:
index.indexing.slowlog.threshold.index.warn: 10s
index.indexing.slowlog.threshold.index.info: 5s
index.indexing.slowlog.threshold.index.debug: 2s
index.indexing.slowlog.threshold.index.trace: 500ms
index.indexing.slowlog.level: info
index.indexing.slowlog.source: 1000
用下面的log4j.properties配置就可以設置索引慢寫入日志的格式:
appender.index_indexing_slowlog_rolling.type = RollingFile
appender.index_indexing_slowlog_rolling.name = index_indexing_slowlog_rolling
appender.index_indexing_slowlog_rolling.fileName = ${sys:es.logs}_index_indexing_slowlog.log
appender.index_indexing_slowlog_rolling.layout.type = PatternLayout
appender.index_indexing_slowlog_rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %marker%.10000m%n
appender.index_indexing_slowlog_rolling.filePattern = ${sys:es.logs}_index_indexing_slowlog-%d{yyyy-MM-dd}.log
appender.index_indexing_slowlog_rolling.policies.type = Policies
appender.index_indexing_slowlog_rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.index_indexing_slowlog_rolling.policies.time.interval = 1
appender.index_indexing_slowlog_rolling.policies.time.modulate = truelogger.index_indexing_slowlog.name = index.indexing.slowlog.index
logger.index_indexing_slowlog.level = trace
logger.index_indexing_slowlog.appenderRef.index_indexing_slowlog_rolling.ref = index_indexing_slowlog_rolling
logger.index_indexing_slowlog.additivity = false
正常情況下,慢查詢應該是比較少數的。如果發現某個查詢特別慢,就要通知寫這個查詢的開發人員,讓他們去優化一下性能。
設置 Slowlogs,發現一些性能不好,甚至是錯誤的使用 Pattern。例如:錯誤的將網址映射成 keyword,然后用通配符查詢。應該使用 Text,結合 URL 分詞器。嚴禁一切“x”開頭的通配符查詢
性能調優之基本優化建議
-
搜索結果不要返回過大的結果集
es是一個搜索引擎,所以如果用這個搜索引擎對大量的數據進行搜索,并且返回搜索結果中排在最前面的少數結果,是非常合適的。然而,如果要做成類似數據庫的東西,每次都進行大批量的查詢,是很不合適的。如果真的要做大批量結果的查詢,記得考慮用scroll api,批量滾動查詢。 -
避免超大的document
http.max_context_length的默認值是100mb,意味著一次document寫入時,document的內容不能超過100mb,否則es就會拒絕寫入。也許可以將這個參數設置的更大,從而讓超大的documdent可以寫入es,但是es底層的lucene引擎還是有一個2gb的最大限制。
即使不考慮引擎層的限制,超大的document在實際生產環境中是很不好的。超大document會耗費更多的網絡資源,內存資源和磁盤資源,甚至對那些不要求獲取_source的請求,也是一樣,因為es需要從_source中提取_id字段,對于超大document這個獲取_id字段的過程的資源開銷也是很大的。而將這種超大document寫入es也會使用大量的內存,占用內存空間的大小甚至會是documdent本身大小的數倍。近似匹配的搜索,比如phrase query,以及高亮顯示,對超大document的資源開銷會更大,因為這些操作的性能開銷直接跟document的大小成正比。
因此對于超大document,我們需要考慮一下,我們到底需要其中的哪些部分。舉例來說,如果我們要對一些書進行搜索,那么我們并不需要將整本書的內容就放入es中。可以僅僅使用每一篇章或者一個段落作為一個document,然后給一個field標識出來這些document屬于哪本書,這樣每個document的大小就變小了。可以避免超大document導致的各種開銷,同時可以優化搜索的體驗。
- 避免稀疏的數據
lucene的內核結構,跟稠密的數據配合起來,性能會更好, 原因就是,lucene在內部會通過doc id來唯一標識一個document,這個doc id是integer類型,范圍在0到索引中含有的document數量之間。這些doc id是用來在lucene內部的api之間進行通信的,比如說,對一個term用一個match query來進行搜索,就會產生一個doc id集合,然后這些doc id會用來獲取對應的norm值,以用來計算每個doc的相關度分數。而根據doc id查找norm的過程,是通過每個document的每個field保留一個字節來進行的一個算法,這個過程叫做norm查找,norm就是每個document的每個field保留的一個字節。對于每個doc id對應的那個norm值,可以通過讀取es一個內置索引,叫做doc_id的索引,中的一個字節來獲取。這個過程是性能很高的,而且可以幫助lucene快速的定位到每個document的norm值,但是同時這樣的話document本身就不需要存儲這一個字節的norm值了。
在實際運行過程中,這就意味著,如果一個索引有100個document,對于每個field,就需要100個字節來存儲norm值,即使100個document中只有10個document含有某個field,但是對那個field來說,還是要100個字節來存儲norm值。這就會對存儲產生更大的開銷,存儲空間被浪費的一個問題,而且也會影響讀寫性能。
下面有一些避免稀疏數據的辦法:
(1)避免將沒有任何關聯性的數據寫入同一個索引
必須避免將結構完全不一樣的數據寫入同一個索引中,因為結構完全不一樣的數據,field是完全不一樣的,會導致index數據非常稀疏。最好將這種數據寫入不同的索引中,如果這種索引數據量比較少,那么可以考慮給其很少的primary shard,比如1個,避免資源浪費。
(2)對document的結構進行規范化/標準化
即使要將不同類型的document寫入相同的索引中,可對不同類型的document進行標準化。如果所有的document都有一個時間戳field,不過有的叫做timestamp,有的叫做creation_date,那么可以將不同document的這個field重命名為相同的字段,盡量讓documment的結構相同。另外一個,就是比如有的document有一個字段,叫做goods_type,但是有的document沒有這個字段,此時可以對沒有這個字段的document,補充一個goods_type給一個默認值,比如default。
(3)避免使用多個types存儲不一樣結構的document
不建議在一個index中放很多個types來存儲不同類型的數據。但是其實不是這樣的,最好不要這么干,如果你在一個index中有多個type,但是這些type的數據結構不太一樣,那么這些type實際上底層都是寫到這個索引中的,還是會導致稀疏性。如果多個type的結構不太一樣,最好放入不同的索引中,不要寫入一個索引中。
(4)對稀疏的field禁用norms和doc_values
如果上面的步驟都沒法做,那么只能對那種稀疏的field,禁止norms和doc_values字段,因為這兩個字段的存儲機制類似,都是每個field有一個全量的存儲,對存儲浪費很大。如果一個field不需要考慮其相關度分數,那么可以禁用norms,如果不需要對一個field進行排序或者聚合,那么可以禁用doc_values字段。
性能調優之索引寫入性能優化
提高寫入性能就是提高吞吐量
- 用bulk批量寫入
調整bulk線程池和隊列。
如果要往es里面灌入數據的話,那么根據業務場景來,如果業務場景可以支持,,將一批數據聚合起來,一次性寫入es,那么就盡量采用bulk的方式,每次批量寫個幾百條這樣子。
bulk批量寫入的性能比一條一條寫入大量的document的性能要好很多。如果要知道一個bulk請求最佳的大小,需要對單個es node的單個shard做壓測。先bulk寫入100個document,然后200個,400個,以此類推,每次都將bulk size加倍一次。如果bulk寫入性能開始變平緩的時候,那么這個就是最佳的bulk大小。并不是bulk size越大越好,而是根據集群等環境具體要測試出來的,因為越大的bulk size會導致內存壓力過大,因此最好一個請求不要發送超過10mb的數據量。
之前測試這個bulk寫入,上來就是多線程并發寫bulk,先確定一個是bulk size,此時就盡量是用你的程序,單線程,一個es node,一個shard,測試。看看單線程最多一次性寫多少條數據,性能是比較好的。
Bulk、線程池、隊列大小:
1)客戶端:
單個 bulk 請求體的數據量不要太大,官方建議大約5-15mb
寫入端的 bulk 請求超時需要足夠長,建議60s 以上
寫入端盡量將數據輪詢打到不同節點
2)服務器端:
索引創建屬于計算密集型任務,應該使用固定大小的線程池來配置。來不及處理的放入隊列,線程數應該配置成CPU 核心數 +1,避免過多的上下文切換
隊列大小可以話當增加 不要過大 否則占用的內存會成為 GC 的負擔
-
使用多線程將數據寫入es
單線程發送bulk請求是無法最大化es集群寫入的吞吐量的。如果要利用集群的所有資源,就需要使用多線程并發將數據bulk寫入集群中。為了更好的利用集群的資源,這樣多線程并發寫入,可以減少每次底層磁盤fsync的次數和開銷。一樣,可以對單個es節點的單個shard做壓測,比如說,先是2個線程,然后是4個線程,然后是8個線程,16個,每次線程數量倍增。一旦發現es返回了TOO_MANY_REQUESTS的錯誤,JavaClient也就是EsRejectedExecutionException,之前有學員就是做多線程的bulk寫入的時候,就發生了。此時那么就說明es是說已經到了一個并發寫入的最大瓶頸了,此時我們就知道最多只能支撐這么高的并發寫入了。 -
降低IO操作,增加refresh間隔和時長,降低refresh頻率
默認的refresh間隔是1s,用index.refresh_interval參數可以設置,這樣會其強迫es每秒中都將內存中的數據寫入磁盤中,創建一個新的segment file。正是這個間隔,讓我們每次寫入數據后,1s以后才能看到。但是如果我們將這個間隔調大,比如30s,可以接受寫入的數據30s后才看到,那么我們就可以獲取更大的寫入吞吐量,因為30s內都是寫內存的,每隔30s才會創建一個segment file。
增加 refresh interval 的數值。默認為 1s,如果設置成-1,會禁止自動 refresh。避免過于頻繁的 refresh,而生成過多的 segment 文件。但是會降低搜索的實時性
增大靜態配置參數indices.memory.index buffer_size,默認是 10%,會導致自動觸發 refresh -
禁止refresh和replia
如果要一次性加載大批量的數據進es,可以先禁止refresh和replia復制,將index.refresh_interval設置為-1,將index.number_of_replicas設置為0即可。這可能會導致數據丟失,因為沒有refresh和replica機制了。但是不需要創建segment file,也不需要將數據replica復制到其他的replica shasrd上面去。一旦寫完之后,可以將refresh和replica修改回正常的狀態。 -
禁止swapping交換內存
可以將swapping禁止掉,有的時候,如果要將es jvm內存交換到磁盤,再交換回內存,大量磁盤IO,性能很差。 -
給filesystem cache更多的內存
filesystem cache被用來執行更多的IO操作,如果能給filesystem cache更多的內存資源,那么es的寫入性能會好很多。 -
使用es自動生成的文檔id
如果要手動給es document設置一個id,那么es需要每次都去確認一下那個id是否存在,這個過程是比較耗費時間的。如果我們使用自動生成的id,那么es就可以跳過這個步驟,寫入性能會更好。對于你的業務中的表id,可以作為es document的一個field。 -
用性能更好的硬件
我們可以給filesystem cache更多的內存,也可以使用SSD替代機械硬盤,避免使用NAS等網絡存儲,考慮使用RAID 0來條帶化存儲提升磁盤并行讀寫效率,等等。 -
index buffer
如果要進行非常重的高并發寫入操作,那么最好將index buffer調大一些,indices.memory.index_buffer_size,這個可以調節大一些,設置的這個index buffer大小,是所有的shard公用的,但是如果除以shard數量以后,算出來平均每個shard可以使用的內存大小,一般建議,但是對于每個shard來說,最多給512mb。es會將這個設置作為每個shard共享的index buffer,那些特別活躍的shard會更多的使用這個buffer。默認這個參數的值是10%,也就是jvm heap的10%,如果我們給jvm heap分配10gb內存,那么這個index buffer就有1gb,對于兩個shard共享來說,是足夠的了。
提升es集群寫入性能方法:
寫性能優化的目標: 增大寫吞吐量 (Events Per Second) ,越高越好
- 客戶端: 多線程,批量寫
可以通過性能測試,確定最佳文檔數量
多線程:需要觀察是否有 HTTP 429 返回,實現 Retry 以及線程數量的自動調節 - 服務器端: 單個性能問題,往往是多個因素造成的。需要先分解問題,在單個節點上進行調整并且結合測試,盡可能壓榨硬件資源,以達到最高吞吐量
使用更好的硬件。觀察 CPU /IO Block0
線程切換、堆棧情況
降低 CPU 和存儲開銷,減少不必要分詞 /避免不需要的 doc values /文檔的字段盡量保證相同的順序,可以提高文檔的壓縮率
盡可能做到寫入和分片的均衡負載,實現水平擴展,0Shard FilteringWrite Load Balancer
關閉無關的功能:
只需要聚合不需要搜索,lndex 設置成 false
不需要算分,Norms 設置成 false
不要對字符串使用默認的 dynamic mapping。因為索引字段數量過多,會對性能產生比較大的影響
Index_options 控制在創建倒排索引時,哪些內容會被添加到倒排索引中。優化這些設置,一定程度可以節約 CPU
關閉_source,減少IO 操作;(適合指標型數據)
針對性能的取舍:
如果需要追求極致的寫入速度,可以犧牲數據可靠性及搜索實時性以換取性能
犧牲可靠性: 將副本分片設置為 0,寫入完畢再調整回去
犧牲搜索實時性: 增加 Refresh interval 的時間
犧牲可靠性: 修改 Translog 的配置
ES 的默認設置,已經綜合考慮了數據可靠性,搜索的實時性質,寫入速度,一般不要盲目修改。一切優化,都要基于高質量的數據建模
性能調優之集群讀性能優化
盡量 Denormalize 數據:避免嵌套和父子關系
Elasticsearch != 關系型數據庫
盡可能Denormalize 數據,從而獲取最佳的性能
使用 Nested 類型的數據。查詢速度會慢幾倍
使用 Parent /Child 關系。查詢速度會慢幾百倍
數據建模:
盡量將數據先行計算,然后保存到 Elasticsearch 中。盡量避免查詢時的 Script 腳本計算
盡量使用 Filter Context,利用緩存機制,減少不必要的算分
結合 profile,explain API 分析慢查詢的問題,持續優化數據模型。嚴禁使用*開頭通配符 Terms 查詢
左側可以優化為右側,filter方式查詢,避免使用query context
聚合查詢中使用query方式控制聚合數據量
避免使用*星號開頭查詢
優化分片:
避免 Over Sharing。一個查詢需要訪問每一個分片,分片過多,會導致不必要的查詢開銷
結合應用場景,控制單個分片的尺寸
Search: 20GB
Logging:40GB
Force-merge Read-only 索引。使用基于時間序列的索引,將只讀的索引進行 force merge,減少 segment 數量
性能調優之搜索性能優化
-
給filesysgtem cache更多的內存
es的搜索引擎嚴重依賴于底層的filesystem cache,如果給filesystem cache更多的內存,盡量讓內存可以容納所有的indx segment file索引數據文件,那么搜索的時候就基本都是走內存的,性能會非常高。
如果最佳的情況下,生產環境實踐經驗,最好是用es就存少量的數據,就是要用來搜索的那些索引,內存留給filesystem cache的,就100G,那么你就控制在100gb以內,相當于數據幾乎全部走內存來搜索,性能非常之高,一般可以在1秒以內。所以盡量在es里,就存儲必須用來搜索的數據,比如現在有一份數據,有100個字段,其實用來搜索的只有10個字段,建議是將10個字段的數據,存入es,剩下90個字段的數據,可以放mysql,hadoop hbase,都可以。這樣的話,es數據量很少,10個字段的數據,都可以放內存,就用來搜索,搜索出來一些id,通過id去mysql,hbase里面去查詢明細的數據。 -
用更快的硬件資源
(1)給filesystem cache更多的內存資源
(2)用SSD固態硬盤
(3)使用本地存儲系統,不要用NFS等網絡存儲系統
(4)給更多的CPU資源 -
document模型設計
document模型設計是非常重要的,盡量在document模型設計的時候,寫入的時候就完成。另外對于一些太復雜的操作,比如join,nested,parent-child搜索都要盡量避免,性能都很差的。
在搜索/查詢的時候,要執行一些業務強相關的特別復雜的操作:
(1)在寫入數據的時候,就設計好模型,加幾個字段,把處理好的數據寫入加的字段里面
(2)自己用java程序封裝,es能做的用es來做,搜索出來的數據,在java程序里面去做,比如說我們基于es,用java封裝一些特別復雜的操作 -
預先index data
為了性能,提前優化data index時的數據模型,比如說document有一個price field,然后大多數查詢都對一個固定的范圍,對這個field使用range范圍查詢,那么可以提前將這個price的范圍處理出來,寫入一個字段中。 -
預熱filesystem cache
如果重啟了es,那么filesystem cache是空殼的,就需要不斷的查詢才能重新讓filesystem cache熱起來,可以先對一些數據進行查詢。比如一個查詢,要用戶點擊以后才執行,才能從磁盤加載到filesystem cache里,第一次執行要10s,以后每次就幾百毫秒。
完全可以程序執行那個查詢,預熱,數據就加載到filesystem cahce,程序執行的時候是10s,以后用戶真的來看的時候就才幾百毫秒。 -
避免使用script腳本
一般是避免使用es script的,實際生產中更是少用,性能不高,盡量不要使用。 -
使用固定范圍的日期查詢
盡量不要使用now這種內置函數來執行日期查詢,因為默認now是到毫秒級的,是無法緩存結果,盡量使用一個階段范圍,比如now/m,就是到分鐘級。那么如果一分鐘內,都執行這個查詢,是可以取用查詢緩存的。
性能調優之GC優化
ElasticSearch本質上是個Java程序,所以配置JVM垃圾回收器本身也是一個很有意義的工作。使用JVM的Xms和Xmx參數來提供指定內存大小,本質上提供的是JVM的堆空間大小,當JVM的堆空間不足的時候就會觸發致命的OutOfMemoryException。這意味著要么內存不足,要么出現了內存泄露。處理GC問題,首先要確定問題的源頭,一般有兩種方案:
- 開啟ElasticSearch上的GC日志
- 使用jstat命令
- 生成內存Dump
關于第一條,在ES的配置文件elasticsearch.yml中有相關的屬性可以配置,關于每個屬性的用途這里當然說不完。
第二條,jstat命令可以幫助查看JVM堆中各個區的使用情況和GC的耗時情況。
第三條,最后就是將JVM的堆空間轉儲到文件中去,實質上是對JVM堆空間的一個快照。
想了解更多關于JVM本身GC調優方法請參考:http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html
另外,通過修改ES節點的啟動參數,也可以調整GC的方式,但是實質上和上述方法是等同的。
性能調優之路由優化
ES中所謂的路由和IP網絡不同,是一個類似于Tag的東西。在創建文檔的時候,可以通過字段為文檔增加一個路由屬性的Tag。ES內在機制決定了擁有相同路由屬性的文檔,一定會被分配到同一個分片上,無論是主分片還是副本。那么,在查詢的過程中,一旦指定了感興趣的路由屬性,ES就可以直接到相應的分片所在的機器上進行搜索,而避免了復雜的分布式協同的一些工作,從而提升了ES的性能。于此同時,假設機器1上存有路由屬性A的文檔,機器2上存有路由屬性為B的文檔,那么在查詢的時候一旦指定目標路由屬性為A,即使機器2故障癱瘓,對機器1構不成很大影響,所以這么做對災況下的查詢也提出了解決方案。所謂的路由,本質上是一個分桶(Bucketing)操作。當然,查詢中也可以指定多個路由屬性,機制大同小異。
性能調優之分片優化
選擇合適的分片數和副本數。ES的分片分為兩種,主分片(Primary Shard)和副本(Replicas)。默認情況下,ES會為每個索引創建5個分片,即使是在單機環境下,這種冗余被稱作過度分配(OverAllocation),目前看來這么做完全沒有必要,僅在散布文檔到分片和處理查詢的過程中就增加了更多的復雜性,好在ES的優秀性能掩蓋了這一點。假設一個索引由一個分片構成,那么當索引的大小超過單個節點的容量的時候,ES不能將索引分割成多份,因此必須在創建索引的時候就指定好需要的分片數量。此時所能做的就是創建一個新的索引,并在初始設定之中指定這個索引擁有更多的分片。反之如果過度分配,就增大了Lucene在合并分片查詢結果時的復雜度,從而增大了耗時,所以我們得到了以下結論:
應該使用最少的分片!
主分片,副本和節點最大數之間數量存在以下關系:
節點數<=主分片數*(副本數+1)
控制分片分配行為。以上是在創建每個索引的時候需要考慮的優化方法,然而在索引已創建好的前提下,是否就是沒有辦法從分片的角度提高了性能了呢?當然不是,首先能做的是調整分片分配器的類型,具體是在elasticsearch.yml中設置cluster.routing.allocation.type屬性,共有兩種分片器even_shard,balanced(默認)。even_shard是盡量保證每個節點都具有相同數量的分片,balanced是基于可控制的權重進行分配,相對于前一個分配器,它更暴漏了一些參數而引入調整分配過程的能力。
每次ES的分片調整都是在ES上的數據分布發生了變化的時候進行的,最有代表性的就是有新的數據節點加入了集群的時候。當然調整分片的時機并不是由某個閾值觸發的,ES內置十一個裁決者來決定是否觸發分片調整,這里暫不贅述。另外,這些分配部署策略都是可以在運行時更新的。