#研發解決方案介紹#基於ES的搜索+篩選+排序解決方案


鄭昀 基於胡耀華和王超的設計文檔 最后更新於2014/12/3
關鍵詞: ElasticSearch 、Lucene、solr、搜索、facet、高可用、可伸縮、mongodb、SearchHub、商品中心

本文檔適用人員:研發和運維
提綱:
  1. 曾經的基於MongoDB的篩選+排序解決方案
  2. MongoDB方案的缺陷
  3. 看中了搜索引擎的facet特性
  4. 看中了ES的簡潔
  5. 看中了ES的天生分布式設計
  6. 窩窩的ES方案
  7. ES的幾次事故和教訓
  8. ES自身存在的問題

  首先要感謝王超和胡耀華兩位研發經理以嚴謹治學的研究精神和孜孜以求的工作態度給我們提供了高可用、可伸縮、高性能的ES方案。
一,曾經的基於 MongoDB 的篩選+排序解決方案
  電商的商品展示無非“List(列表頁)-Detail(詳情頁)”模式。生活服務電商更特殊一點,不同開站城市下的用戶看到的團購/旅游/酒店/抽獎/電影訂座/外賣…等商品集合以及排序也不一樣。
  起初窩窩的 List 需求比較簡單,所以用 memcached+mysql 也就解決了,但隨着在 List 頁做多級篩選,根據排序公式計算商品得分來做自動排序等需求的提出,我們把視線轉向了 MongoDB。
  2012年,我們針對窩窩當時的 MongoDB 實現方案進一步提出,商品中心的改造思路為“ 持久化緩存模式,盡量減少接口調用”:
  • 商品中心小組對外提供的實際上是一個存儲介質
  • 把本需做復雜關聯查詢的商品數據(base屬性集合、ext屬性集合、BLOB集合)組裝成一個 Document 放入 MongoDB 等持久化存儲介質中
  • 允許不同商品具有不同屬性的可擴展性
  • 商品中心要做的是維護好這個存儲介質,保證:
    • 商品數據的准確性:
      • 如商品自然下線,從介質中清除;
      • 如商品緊急下線,默認保留一段時間如6小時;
      • 如商品base/ext/blob屬性發生變更,有不同的時間策略來更新,如base屬性改變,則需要第一時間更新;
    • 商品可按常見規則快速抽取:
      • 如view層按頻道+城市抽取商品,
      • 如view層按城市+區縣+前台分類抽取商品,
  • view層可由各個系統自行開發
  這樣,MongoDB 里不僅僅存儲了一份份 documents,還存儲了不同開站城市、不同頻道、不同排列組合下商品列表的 Goods IDs 清單。排序基本靠 MongoDB 排。ids 清單定時更新。
  這之后,商品中心分拆為:泰山和 GoodsCenter 兩部分。
 
二,MongoDB 方案的缺陷
  隨着網站業務的不斷發展,網站商品搜索篩選的粒度越來越細,維度也就越來越多,多維度的 count 和 select 查詢,業務上各種排序需求,使 MongoDB 集群壓力山大,以至於屢屢拖累商品中心和泰山的性能。
  2012年下半年,我們意識到:
  由於頻道頁流量小於首頁,尤其是用戶很少點擊到的深度篩選條件組合查詢,所以下圖中的所有枚舉項商品數量都容易緩存失效或緩存擠出:
圖2 篩選越來越復雜,標題數字卻要保持准確性
  一旦緩存失效后,但凡我從上圖的“20元以下”點擊切換到“51-80元”或做更深層次篩選,那么程序就要針對上面所有組合條件對 MongoDB 商品記錄逐一做 count 計算。
  雖然每一個 count 計算都很快不屬於慢查詢,但也架不住多啊,尤其是配上區縣和商圈等動輒6、7層深的篩選組合,點擊一次輕易就涉及成百次的 count 計算,代價還是很大的。
  由於在商城模式下,不同頻道很可能不斷增加新篩選條件,導致篩選組合越來越復雜,最終可能要求我們從基於 NoSQL 的排序和篩選方案,盡快轉變為基於搜索引擎的排序和篩選方案
  2012年時,不同篩選維度的組合篩選造成  MongoDB 的索引命中率不高,MongoDB一旦沒有命中索引,其查詢效率會直線下降,從而造成整個MongoDB的壓力增大響應變慢(MongoDB 的索引策略基本和 MySQL 的差不多)。有段時間,我們不止一次遇到由於 MongoDB 的慢查,拖掛所有前台工程的情況,焦頭爛額。
  商品中心需要升級。 技術選型主要集中在 solr 和 ES 這兩個均構建於 Lucene 之上的搜索引擎
  這時,我們也注意到了外界對新生事物 Elastic Search 的各種溢美之辭,系統運維部此前也用 Logstash+ElasticSearch+Kibana 方案替代了 Splunk,也算是對 ES 的搭建有了一定了解。
  那時還看到了專門做 solr vs es 的網站: http://solr-vs-elasticsearch.com/
  
三,看中了搜索引擎的 facet 特性
  借用騰訊一篇博文來講解 facet search:
  介紹分面
  分面是指事物的多維度屬性。例如一本書包含主題、作者、年代等分面。而分面搜索是指通過事物的這些屬性不斷篩選、過濾搜索結果的方法。可以將分面搜索看成搜索和瀏覽的結合。

  靈活使用分面
  分面不但可以用來篩選結果,也可以用來對結果排序。電商網站中常用風格、品牌等分面篩選搜索結果,而價格、信譽、上架時間等分面則用來排序。

  有時用戶並不明確自己的目的,因此提供寬松的篩選方式更符合這部分用戶的預期。Bing 的旅行搜索中選擇航班時,用戶可以通過滑塊來選擇某個時間段起飛的航班。

  facet 的字段必須被索引,無需分詞,無需存儲。無需分詞是因為該字段的值代表了一個整體概念,無需存儲是因為一般而言用戶所關心的並不是該字段的具體值,而是作為對查詢結果進行分組的一種手段,用戶一般會沿着這個分組進一步深入搜索。
   facet 特性對我們最大優點是,查詢結果里自帶 count 信息,無需我們單獨計算不同排列組合的 count 信息,一舉掃清性能瓶頸。
  solr 里 facet search 分為三種類型:
  1. Field Facet:如果需要對多個字段進行Facet查詢,那么將 facet.field 參數聲明多次,Facet字段必須被索引;
  2. Date Facet:時間字段的取值有無限性,用戶往往關心的不是某個時間點而是某個時間段內的查詢統計結果,譬如按月份查;
  3. Facet Query:利用類似於filter query的語法提供了更為靈活的Facet,譬如根據價格字段查詢時,可設定不同價格區間;
 
四,看中了 ES 的簡潔
  2012年下半年不少人傾倒於 ES 的簡潔之美:
圖3
圖4
  ES 的優點:
  • 簡單 
  • RESTful 
  • json 格式 Response 
  • 天生分布式 
  • Querydsl 風格查詢
 
五,看中了 ES 的天生分布式
  ES 畢竟是后來者,所以可以說為分布式而生。它的處理能力上,支持橫向擴展,理論上無限制;存儲能力上,取決於磁盤空間(根據提取字段的數量,索引后的數據量是原始數據量的幾倍,譬如我們的 Logstash+ES 方案中對 nginx 訪問日志提取了17個字段(都建立了索引),存儲數據量8倍於原始日志)。
  比如在高峰期,我們可以采用調配臨時節點的方式,來分解壓力,在不需要的時候我們可以停掉多余的節點來節省資源。
  還有ES的高可用性,在集群節點出現一個節點或者多個節點出現故障時,主要數據完整,依然可以正常提供服務。
  這里有一個數據,大概是2013年時,有一個 訪談提及 Github 是如何使用 ES:1,用了 40~50 個索引,包括庫、用戶、問題、pull請求、代碼、用戶安全日志、系統異常日志等等;2,44台 EC2 主機處理 30T 的數據,每台機器配備 2T SSD 存儲;3,8台機器僅僅用於搜索,不保存數據。當然,Github 也曾經在 ES 升級上栽過大跟頭,那是2013年1月17日的事兒了,參考《2013,GitHub使用elasticsearch遇到的一些問題及解決方法, 中文譯稿英文原文》。
 
六,窩窩的 ES 方案
 
6.1>架出一層 SearchHub
  所有數據查詢均通過 SearchHub 工程完成,如下圖所示:
圖5 SearchHub
 
6.2>通過 NotifyServer 來異步更新各個系統
  窩窩的數據更新基本都是通過 notify (基於中間件 NotifyServer)的形式來保證各個系統的數據一致性。
 
6.3>索引設計方案
   6.3.1>商品索引設計

  商品維度是我們主要的查詢維度,其業務復雜度也比較高。針對網站查詢特性,我們的商品主索引方案為:每個城市建立一個 index,所以一共有400多個 index,每個城市僅有1個主 shard(不分片)。這樣做的好處是以后我們根據熱點城市和非熱點城市,可以將各個 index 手工分配到不同的 node 上,可以做很多優化。

  其結構為:

圖7 goodsinfo

  為了減少索引量和功能拆分,減少商品索引的內存占用,所以我們把全文檢索單獨建為一個索引。

  每個城市索引或者商品索引按頻道分為幾個type,如下圖所示。

 

圖9 type

   商品頻道映射到es的type是很容易理解的,因為每個頻道的模型不同:有的頻道特有“用餐人數”屬性,有的頻道特有“出發城市”和“目的地城市”屬性 所以每個頻道對應一個es的type,每個type綁定一種特定的mapping(這個mapping里面可以指定該頻道各自的特殊屬性如何儲存到ES)。
 
   6.3.2>門店索引設計
  門店索引方案,采用了默認的形式,就是一個索引叫做 shop_index, 5 shard 的形式。
 
6.4>集群節點設計方案
  按照業務拆分,我們將ES拆分為兩大集群:商品索引集群(商品分城市索引和全文檢索索引)和非商品索引集群(或叫通用集群,目前主要是門店索引和關鍵詞提示索引)。這樣分的主要原因是,商品索引數據量較大,而且它是主站主要業務邏輯,所以將其單獨設立集群。
  網絡拓撲如下圖所示:
 
圖10 集群網絡拓撲
6.5>分詞設計
  中文檢索最主要的問題是分詞,但是分詞有一個很大的弊端:當我增加一個新的詞庫后,需要重新索引現有數據,導致我們重建索引代價較大。所以在犧牲一些查詢效率的情況下,窩窩采取了在建立索引時做單字索引,在查詢時控制分詞索引的方案。
  具體方案如下所示:
 
圖11 分詞設計
 
6.6>高可用和可伸縮方案
  看一下窩窩商品索引,窩窩采用的方案是一個城市一個索引,所有索引的“副本(replicas)”都設為 1,這樣比如 shop_index,它有 5 個 shard,每個 shard (只)有一個副本。(注:1個副本一方面可以省空間,另一方面是為了效率,在 ES 0.90版本下,ES 的副本更新是全量備份的方案,多個副本就會有更新效率的問題。ES 1.0 后有改進,王超認為在增加服務器后,可以考慮多增加副本。)

  ES 會保證所有 shard 的主副本不在同一個 node 上面,但我們是 ES 服務器集群,每台服務器上有多個 node,一個 shard 的主副本不在同一個節點還是不夠的,我們還需要一個 shard 的主副本不在同一台服務器,甚至在多台物理機的情況下保證要保證不在同一個機架上,才可以保證系統的高可用性。

     所以ES提供了一個配置:cluster.routing.allocation.awareness.attributes: rack_id

     這個屬性保證了主副 shard 會分配到名稱不同的 rack_id 上面。

 

  當我們停止一個節點時,如停止 174_node_2,則 ES 會自動重新平衡數據,如下圖所示:

 

圖13 重新分布

  即使一台物理機完全 down 掉,我們可以看到其他物理機上的數據是完整的,ES 依然可以保證服務正常。

 
七,ES 的幾次事故和教訓
 
7.1>誤刪數據
  ES 的 Web 控制台權限很大,可以刪數據。
  有一天,一個開發者需要查詢索引 mapping,他用 firefox 的插件訪問,結果 Method 默認居然為 DELETE,如下圖所示:
圖14 delete
  沒有注意,於是悲劇發生了。
教訓:
1)后來耀華咨詢了長期運維ES的一些人,大部分都建議前置一個 ngnix,通過 ngnix 禁用 delete 和 put 的 HTTP 請求,借此來限制開放的ES接口服務。
2)這次的誤操作,實際上 是在沒有給定索引的情況下,誤執行了DELETE 操作,結果刪除了全部索引。其實配一下 ES 是可以避免的,加入這個配置:
     action.disable_delete_all_indices=true
   這樣會禁止刪除所有索引的命令,刪除索引的話,必須要給定一個索引 ,稍微安全一些。
 
7.2>mvel 腳本引發的ES事故
ES集群表象:
  一天,ES 各個節點負載升高,JVM Full GC 頻繁。

  查看其內存使用狀況發現,ES 各個節點的 JVM perm 區均處於滿或者將要滿的狀態,如下圖所示:

 

圖15 當時perm的容量

 注1:jstat -gc <pid>命令返回結果集中,上圖紅色方框中字段的含義為:

    PC Current permanent space capacity (KB). 當前perm的容量 ;

    PU Permanent space utilization (KB). perm的使用。

大家可以看到圖1中的PU值基本等於PC值了。

 
問題原因:
  頭一天上線了商品搜索的一個動態排序功能,它采用 ES 的 mvel 腳本 來動態計算商品的排序分值。而 mvel 的原理是基於 JIT,動態字節碼生成的,於是很有可能造成 perm 區持續升高,原因是它不斷地加載和生成動態 class。

  由於 ES 各個節點的 perm 區接近飽和狀態,所以造成了服務器負載升高,GC 頻繁,並進一步造成 ES 集群出現了類似於“腦裂”的狀態。

 
經驗教訓:
  • 引入新技術,還是要謹慎,畢竟如果真是 mevl 腳本引起的問題,其實線下做壓力測試就能提前發現。
  • 加強ES的監控。
  • 雖然現在回過頭來看,如果在第一時間重啟所有 nodes,損失應該是最小的——但是王超認為當時采用的保守策略依然是有意義的,因為在弄清楚問題原因之前,直接重啟 nodes 有可能反而造成更大的數據破壞。
 
7.3>mark shard as failed 的 ES事故
問題現象:
  一天,打算對 JVM 參數和 ES 配置做了小幅度的謹慎調整。
  凌晨 00:10 左右,維護者開始按照計划對 ES 集群的各個 node 依次進行重啟操作,以便使新配置的參數生效(這類操作之前進行過很多次,比較熟練)。
1, 使用 http 正常的關閉接口,通知 174_0 節點進行關閉,成功。
2, 觀察其余 node 的狀態,幾十秒后,ES 剩余的9個節點恢復了 green 狀態。
3, 啟動 174_0 節點。
         ——至此僅僅完成了 174_0 節點的重啟工作,但緊接着就發現了問題:174_0 節點無法加入集群!
 
  此時的狀態是:174_0 報告自己找不到 master,剩余9個節點的集群依然運行良好。
  於是用 jstat 查看了 174_0 的內存占用情況,發現其 ParNew 區在正常增長,所以他認為這次重啟只是比往常稍慢而已,決定等待。
  但是在 00:16 左右,主節點 174_4 被踢出集群,174_3 被選舉為主節點。
  之后,日志出現了 shard 損壞的情況:
  [WARN ][cluster.action.shard ] [174_node_2] sending failed shard for [goods_city_188][0], node[eVxRF1mzRc-ekLi_4GvoeA], [R], s[STARTED], reason [master [174_node_4][6s7o-Yr-QXayxeXRROnFPg][inet[/174:9354]]{rack_id=rack_e_14} marked shard as started, but shard has not been created, mark shard as failed]
  更糟的是,不但損壞的 shard 無法自動回復,而且損壞的 shard 數量越來越多,最終在將近 01:00 的時候,集群由 yellow 狀態轉為 red 狀態,數據不再完整(此時損壞的主 shard 不到 20%,大部分城市還是可以訪問的)。
 
臨時解決辦法:
  首先對主站做業務降級,關閉了來自前端工程的流量。
  維護者開始采用第一套方案:依次關閉所有 node,然后再依次啟動所有 node。此時上面新增的 gateway 系列參數開始起作用,集群在達到 6 個 node 以上才開始自動恢復,並且在幾分鍾后自動恢復了所有的 shard,集群狀態恢復 green。
  隨后打開了前端流量,主站恢復正常。
  接着補刷了過去2小時的數據到 ES 中。
  至此,故障完全恢復。
 
經驗教訓:
1, 此次事故發生時,出問題的 nodes 都是老配置;而事故修復之后的所有 nodes 都是采用的新配置,所以 可以確定此次問題並不是新配置導致的
2, ES 自身的集群狀態管理(至少在 0.90.2 這個版本上)是有問題的——先是從正常狀態逐漸變為“越來越多的 shard 損壞”,重啟之后數據又完全恢復,所有 shard 都沒有損壞。
3, 由於是深夜且很短時間就恢復了服務,所幸影響范圍較小。
4, 故障原因不明,所以隨后安排 ES 從 0.90 版本升級到 1.3 版本。
 
八,ES 存在的問題以及改進

  Elastic Search 在窩窩運行幾年來基本穩定,可靠性和可用性也比較高,但是也暴露了一些問題:

  1. ES 的更新效率,作為基於 lucene 的分布式中間件,受限於底層數據結構,所以其更新索引的效率較低,lucene 一直在優化;
  2. ES 的可靠性的前提是保證其集群的整體穩定性,但我們遇到的情況,往往是當某個節點性能不佳的情況下,可能會拖累與其同服務器上的所有節點,從而造成整個集群的不穩定。
    • 其實解決這個問題不難,兩種方法:
      1. 增加服務器,讓節點盡可能地散開;
      2. 當某個節點出現問題的時候,需要我們及早發現處理,不至於拖累整個集群。其實監控一個節點是否正常的方法不難,ES 是基於 JVM 的服務,它出現問題,往往和 GC、和內存有關,所以只要監控其內存達到某個上限就報警即可;
  3. 沒有一個好的客戶端可視化集群管理工具,官方或者主流的可視化管理工具,基本都是基於 ES 插件的,不符合我們的要求,所以需要一款可用的客戶端可視化集群管理工具;
  4. ES 的升級問題,由於 ES 是一個快速發展的中間件系統,每一次新版本的更新,更改較大,甚至導致我們無法兼容老版本,所以 ES 升級問題是個不小的問題,再加上我們數據量較大,遷移也比較困難。

 

——END——

窩窩的解決方案介紹列表:

#研發解決方案#基於StatsD+Graphite的智能監控解決方案

#研發中間件介紹#定時任務調度與管理JobCenter

#研發解決方案介紹#Recsys-Evaluate(推薦評測) 

#研發解決方案介紹#Tracing(鷹眼)

#研發解決方案介紹#基於持久化配置中心的業務降級

#研發中間件介紹#異步消息可靠推送Notify

#研發解決方案介紹#IdCenter(內部統一認證系統)

#研發解決方案介紹#基於ES的搜索+篩選+排序解決方案

#數據技術選型#即席查詢Shib+Presto,集群任務調度HUE+Oozie

 
附錄A:

es術語介紹:

cluster:

代表一個集群,集群中有多個節點,其中有一個為主節點。這個主節點是可以通過選舉產生的。注意,主從節點是對於集群內部來說的。es的一個概念就是去中心化,字面上理解就是無中心節點,這是對於集群外部來說的,因為從外部來看es集群,在邏輯上是個整體,你與任何一個節點的通信和與整個es集群通信是等價的。

shards

代表索引分片。es可以把一個完整的索引分成多個分片,這樣的好處是可以把一個大的索引拆分成多個,分布到不同的節點上。構成分布式搜索。分片的數量只能在索引創建前指定,並且索引創建后不能更改。

replicas

代表索引副本,es可以設置多個索引的副本。副本的作用,一是提高系統的容錯性,當某個節點的某個分片損壞或丟失時可以從副本中恢復,二是提高es的查詢效率,es會自動對搜索請求進行負載均衡。

recovery

代表數據恢復或叫數據重新分布,es在有節點加入或退出時會根據機器的負載對索引分片進行重新分配,掛掉的節點重新啟動時也會進行數據恢復。

river

代表es的一個數據源,也是其他存儲方式(如:數據庫)同步數據到es的一個方法。它是以插件方式存在的一個es服務,通過讀取river中的數據並把它索引到es中,官方的river有couchDB的,RabbitMQ的,Twitter的,Wikipedia的。

gateway

代表es索引快照的存儲方式。es默認是先把索引存放到內存中,當內存滿了時再持久化到本地硬盤。gateway對索引快照進行存儲,當這個es集群關閉再重新啟動時,就會從gateway中讀取索引備份數據。es支持多種類型的gateway,有本地文件系統(默認),分布式文件系統,Hadoop的HDFS和amazon的s3雲存儲服務。

discovery.zen

代表es的自動發現節點機制。es是一個基於p2p的系統,它先通過廣播尋找存在的節點,再通過多播協議來進行節點之間的通信,同時也支持點對點的交互。

Transport

代表es內部節點或集群與客戶端的交互方式。默認內部是使用tcp協議進行交互,同時它支持http協議(json格式)、thrift、servlet、memcached、zeroMQ等的傳輸協議(通過插件方式集成)。

 

歡迎訂閱我的微信訂閱號『老兵筆記』,請掃描二維碼關注:
老兵筆記訂閱號二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM