#研發解決方案介紹#基於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 計算都很快不屬於慢查詢,但也架不住多啊,尤爲是配上區縣和商圈等動輒六、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:
  介紹分面
  分面是指事物的多維度屬性。例如一本書包含主題、做者、年代等分面。而分面搜索是指經過事物的這些屬性不斷篩選、過濾搜索結果的方法。能夠將分面搜索當作搜索和瀏覽的結合。

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

  有時用戶並不明確本身的目的,所以提供寬鬆的篩選方式更符合這部分用戶的預期。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 上,能夠作不少優化。 node

  其結構爲: mysql

圖7 goodsinfo nginx

  爲了減小索引量和功能拆分,減小商品索引的內存佔用,因此咱們把全文檢索單獨建爲一個索引。 git

  每一個城市索引或者商品索引按頻道分爲幾個type,以下圖所示。 github

 

圖9 type sql

   商品頻道映射到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 的主副本不在同一臺服務器,甚至在多臺物理機的狀況下保證要保證不在同一個機架上,才能夠保證系統的高可用性。 mongodb

     因此ES提供了一個配置:cluster.routing.allocation.awareness.attributes: rack_id json

     這個屬性保證了主副 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——

相關文章
相關標籤/搜索