1. 程式人生 > >百姓網 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,需要做的事情就很多了。而且很不幸,還有坑。

下面談談我們在升級過程所遇到的一些問題和解決之路。

一、Elasticsearch 2.x 變化

1、doc_values

這無疑是 2.x 中最大變化之一,雖然之前也有 doc_values,但是這次是預設開啟 doc_values,也說明官方是建議你使用 doc_values 的。

2、Filtered Query

這個已經 deprecated,在 2.x 中你還可以用。但是建議做如下修改

{

“query”: {

“filtered”: {

“query”: {

…….

},

“filter”: {

…….

}

}

}

}

修改為

{

“query”: {

“bool”: {

“must”: {

…….

},

“filter”: {

…….

}

}

}

}

把 query 和 filter 移到 bool 查詢的 must 和 filter 引數之中。

3、DeleteByQuery

現在作為一個外掛了,而且使用的是 Scroll/Scan & Bulk 來進行安全刪除,當然,速度可能慢一些。(./bin/plugin install delete-by-query 安裝外掛)

4、Facet 已經刪除,使用 aggerations 代替。

Aggerations histogram min_doc_count 預設值現在是 0。

5、network.host 預設是 localhost,如果不設定就只能本機訪問了。

一般設定為網路裝置名稱相關,如 eth0,則設定為 _eth0:ipv4,若是 em1,這設定為 _em1:ipv4。

6、Discovery:multicast (組播)因為系統受限的原因,現在從 Elasticsearch 移除。

不過它也可以作為一個外掛加入。1.x multicast 預設是啟用的,2.x 使用 unicast (單播),需要設定 discovery.zen.ping.unicast.hosts: [“host1:port”, “host2”],以使得叢集可以加入相關機器。

7、Store FS: 記憶體(memory/ram)儲存模式被移除。

預設使用 default_fs,是一種 Lucene MMapDirectory 和 NIOFSDirectory 混合的模式,詞典檔案和 doc values 檔案使用 mmap 對映到系統虛擬記憶體(需要設定 vm.max_map_count=262144),其他的檔案(如頻率、位置等)使用 nio 檔案系統。

8、Mapping 變化

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)同一配置。

9、快照的配置 path.repo 要設定白名單(注:是一個數組)。

10、Scroll:

1)search_type=scan deprecated,你可以在 scroll 查詢時使用 sort:”_doc” 來代替,_doc 排序已經進行了優化,因此它的效能和 scan 相同。

2)search_type=count deprecated, 可以設定 size:0 。

11、 Optimize:deprecated。使用 force merge 介面代替。

12、geo_point:percolating 的地理查詢移除了。

Percolator docs 是在記憶體中,不支援 doc_values,而 geo_point(ES 2.2)的一些查詢功能需要啟用 doc_values。

不過,geo_point 禁用了doc_values,有些一般查詢仍然有效。

13、indices.fielddata.cache.expire 配置移除(預設會忽略)。

二、升級之路

1、IO 壓力增大

2.0 剛出的時候,我們進行了測試,發現 IO 壓力有點大。啟用比不啟用 doc_values,IO 壓力要增加一倍以上(測試磁碟非 SSD)。

2.0 初始版本,Delete 會導致 IO 壓力更大,刪除操作會有 translog 等詭異問題。

解決辦法:建議升級到較高版本,如 2.2 及以上。

2、Index 速度變慢。

在 1.x 時候,我們沒有啟用 Bulk 介面,而是使用 Index 介面,升級後發現更新速度比較慢。我們改用 Bulk 介面以解決這個問題。

3、Bulk 介面的問題

如果使用 Bulk 介面來進行刪除,建議升級到較高版本,因為 2.0 初始版本 Bulk delete 可以不需要提供 routing,但是這樣效能也很差。較高版本修復了這個問題,刪除一個 DOC,需要提供 routing。

其實要獲得 routing 並不困難,2.x 在你查詢的時候,提供的結果中,就有 routing 這個資料,這個對於做刪除操作還是比較方便的,不需要進行計算,還能保證在 routing 頻繁變化後刪除乾淨。

4、Doc Values

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 以減少索引大小。

5、GC 各種掛、掛、掛。

在 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。最後,我們不得不另尋出路。

1)對 Cache (Static 配置,需要配置在 elasticsearch.yml 並重啟) 和 Circuit breaker 配置進行調整。如下:

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

前者和後者中相關的配置需要保持前者小於後者。

調整這些資料,未果。

2)Mapping 過大?

我們的 Mapping 確實比較大,因為業務處理邏輯複雜,各種名字的欄位沒有明確的限制,所以 Mapping 是比較大的。在 Mapping 很大的時候,當一個新的欄位進行索引,每個索引都要進行 mapping 更新,可能會導致 OOM。不過我們觀察到我們的 GC 問題和索引更新並沒有很明顯的聯絡,因為我們在進行索引初始化時,快速 Bulk 索引也只是 LA 比較大,並無 GC 問題,再一個在 1.x mapping 也沒有什麼問題。

3)Shards 太多?

shards 過多,也是可能導致 GC 問題的。因為每個 shard 的記憶體使用控制變得複雜。儘管我們某些叢集的 shards 數量較多( shards 90 * 2 = 180 個 shard),但嘗試調整或合併 Shards,均告無果。

4)Doc_values 和 fielddata cache 選擇

因為 GC 這種問題,所以我們嘗試減少 JVM 的記憶體使用,降低 GC 壓力。啟用 doc_values後,Heap 記憶體佔用變小,但不能解決這個問題。減小 Heap 大小,以減輕 GC 壓力,也無法解決這個問題。

5) Filtered Query 相容之坑

我們對 1.x 和 2.x 叢集加上了版本區分。在 2.x 的情況下,我們對查詢進行了強制修改。修改辦法就是上面提到的 Filtered Query 變更。即取消 filtered 而使用 bool 來進行代替。GC 問題得到緩解。

6)Aggerations histogram 

我們經過仔細對比 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。

1、routing : UID, first_category, city + second_category

為了提供快速查詢,根據業務特點對叢集進行不同搭配,如使用者訪問(帶有 Uid)將指向到 UID 叢集;查詢一個城市的二手手機將會指向到 city + second_category 叢集;指定了類目的查詢將指向 city + second_category 叢集的 first_category 索引(我們的特點是一級類目基本固定)。

2、fulltext && normal Cluster

我們的資訊特點是,資訊描述內容比較多,並且需要對描述內容做全文索引。這樣會導致叢集的索引大小非常大,需要佔用的磁碟和記憶體也就很多。從上文可知,我們根據業務特點劃分了不同的叢集,如果每個叢集都包含了資訊描述內容,索引都會很大,帶來成本的提高,也增加了維護難度。

我們業務另外一個特點是,全文索引查詢佔所有型別查詢比例較低,所以一個大的叢集可以提供全部全文索引查詢,那麼另外的叢集就可以不需要索引“資訊描述內容”,索引就大大的減小了。

3、time: week(N百萬級),2 month(N千萬級),full(N 億級)

我們還有采用時間來進行區分的叢集。基於某些業務對資訊新鮮度敏感,所以可以獲取一週或二月的資訊即可滿足需求。大大減少對 Full 型別叢集的訪問壓力,也能提供快速訪問。

4、full cluster(N 億級) && mini Cluster(N千萬級)

使用時間劃分集群后,還有一個好處,我們可以用二月的資訊的叢集來作為較小叢集,讓查詢優先訪問這個叢集,當資料滿足條件後,就不需要查詢 Full 叢集;資料不足繼續查詢 Full 叢集。大叢集的訪問壓力進一步降低。

這時,若查詢了較小叢集,並且需要準確的 Total Count (預設提供一個 Mini 叢集10倍的數字),可以進一步使用 Count API (設定 size:0)去訪問 Full 叢集。

5、full cluster && other small cluster(N千萬級/百萬級,業務分拆)

這裡和 4)不同的地方在於,上面使用的是時間劃分,而這裡是業務劃分。這個叢集只包含了特定資料的叢集(比如二手大類目的二級類目手機),主要看相關查詢量是否很大,若是這類查詢帶來壓力較大,就有必要分出去。

6、Cloud Query (Cached Query)

我們的一個特點,第一至三頁幾乎是所有訪問的 80% 以上,所以這部分查詢我們構造了一個 Cloud Query 池,用於提供快速訪問。這個池:

1)使用 DSL 查詢,查詢方法同 Elasticsearch。

2)初始化資料從 Elasticsearch 獲取。

3)保留了幾百個左右新鮮資料。

4)不斷更新。

5)資料不足,查詢指向 Elasticsearh。

6)使用 Redis zset 儲存新鮮資料 (Redis Cluster)。

為實現上面的功能,我們使用 golang 語言開發一個 Proxy 型別的服務(代號 4Sea)。

五、後話:Elasticsearch 5.0

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

Q&A

提問:對比下 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 進行了不少優化。

文章出處:高可用架構

高可用架構