索引構建

背景
面向C端用戶的在線核心搜索系統底層採用ES作爲核心“存儲/檢索”引擎,搜索作爲用戶購買決策的核心鏈路的一部分,對系統的可靠性要求tp9999,對查詢性能要求極高(召回+排序 300ms以內)。如何搭建一套這樣系統穩定、性能可靠的搜索系統呢?

索引構建

`

`

離線索引構建

離線索引構建流程圖如下:

  1. 各類業務數據存儲在自己的 MySQL 表,業務表會同步到 Hive 數倉(Hive數據倉庫分層),可經過一些清洗處理,生成業務數據 Hive 底表
  2. 每類業務數據有自己的 Hive 底表,通過 Hive join 各類業務表中需要支持檢索的字段,生成ES的Hive數據底表
  3. Hive底表同步到 HBase 中,採用的是 HBase 的 bulkload 功能,直接生成 HFile 文件。此外,由於 HBase是列式數據庫,因此能夠非常方便地解決:新增字段需要支持檢索的問題。
  4. Spark讀取 HBase 中的數據,構建ES索引文件(lucene索引文件)。在開始構建索引時,記錄索引構建時間點。在spark節點服務上,創建一個ES服務,執行索引構建邏輯,創建2個索引,一個是離線索引、一個增量索引。離線索引是 T+1 的離線全量數據,增量索引則是:在索引構建開始時,到索引構建完成這段時間內的增量數據。索引構建完成後,變更的數據也會更新到增量索引中。
  5. 將生成的索引文件壓縮上傳到S3對象存儲
  6. 索引加載服務從S3上下載索引文件,並基於自定義的ES插件,讀取索引文件,加載到ES集羣
  7. 做一些段合併、預熱等操作preload-data-to-file-system-cache,完成索引的加載,並進行副本分配。當本集羣的ES節點的分片全部就緒後,爲當前集羣開流量,並摘除另一個"對等"備份集羣的流量,開始構建另一個集羣的索引

一些索引字段:

商品的spu名稱、商品spu_id---商品團隊的商品表
商家店鋪名稱、商家id---商家團隊的商家表
商品spu的庫存、近30天的銷量---交易團隊的銷售表
商品spu_id的商家錄入類目(一、二、三、四級類目)以及算法預測類目---算法團隊產出的表
商品spu的活動標籤、券標籤等各種營銷活動標籤---營銷團隊的業務表
商品spu可配送區域id---基礎信息表

好處:

  1. 新增的字段接入搜索非常方便
  2. 基於spark分佈式計算來構建索引,可以構建很大量級的索引。在底層spark節點上啓動一個ES進程(ES安裝包已經部署到節點上),然後通過ES進程來構建分片。
  3. 採用離線索引+增量索引,可緩解更新索引doc對查詢性能的影響。如果ES集羣中只有一個索引,索引中各個字段的數據頻繁更新勢必影響查詢性能,頻繁更新帶來的段文件數量不可控,會導致不可避免的查詢抖動。此外,索引中會有很多字段,有些更新頻繁、有些更新不頻繁,有些字段的數據重要、有些字段的不重要。只有一個索引很難滿足所有的這些場景。比如:“近30天銷量”是一天計算並更新一次、“商品spu可配送區域id”需要實時知道、“活動標籤/券標籤”可一天全量更新一次當前參與活動的spu_id,也需要接入實時的活動信息。因此,採用離線索引+增量實時索引方式能較好地兼顧各種需求下的字段變化。
  4. 相比於全部業務數據都放到一個索引的設計方式, 此方式也有更好地利用ES的page cache緩存
  5. 基於 spark 任務讀取 HBase 中的全量數據構建索引,生成 lucene 索引文件這種索引生成方式,相比於:線上直接通過 restful api index document 減少了大量的網絡開銷和IO開銷(寫數據過程中還有寫translog開銷+merge開銷)
  6. 在讀取 HBase 數據生成索引分片時,能夠使用更靈活的文檔路由策略 ShardingStrategy,ES 默認的按 doc_id 哈希可能導致分片不均勻,可以根據全量數據(可以提前知道一些特徵)自定義路由策略,從而確保分片“絕對”均勻

需要注意的地方:
ES查詢非常依賴操作系統的緩存,ES節點每天都會加載一份最新的索引數據(每天構建一遍離線索引+增量索引,這些索引都可能被裝載到內存中),ES節點通過定時任務每天更新索引時,可清除以前的緩存,避免生成的當天的索引在查詢時沒有足夠的page cache而導致過多的內存換入、換出。比如構建20220104的索引時,01、02、03的索引已經過時,但有可能在內存中,而所有的查詢只會查04的數據。

goods_index_20220101
goods_index_20220101_realtime
goods_index_20220102
goods_index_20220102_realtime
goods_index_20220103
goods_index_20220103_realtime

使用 HBase 做底層存儲的問題

  1. HBase 存儲容量適合大規模的索引構建,這裏也是基於 Spark 任務全量讀取 HBase 上的索引數據構建索引。如果使用單機JVM進程構建全量索引,極有可能頻繁 full gc

  2. HBase 的列式數據庫特性能夠很好地支持動態添加字段的需求

  3. 基於搜索的其他業務需求可能需要掃描全量的ES索引中的數據,如果直接遍歷ES,肯定會對線上搜索服務性能造成影響,爲了避免直接遍歷ES索引數據,因此需要:一張“基礎索引表”。而從上面的索引構建的架構來看,HBase 中的數據與ES索引中的數據是實時同步的,因此可以作爲索引基礎表來使用。但是,由於 HBase 只能基於主鍵 row_key 進行查詢,而在索引構建時,有時甚至對主鍵進行了“加鹽”,只支持主鍵的“點查”,對大部分場景下的查詢非常不友好。
    比如根據行政區id過濾所有的不可配送的商品,對線上搜索流程而言,只能通過一個個的 query 請求召回商品,再走不可配送過濾邏輯。這相當於是從 query 角度獲取“滿足某些條件”的商品。這裏的 query 角度是指:通過 query 來獲取商品,而“條件”是指:商品在當前行政區下可配送。而實際上很多搜索需求,並不是從 query 角度獲取商品的,比如 query 推薦中計算某一類商品的“候選推薦詞”,如果“候選推薦詞”召回的商品都是不可配送的,需要將此候選推薦詞過濾掉,也需要用到“不可配送”邏輯的過濾。而這個過濾,如果通過走線上搜索流程過濾,就會影響用戶的搜索主流程。一種比較好的方案是查詢“基礎索引表”,但是正如索引構建架構所示:這裏的基礎索引表是 HBase ,而 HBase 不支持二級索引,只支持基於 row_key 的點查,無法滿足此要求。

  4. 使用 tidb 替代 HBase 存儲全量索引數據。首先解決了 HBase 只支持 row_key 點查的問題,能夠針對各類過濾條件進行查詢(tidb 能夠很好地支持二級索引),從而更靈活方便地支撐業務需求。比如:搜索過濾出某些活動標籤、過濾不可配送的商品……能夠基於商品的標籤進行過濾了(不用查詢ES,從而不影響線上搜索主流程)。此外,tidb 也是分佈式存儲系統,能夠支撐大規模索引存儲。最後,tidb 支持 online DDL 在線改表增加字段,不會導致大規模鎖表,也能方便地在字段上添加二級索引。

  5. 對於離線索引的構建,需要讀取全量的 tidb 數據,可藉助 tispark 實現。藉助於 tispark 以及 tidb,能夠實現 T+0 數據查詢,而且相比於 HBase,能夠支持二級索引等各種條件的過濾,對業務開發非常友好。

增量索引構建

ES集羣部署方式

  1. 單集羣跨機房部署
    只有一個ES集羣,集羣下不同ES節點部署在不同的機房,這樣能做到跨機房容災。但是對於線上要求較高的系統,機房之間的網絡抖動很可能導致ES節點的不穩定,比如出現頻繁選主,或者因心跳檢測超時,master將某臺data節點摘除而出現數據恢復,這對線上核心系統來說是不可接受的。
  2. 集羣同機房部署,部署多集羣
    一個ES集羣下的所有節點部署在同一個機房下,但是爲了跨機房容災,需要在同地域下的另一個機房再部署一個同樣的ES集羣。這樣兩個集羣互爲備份,從而做到跨機房容災。部署2個ES集羣,需要解決這2個集羣中的數據是完全相同的,這樣才能在一個機房的ES集羣出現故障後,可切換到另一個機房的集羣上。
    集羣內的節點都部署在同一個機房,機房內的網絡比較穩定,這樣能夠保證ES節點非常穩定,避免網絡抖動造成的集羣不穩定。(線上核心服務一般都是同機房部署)
    雙集羣部署也有利於日常的索引更新,即做到離線索引每天更新一次,增量索引可做到秒級內實時更新。相比於單集羣,可以不停服務遷索引。當有 doc 更新時,從離線索引裏面刪除doc,並往增量索引裏面插入新doc。當有新 doc 時,只會寫增量索引。查詢時,同時向離線索引和增量索引發起查詢。
  3. 集羣同機房部署,跨地域部署多集羣
    這種方式與第2種方式相同,只是在不同的城市/地域部署ES集羣,一是跨地域容災,另一個則是降低跨地域調用導致的網絡延時(電商搜索)。

雙集羣部署,索引上線切流量示例圖:

  1. 索引構建平臺準備構建ES集羣1的索引,將ES集羣1的流量摘除。此時,線上只有ES集羣2
  2. ES集羣1的索引構建完畢,恢復集羣1的流量
  3. 摘除ES集羣2的流量,開始集羣2的索引構建
  4. 集羣2索引構建完畢,恢復流量,此時線上雙集羣同時在線
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章