《百姓網 Elasticsearch 2.x 升級之路》要點:
本文介紹了百姓網 Elasticsearch 2.x 升級之路,希望對您有用。如果有疑問,可以聯系我們。
導讀:Elasticsearch 是廣泛使用的一個軟件,我們邀請了曾經在高可用架構分享過 ES 的王衛華繼續分享在升級 Elasticsearch 過程中的經驗.
王衛華,資深開發工程師、架構師,具有 10+ 年互聯網從業經驗,曾獲得微軟 2002 – 2009 MVP 榮譽稱號.在百姓網近 9 年,負責后端代碼開發和 Elasticsearch & Solr 維護工作.現就職于途虎養車.
百姓網使用 Elasticsearch 雖然有用于日志(ELK),但本次分享所涉及 Elasticsearch 升級,是指用于業務系統數據服務的 Elasticsearch 集群.
百姓網是一個分類網站,要提供快速數據查詢,我們使用了 Lucene 作為基層的搜索系統,從幾年前的 Solr 到 現在使用的 Elasticsearch.為了提供快速的查詢響應,我們使用了一個 golang 寫的代理系統,代理后面是幾個 Elasticsearch 集群,以應對不同查詢.
因為集群眾多,一次性全部系統升級需要占用一倍的機器,這比較浪費,所以我們采用一個集群一個集群升級,這就需要不同版本的集群同時存在,從 1.0 升級到 1.6/1.7,他們基本查詢都相差不大,然而,從 1.x 到 2.x,需要做的事情就很多了.而且很不幸,還有坑.
下面談談我們在升級過程所遇到的一些問題和解決之路.
這無疑是 2.x 中最大變化之一,雖然之前也有 doc_values,但是這次是默認開啟 doc_values,也說明官方是建議你使用 doc_values 的.
這個已經 deprecated,在 2.x 中你還可以用.但是建議做如下修改
{
“query”: {
“filtered”: {
“query”: {
…….
},
“filter”: {
…….
}
}
}
}
修改為
{
“query”: {
“bool”: {
“must”: {
…….
},
“filter”: {
…….
}
}
}
}
把 query 和 filter 移到 bool 查詢的 must 和 filter 參數之中.
現在作為一個插件了,而且使用的是 Scroll/Scan & Bulk 來進行安全刪除,當然,速度可能慢一些.(./bin/plugin install delete-by-query 安裝插件)
Aggerations histogram min_doc_count 默認值現在是 0.
一般設置為網絡設備名稱相關,如 eth0,則設置為 _eth0:ipv4,若是 em1,這設置為 _em1:ipv4.
不過它也可以作為一個插件加入.1.x multicast 默認是啟用的,2.x 使用 unicast (單播),需要設置 discovery.zen.ping.unicast.hosts: [“host1:port”, “host2”],以使得集群可以加入相關機器.
默認使用 default_fs,是一種 Lucene MMapDirectory 和 NIOFSDirectory 混合的模式,詞典文件和 doc values 文件使用 mmap 映射到系統虛擬內存(需要設置 vm.max_map_count=262144),其他的文件(如頻率、位置等)使用 nio 文件系統.
1)同名字段:如果同一個索引中有不同類型的同名字段,那么這兩個類型的 mapping 必須一致.并且不能刪除 mapping (刪了mapping,另一個類型同名字段就沒有 mapping 了?).
2)各種 _ 前綴的名稱移除了.
3)dot (點)的各種坑,字段名不要包含 dot.
4)字段名最長 255
5)_routing 只能設置為required : true,沒有 path 參數.
6)analyzer 現在可以分開設置 index_analyzer 和 search_analyzer.默認設置 analyzer 即為兩者(index、search)同一配置.
1)search_type=scan deprecated,你可以在 scroll 查詢時使用 sort:”_doc” 來代替,_doc 排序已經進行了優化,因此它的性能和 scan 相同.
2)search_type=count deprecated, 可以設置 size:0 .
Percolator docs 是在內存中,不支持 doc_values,而 geo_point(ES 2.2)的一些查詢功能需要啟用 doc_values.
不過,geo_point 禁用了doc_values,有些一般查詢仍然有效.
2.0 剛出的時候,我們進行了測試,發現 IO 壓力有點大.啟用比不啟用 doc_values,IO 壓力要增加一倍以上(測試磁盤非 SSD).
2.0 初始版本,Delete 會導致 IO 壓力更大,刪除操作會有 translog 等詭異問題.
解決辦法:建議升級到較高版本,如 2.2 及以上.
在 1.x 時候,我們沒有啟用 Bulk 接口,而是使用 Index 接口,升級后發現更新速度比較慢.我們改用 Bulk 接口以解決這個問題.
如果使用 Bulk 接口來進行刪除,建議升級到較高版本,因為 2.0 初始版本 Bulk delete 可以不需要提供 routing,但是這樣性能也很差.較高版本修復了這個問題,刪除一個 DOC,需要提供 routing.
其實要獲得 routing 并不困難,2.x 在你查詢的時候,提供的結果中,就有 routing 這個數據,這個對于做刪除操作還是比較方便的,不需要進行計算,還能保證在 routing 頻繁變化后刪除干凈.
Lucene 索引是一種倒排索引,當需要進行排序或者計算時,需要在內存中使用 fielddata cache 進行計算,極端情況下,可能導致 OOM 或者內存泄露.這時候可以考慮啟用 doc_values,這個是索引時候已經進行處理的一種非倒排索引.啟用 doc_values,性能有一點損失,但是可以設置較小的 heap size,而留下內存給系統緩存 doc_values 索引,性能幾乎相當.
1)啟用 doc_values 后,Index size 增加近一倍.
2)啟用 doc_values ,當進行 aggs,sort 時,減少內存需求,減低 GC 壓力.可以設置較小的 heap size.
3)啟用 doc_values 后,當 Lucene 索引有效使用系統緩存時,性能幾乎相當.
4)2.x 你仍然可以 Disable doc_values,設置一個較大的 heap.只要沒有較大的 GC 問題,選擇 disable doc_values 是可以的,而且帶來的好處是索引較小.這是一個平衡選擇,大家可以根據平時使用情況進行調整.我們選擇了 disable doc_values 以減少索引大小.
在 1.x 升級到 2.x 的過程,基于集群只能滾動式升級,這決定 1.x 和 2.x 集群是同時共存.而在升級過程中,不幸躺著中槍,頻繁遇到 GC 問題,幾乎導致升級失敗.
首先我們嘗試了進行 GC 調優,CMS,G1,調整 heap size,heap NEW size ….,各種策略均告失敗.調整 thread pool 各項參數,對 query:size 過大數字也進行調整,以減少 GC 壓力,這些調整也均失效.
具體表現為,運行一段時間后,集群中某些 Node 的 CPU Usage 會突然上升,最后 JVM 保持在 100% CPU Usage,集群 Node 因為長期下線,被集群踢出,如果運氣好,Node 還會回來,大部分情況下它就保持在 100% CPU Usage 不死不活.
檢查日志,并無 OOM,而顯示 GC 問題很大,在幾次 CMS GC (new heap) 后,發生 Full GC,并且 Heap 使用率一直保持 90% 左右,GC 進入死循環.
一開始,判斷是 GC 問題,故而一直進行 GC 調優,未果.
當我們遇到 JVM GC 時,很可能并非 GC 策略本身問題,而可能是應用的 BUG.最后,我們不得不另尋出路.
Static 配置:
indices.queries.cache.size
indices.cache.filter.size
indices.queries.cache.size
indices.memory.index_buffer_size
Circuit breaker 配置:
indices.breaker.request.limit
indices.breaker.fielddata.limit
indices.breaker.total.limit
前者和后者中相關的配置需要保持前者小于后者.
調整這些數據,未果.
我們的 Mapping 確實比較大,因為業務處理邏輯復雜,各種名字的字段沒有明確的限制,所以 Mapping 是比較大的.在 Mapping 很大的時候,當一個新的字段進行索引,每個索引都要進行 mapping 更新,可能會導致 OOM.不過我們觀察到我們的 GC 問題和索引更新并沒有很明顯的聯系,因為我們在進行索引初始化時,快速 Bulk 索引也只是 LA 比較大,并無 GC 問題,再一個在 1.x mapping 也沒有什么問題.
shards 過多,也是可能導致 GC 問題的.因為每個 shard 的內存使用控制變得復雜.盡管我們某些集群的 shards 數量較多( shards 90 * 2 = 180 個 shard),但嘗試調整或合并 Shards,均告無果.
因為 GC 這種問題,所以我們嘗試減少 JVM 的內存使用,降低 GC 壓力.啟用 doc_values后,Heap 內存占用變小,但不能解決這個問題.減小 Heap 大小,以減輕 GC 壓力,也無法解決這個問題.
我們對 1.x 和 2.x 集群加上了版本區分.在 2.x 的情況下,我們對查詢進行了強制修改.修改辦法就是上面提到的 Filtered Query 變更.即取消 filtered 而使用 bool 來進行代替.GC 問題得到緩解.
我們經過仔細對比 1.x 和 2.x,對于 aggs histogram 的默認值變化(doc_min_count從1到0),一開始并沒有重視,后來顯式的設置這個參數為 1.GC問題得到解決.
上面的 5)6) 就是 GC 問題兩個很深的坑.
雖然他們算不上是 BUG,然而在 filtered query 只是 deprecated,而不是不能使用的情況下,這也太坑人了,遇到需要多集群滾動式升級的(比如我們),可能就會沿用 filtered query,以便能平滑升級,然后就會掉進深坑而不能自拔.
而 6)也算不上是 BUG,不過對于 doc_min_count = 0,大概率會觸發 GC,使用任何 GC 策略都不能正常使用.
1、Lucene version 在初期版本要顯式的在 mapping::settings 中配置.后來的版本沒有問題了.建議升級到較高版本以避免這種問題.
2、 aggerations 盡可能不要用在 analyzed fields,原因是 analyzed fields 是沒有 doc_values的,另外 analyzed fields 分詞之后,你進行 aggerations 也只能得到 term 的統計結果.
3、如果修改文檔是增量的,并且不會帶來數據覆蓋問題,建議使用 update API(或 bulk update API),可接受部分數據更新,而不需要一個完整文檔.
4、thread pool 調整.
如果一臺服務器內存較大或者因為多集群原因需要配置多個 Elasticsearch JVM node,建議調整默認的 threadpool.search.size (默認值:int((available_processors * 3) / 2) + 1),比如默認值為 24,此時這臺機器有 2 JVM node,可以根據各 node 大致的訪問量、訪問壓力在 24 / 2 = 12 上下調整.如果更配置更多的 JVM 以有效利用 CPU 和內存,需要進行這個調整.否則 JVM 可能奔潰而無法啟動.
5、count api (search api with size 0)
Count API 在某種情況下是很有效,比如當你只想獲得 Total Count 的時候,可以使用這個 API.
不過,2.1 以后已經使用 search API 并設置 size = 0 來代替了.新版本中 Elasticsearch Java 代碼中 Count API 已經去除,但是應用層面 _count 還是保留的.
6、timeout 參數 2.x 必須加上 s ,如 :? timeout = 3s
0、基本優化: 包括 硬件(CPU、Memory、SSD)、JVM 及其版本選擇(Heap size,GC,JDK8)、系統配置(File Descriptors、VM/Virtual memory、Swap、Swappiness、mlockall).
我們使用多核服務器和大內存,一定程度上可以彌補非 SSD 磁盤.
一臺服務器多個 JVM,版本為 JDK8;Heapsize 一般為 30G 以內,根據不同用途、索引大小和訪問壓力 ,Heapsize 有 5、10、20、30G 的不同配置,Heap NewSize 配置比較激進,通常大于 Heapsize 的一半;GC 選擇 CMS GC.
為了提供快速查詢,根據業務特點對集群進行不同搭配,如用戶訪問(帶有 Uid)將指向到 UID 集群;查詢一個城市的二手手機將會指向到 city + second_category 集群;指定了類目的查詢將指向 city + second_category 集群的 first_category 索引(我們的特點是一級類目基本固定).
我們的信息特點是,信息描述內容比較多,并且需要對描述內容做全文索引.這樣會導致集群的索引大小非常大,需要占用的磁盤和內存也就很多.從上文可知,我們根據業務特點劃分了不同的集群,如果每個集群都包含了信息描述內容,索引都會很大,帶來成本的提高,也增加了維護難度.
我們業務另外一個特點是,全文索引查詢占所有類型查詢比例較低,所以一個大的集群可以提供全部全文索引查詢,那么另外的集群就可以不需要索引“信息描述內容”,索引就大大的減小了.
我們還有采用時間來進行區分的集群.基于某些業務對信息新鮮度敏感,所以可以獲取一周或二月的信息即可滿足需求.大大減少對 Full 類型集群的訪問壓力,也能提供快速訪問.
使用時間劃分集群后,還有一個好處,我們可以用二月的信息的集群來作為較小集群,讓查詢優先訪問這個集群,當數據滿足條件后,就不需要查詢 Full 集群;數據不足繼續查詢 Full 集群.大集群的訪問壓力進一步降低.
這時,若查詢了較小集群,并且需要準確的 Total Count (默認提供一個 Mini 集群10倍的數字),可以進一步使用 Count API (設置 size:0)去訪問 Full 集群.
這里和 4)不同的地方在于,上面使用的是時間劃分,而這里是業務劃分.這個集群只包含了特定數據的集群(比如二手大類目的二級類目手機),主要看相關查詢量是否很大,若是這類查詢帶來壓力較大,就有必要分出去.
我們的一個特點,第一至三頁幾乎是所有訪問的 80% 以上,所以這部分查詢我們構造了一個 Cloud Query 池,用于提供快速訪問.這個池:
1)使用 DSL 查詢,查詢方法同 Elasticsearch.
2)初始化數據從 Elasticsearch 獲取.
3)保留了幾百個左右新鮮數據.
4)不斷更新.
5)數據不足,查詢指向 Elasticsearh.
6)使用 Redis zset 存儲新鮮數據 (Redis Cluster).
為實現上面的功能,我們使用 golang 語言開發一個 Proxy 類型的服務(代號 4Sea).
0、Lucene 6
“磁盤空間少一半;索引時間少一半;” ,Merge 時間和 JVM Heap 占用都會減少,索引本身的性能也提升.
“查詢性能提升25%;IPV6也支持了”.
1、Profile API,可以用來進行查詢性能監控和查詢優化.不用再對耗時查詢兩眼一抹黑.
2、翻頁利器: Search After.search 接口的一個新實現,使得你可以深度翻頁.這個彌補了 scroll 和 search 的不足.
3、Shrink API: 合并 Shards 數,現在不用擔心 shard 數字設置不合理,你可以使用這個 API 去合并以減少索引 shards 數量.
4、Reindex,應該是比較令人心動的 API,可惜需要啟用 _source.
5、更新數據的 wait_for refresh 特性,可能在某種用戶非異步更新時會有好處,讓用戶(更新接口)等待到更新完成,避免用戶得不到數據或者得到老數據.
6、delete_by_query 重回 core !!!但是實現方式優化了.
7、2.x 中 Deprecated 的功能在這個版本大多移除.
還有更多….
升級 2.x 成功,5.x 還會遠嗎?看到上面的好處,我想大家都有強烈的升級沖動.
升級工具:
0.90.x /1.x => 2.x
https://github.com/elastic/elasticsearch-migration/tree/1.x
2.x => 5.0
https://github.com/elastic/elasticsearch-migration/tree/2.x
提問:對比下 Elasticsearch 和 Solr?為何貴司選了 ES?
王衛華:當初使用 Solr 的時候,Elasticsearch 還沒出現.Elasticsearch 作為一個新出現的開源搜索引擎,有許多新特性,我們從 0.x 就開始使用,當初最看好的是它的管理方便,插件多,接口設計好等比較人性化的特性.
提問:線上集群如何進行不停機 reindex 的,這個過程在有數據不斷索引的情況下如何保證原有集群數據同新集群數據一致性?
王衛華:Reindex 是一個高耗操作,所以一般情況下,最好不要提供服務,但是如果索引比較小,這個操作帶來的壓力一般.索引大,大量的碎片會帶來很大的性能問題.所以我們一般對集群每天進行 optimize(force merge).這樣在高峰期可以提供較好的性能.
我們現在因為通過 4Sea 的配置,可以讓任何比較清閑的集群承擔當前 reindex 索引的查詢.
提問:GC 選擇 CMS,為何不選擇 G1 呢?
王衛華:G1 的性能也很不錯.官方目前支持 CMS,認為 G1 在 JDK8 還不算成熟.我們在試驗中得出的結論 G1 對比稍差一點,并不落后多少.
不過,如果你設置 heap size 大于 30G,我建議你使用 G1.小于 30G,CMS 比較好.
提問:百姓網的 es 集群是從一開始就切成多個了嗎?大體分為幾個,為什么如此切分?代理服務器上路由實現是如何進行的?
王衛華:我們一開始也只有一個集群,但是我們對性能有追求,幾百毫秒一個查詢是不可接受的.隨著數據越來越多,有些查詢頂不住,需要分而治之.才能提供快速訪問(毫秒級別).
切分的原則,我們上面講了,大致是:routing,時間,業務,是否提供全文索引.
代理服務器對查詢進行分析,然后導引到合適的集群,比如 week,month,業務,并提供不同的routing.
提問:請問,32G 的物理內存,慢慢越來越少,是否正常?怎樣做這方面優化?
王衛華:32G 的內存,JVM 會使用一部分內存.Lucene(系統緩存)會使用一部分.
越來越少是因為 Lucene 索引使用了內存,還有一些可能是其他文件緩存.
一般處理原則是 JVM + 索引大小 < 物理內存 即可.
提問:為何選 Golang 做 Proxy,不用 Java?
王衛華:主要看中 Golang 的 goroutine 和編碼的簡單舒適感,第三方工具包也足夠多,使用過程中也沒有 GC 性能問題(至少我們使用中沒有這個問題).
題外:我們從 Go 1.4 直接跳到 Go 1.6,解決一些坑(比如升級后鎖變化問題),性能有很大的提升.
提問:怎么應對網絡不穩定對集群的影響,特別是集群意外斷電,導致 shard 的自動遷移,恢復時間長,從而導致集群不穩定,在 2.x 版本對 shard 的均衡分布和自動遷移有沒有相關的更新?
王衛華:網絡不穩定的情況下,解決辦法就是提高 discovery.zen.ping.timeout 的時間,然而這樣提供快速查詢就比較傷.所以一個集群中保持一個穩定的網絡環境還是很重要的.
要加快恢復時間而網絡帶寬允許的情況下,可以調整 cluster.routing.allocation 和 recovery 各項參數,增加并發,提高同時恢復的 node 數,提高傳輸速率.
2.x 對 allocation,recovery 進行了不少優化.
文章出處:高可用架構
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/4453.html