本文已整理致我的 github 地址 https://github.com/allentofight/easy-cs,歡迎大家 star 支援一下
前言
近年來公司業務迅猛發展,資料量爆炸式增長,隨之而來的的是海量資料查詢等帶來的挑戰,我們需要資料量在十億,甚至百億級別的規模時依然能以秒級甚至毫秒級的速度返回,這樣的話顯然離不開搜尋引擎的幫助,在搜尋引擎中,ES(ElasticSearch)毫無疑問是其中的佼佼者,連續多年在 DBRanking 的搜尋引擎中評測中排名第一,也是絕大多數大公司的首選,那麼它與傳統的 DB 如 MySQL 相比有啥優勢呢,ES 的資料又是如何生成的,資料達到 PB 時又是如何保證 ES 索引資料的實時性以更好地滿足業務的需求的呢。
本文會結合我司在 ES 上的實踐經驗與大家談談如何構建準實時索引的一些思路,希望對大家有所啟發。本文目錄如下
- 為什麼要用搜尋引擎,傳統 DB 如 MySQL 不香嗎
- MySQL 的不足
- ES 簡介
- ES 索引資料構建
- PB 級的 ES 準實時索引資料構建之道
為什麼要用搜尋引擎,傳統 DB 如 MySQL 不香嗎
MySQL 的不足
MySQL 架構天生不適合海量資料查詢,它只適合海量資料儲存,但無法應對海量資料下各種複雜條件的查詢,有人說加索引不是可以避免全表掃描,提升查詢速度嗎,為啥說它不適合海量資料查詢呢,有兩個原因:
1、加索引確實可以提升查詢速度,但在 MySQL 中加多個索引最終在執行 SQL 的時候它只會選擇成本最低的那個索引,如果沒有索引滿足搜尋條件,就會觸發全表掃描,而且即便你使用了組合索引,也要符合最左字首原則才能命中索引,但在海量資料多種查詢條件下很有可能不符合最左字首原則而導致索引失效,而且我們知道儲存都是需要成本的,如果你針對每一種情況都加索引,以 innoDB 為例,每加一個索引,就會建立一顆 B+ 樹,如果是海量資料,將會增加很大的儲存成本,之前就有人反饋說他們公司的某個表實際內容的大小才 10G, 而索引大小卻有 30G!這是多麼巨大的成本!所以千萬不要覺得索引建得越多越好。
2、有些查詢條件是 MySQL 加索引都解決不了的,比如我要查詢商品中所有 title 帶有「格力空調」的關鍵詞,如果你用 MySQL 寫,會寫出如下程式碼
SELECT * FROM product WHERE title like '%格力空調%'
這樣的話無法命中任何索引,會觸發全表掃描,而且你不能指望所有人都能輸對他想要的商品,是人就會犯錯誤,我們經常會犯類似把「格力空調」記成「格空間」的錯誤,那麼 SQL 語句就會變成:
SELECT * FROM product WHERE title like '%格空調%'
這種情況下就算你觸發了全表掃描也無法查詢到任何商品,綜上所述,MySQL 的查詢確實能力有限。
ES 簡介
與其說上面列的這些點是 MySQL 的不足,倒不如說 MySQL 本身就不是為海量資料查詢而設計的,術業有專攻,海量資料查詢還得用專門的搜尋引擎,這其中 ES 是其中當之無愧的王者,它是基於 Lucene 引擎構建的開源分散式搜尋分析引擎,可以提供針對 PB 資料的近實時查詢,廣泛用在全文檢索、日誌分析、監控分析等場景。
它主要有以下三個特點:
輕鬆支援各種複雜的查詢條件
: 它是分散式實時檔案儲存,會把每一個欄位都編入索引(倒排索引),利用高效的倒排索引,以及自定義打分、排序能力與豐富的分詞外掛等,能實現任意複雜查詢條件下的全文檢索需求可擴充套件性強
:天然支援分散式儲存,通過極其簡單的配置實現幾百上千臺伺服器的分散式橫向擴容,輕鬆處理 PB 級別的結構化或非結構化資料。高可用,容災效能好
:通過使用主備節點,以及故障的自動探活與恢復,有力地保障了高可用
我們先用與 MySQL 類比的形式來理解 ES 的一些重要概念

通過類比的形式不難看出 ES 的以下幾個概念
1、 MySQL 的資料庫(DataBase)相當於 Index(索引),資料的邏輯集合,ES 的工作主要也是建立索引,查詢索引。
2、 一個資料庫裡會有多個表,同樣的一個 Index 也會有多個 type
3、 一個表會有多行(Row),同樣的一個 Type 也會有多個 Document。
4、 Schema 指定表名,表欄位,是否建立索引等,同樣的 Mapping 也指定了 Type 欄位的處理規則,即索引如何建立,是否分詞,分詞規則等
5、 在 MySQL 中索引是需要手動建立的,而在 ES 一切欄位皆可被索引,只要在 Mapping 在指定即可
那麼 ES 中的索引為何如此高效,能在海量資料下達到秒級的效果呢?它採用了多種優化手段,最主要的原因是它採用了一種叫做倒排索引的方式來生成索引,避免了全文件掃描,那麼什麼是倒排索引呢,通過文件來查詢關鍵詞等資料的我們稱為正排索引,返之,通過關鍵詞來查詢文件的形式我們稱之為倒排索引,假設有以下三個文件(Document)

要在其中找到含有 comming 的文件,如果要正排索引,那麼要把每個文件的內容拿出來查詢是否有此單詞,毫無疑問這樣的話會導致全表掃描,那麼用倒排索引會怎麼查詢呢,它首先會將每個文件內容進行分詞,小寫化等,然後建立每個分詞與包含有此分詞的文件之前的對映關係,如果有多個文件包含此分詞,那麼就會按重要程度即文件的權重(通常是用 TF-IDF 給文件打分)將文件進行排序,於是我們可以得到如下關係

這樣的話我們我要查詢所有帶有 comming 的文件,就只需查一次,而且這種情況下查詢多個單詞效能也是很好的,只要查詢多個條件對應的文件列表,再取交集即可,極大地提升了查詢效率。
畫外音:這裡簡化了一些流程,實際上還要先根據單詞表來定位單詞等,不過這些流程都很快,可以忽略,有興趣的讀者可以查閱相關資料瞭解。
除了倒排索引外,ES 的分散式架構也天然適合海量資料查詢,來看下 ES 的架構

一個 ES 叢集由多個 node 節點組成,每個 index 是以分片(Shard,index 子集)的資料存在於多個 node 節點上的,這樣的話當一個查詢請求進來,分別在各個 node 查詢相應的結果並整合後即可,將查詢壓力分散到多個節點,避免了單個節點 CPU,磁碟,記憶體等處理能力的不足。
另外當新節點加入後,會自動遷移部分分片至新節點,實現負載均衡,這個功能是 ES 自動完成的,對比一個下 MySQL 的分庫分表需要開發人員引入 Mycat 等中介軟體並指定分庫分表規則等繁瑣的流程是不是一個巨大的進步?這也就意味著 ES 有非常強大的水平擴充套件的能力,叢集可輕鬆擴充套件致幾百上千個節點,輕鬆支援 PB 級的資料查詢。
當然 ES 的強大不止於此,它還採用了比如主備分片提升搜尋吞吐率,使用節點故障探測,Raft 選主機制等提升了容災能力等等,這些不是本文重點,讀者可自行查閱,總之經過上面的簡單總結大家只需要明白一點:ES 的分散式架構設計天生支援海量資料查詢。
那麼 ES 的索引資料(index)是如何生成的呢,接下來我們一起來看看本文的重點
如何構建 ES 索引
要構建 ES 索引資料,首先得有資料來源,一般我們會使用 MySQL 作為資料來源,你可以直接從 MySQL 中取資料然後再寫入 ES,但這種方式由於直接呼叫了線上的資料庫查詢,可能會對線上業務造成影響,比如考慮這樣的一個場景:
在電商 APP 裡用的最多的業務場景想必是使用者輸入關鍵詞來查詢相對應的商品了,那麼商品會有哪些資訊呢,一個商品會有多個 sku(sku 即同一個商品下不同規格的品類,比如蘋果手機有 iPhone 6, iPhone 6s等),會有其基本屬性如價格,標題等,商品會有分類(居家,服飾等),品牌,庫存等,為了保證表設計的合理性,我們會設計幾張表來儲存這些屬性,假設有 product_sku(sku 表), product_property(基本屬性表),sku_stock(庫存表),product_category(分類表)這幾張表,那麼為了在商品展示列表中展示所有這些資訊,就必須把這些表進行 join,然後再寫入 ES,這樣查詢的時候就會在 ES 中獲取所有的商品資訊了。

這種方案由於直接在 MySQL 中執行 join 操作,在商品達到千萬級時會對線上的 DB 服務產生極大的效能影響,所以顯然不可行,那該怎麼生成中間表呢,既然直接在 MySQL 中操作不可行,能否把 MySQL 中的資料同步到另一個地方再做生成中間表的操作呢,也就是加一箇中間層進行處理,這樣就避免了對線上 DB 的直接操作,說到這相信大家又會對計算機界的名言有進一步的體會:沒有什麼是加一箇中間層不能解決的,如果有,那就再加一層。
這個中間層就是 hive
什麼是 hive
hive 是基於 Hadoop 的一個資料倉儲工具,用來進行資料提取、轉化、載入,這是一種可以儲存、查詢和分析儲存在 Hadoop 中的大規模資料的機制,它的意義就是把好寫的 hive 的 sql 轉換為複雜難寫的 map-reduce 程式(map-reduce 是專門用於用於大規模資料集(大於1TB)的並行運算程式設計模型),也就是說如果資料量大的話通過把 MySQL 的資料同步到 hive,再由 hive 來生成上述的 product_tmp 中間表,能極大的提升效能。hive 生成臨時表儲存在 hbase(一個分散式的、面向列的開源資料庫) 中,生成後會定時觸發 dump task 呼叫索引程式,然後索引程式主要從 hbase 中讀入全量資料,進行業務資料處理,並重新整理到 es 索引中,整個流程如下

這樣構建索引看似很美好,但我們需要知道的是首先 hive 執行 join 任務是非常耗時的,在我們的生產場景上,由於資料量高達幾千萬,執行 join 任務通常需要幾十分鐘,從執行 join 任務到最終更新至 ES 整個流程常常需要至少半小時以上,如果這期間商品的價格,庫存,上線狀態(如被下架)等重要欄位發生了變更,索引是無法更新的,這樣會對使用者體驗產生極大影響,優化前我們經常會看到通過 ES 搜尋出的中有狀態是上線但實際是下架的產品,嚴重影響使用者體驗,那麼怎麼解決呢,有一種可行的方案:建立寬表
既然我們發現 hive join 是效能的主要瓶頸,那麼能否規避掉這個流程呢,能否在 MySQL 中將 product_sku,product_property,sku_stock 等表組合成一個大表(我們稱其為寬表)

這樣在每一行中商品涉及到的的資料都有了,所以將 MySQL 同步到 hive 後,hive 就不需要再執行耗時的 join 操作了,極大的提升了整體的處理時間,從 hive 同步 MySQL 再到 dump 到 ES 索引中從原來的半小時以上降低到了幾分鐘以內,看起來確實不錯,但幾分鐘的索引延遲依然是無法接受的。
為什麼 hive 無法做到實時匯入索引
因為 hive 構建在基於靜態批處理的Hadoop 之上,Hadoop 通常都有較高的延遲並且在作業提交和排程的時候需要大量的開銷。因此,hive 並不能夠在大規模資料集上實現低延遲快速的查詢等操作,再且千萬級別的資料全量從索引程式匯入到 ES 叢集至少也是分鐘級。
另外引入了寬表,它的維護也成了一個新問題,設想 sku 庫存變了,產品下架了,價格調整了,那麼除了修改原表(sku_stock,product_categry 等表)記錄之外,還要將所有原表變更到的記錄對應到寬表中的所有記錄也都更新一遍,這對程式碼的維護是個噩夢,因為你需要在所有商品相關的表變更的地方緊接著著變更寬表的邏輯,與寬表的變更邏輯變更緊藕合!
PB 級的 ES 準實時索引構建之道
該如何解決呢?仔細觀察上面兩個問題,其實都是同一個問題,如果我們能實時監聽到 db 的欄位變更,再將變更的內容實時同步到 ES 和寬表中不就解決了我們的問題了。
怎麼才能實時監聽到表欄位的變更呢?
答案:binlog
來一起復習下 MySQL 的主從同步原理

- MySQL master 將資料變更寫入二進位制日誌( binary log, 其中記錄叫做二進位制日誌事件binary log events,可以通過 show binlog events 進行檢視)
- MySQL slave 將 master 的 binary log events 拷貝到它的中繼日誌(relay log)
- MySQL slave 重放 relay log 中事件,將資料變更反映它自己的資料
可以看到主從複製的原理關鍵是 Master 和 Slave 遵循了一套協議才能實時監聽 binlog 日誌來更新 slave 的表資料,那我們能不能也開發一個遵循這套協議的元件,當元件作為 Slave 來獲取 binlog 日誌進而實時監聽表欄位變更呢?阿里的開源專案 Canal 就是這個乾的,它的工作原理如下:
- canal 模擬 MySQL slave 的互動協議,偽裝自己為 MySQL slave ,向 MySQL master 傳送dump 協議
- MySQL master 收到 dump 請求,開始推送 binary log 給 slave (即 canal )
- canal 解析 binary log 物件(原始為 byte 流)

這樣的話通過 canal 就能獲取 binlog 日誌了,當然 canal 只是獲取接收了 master 過來的 binlog,還要對 binlog 進行解析過濾處理等,另外如果我們只對某些表的欄位感興趣,該如何配置,獲取到 binlog 後要傳給誰? 這些都需要一個統一的管理元件,阿里的 otter 就是幹這件事的。
什麼是 otter
Otter 是由阿里提供的基於資料庫增量日誌解析,準實時同步到本機房或異地機房 MySQL 資料庫的一個分散式資料庫同步系統,它的整體架構如下:

注:以上是我司根據 otter 改造後的業務架構,與原版 otter 稍有不同,不過大同小異
主要工作流程如下
- 在 Manager 配置好 zk,要監聽的表 ,負責監聽表的節點,然後將配置同步到 Nodes 中
- node 啟動後則其 canal 會監聽 binlog,然後經過 S(select),E(extract),T(transform),L(load) 四個階段後資料傳送到 MQ
- 然後業務就可以訂閱 MQ 訊息來做相關的邏輯處理了
畫外音:zookeeper 主要協調節點間的工作,如在跨機房資料同步時,可能要從 A 機房的節點將資料同步到 B 機房的節點,要用 zookeeper 來協調,
大家應該注意到了node 中有 S,E,T,L 四個階段,它們的主要作用如下

Select 階段
: 為解決資料來源的差異性,比如接入 canal 獲取增量資料,也可以接入其他系統獲取其他資料等。Extract階段
: 組裝資料,針對多種資料來源,mysql,oracle,store,file等,進行資料組裝和過濾。Transform 階段
: 資料提取轉換過程,把資料轉換成目標資料來源要求的型別Load 階段
: 資料載入,把資料載入到目標端,如寫入遷移後的資料庫, MQ,ES 等
以上這套基於阿里 otter 改造後的資料服務我們將它稱為 DTS(Data Transfer Service),即資料傳輸服務。
搭建這套服務後我們就可以通過訂閱 MQ 來實時寫入 ES 讓索引實時更新了,而且也可以通過訂閱 MQ 來實現寬表欄位的更新,實現了上文中所說的寬表欄位更新與原表緊藕合的邏輯,基於 DTS 服務的索引改進架構如下:

注意:「build 資料」這一模組對實時索引更新是透明的,這個模組主要用在更新或插入 MySQL 寬表,因為對於寬表來說,它是幾個表資料的並集,所以並不是監聽到哪個欄位變更就更新哪個,它要把所有商品涉及到的所有表資料拉回來再更新到寬表中。
於是,通過 MySQL 寬表全量更新+基於 DTS 的實時索引更新我們很好地解決了索引延遲的問題,能達到秒級的 ES 索引更新!
這裡有幾個問題可能大家比較關心,我簡單列一下
需要訂閱哪些欄位
對於 MySQL 寬表來說由於它要儲存商品的完整資訊,所以它需要訂閱所有欄位,但是對於紅框中的實時索引更新而言,它只需要訂閱庫存,價格等欄位,因為這些欄位如果不及時更新,會對銷量產生極大的影響,所以我們實時索引只關注這些敏感欄位即可。
有了實時索引更新,還需要全量索引更新嗎
需要,主要有兩個原因:
- 實時更新依賴訊息機制,無法百分百保證資料完整性,需要全量更新來支援,這種情況很少,而且訊息積壓等會有告警,所以我們一天只會執行一次全量索引更新
- 索引叢集異常或崩潰後能快速重建索引
全量索引更新的資料會覆蓋實時索引嗎
會,設想這樣一種場景,你在某一時刻觸發了實時索引,然後此時全量索引還在執行中,還未執行到實時索引更新的那條記錄,這樣在的話當全量索引執行完之後就會把之前實時索引更新的資料給覆蓋掉,針對這種情況一種可行的處理方式是如果全量索引是在構建中,實時索引更新訊息可以延遲處理,等全量更新結束後再消費。也正因為這個原因,全量索引我們一般會在凌晨執行,由於是業務低峰期,最大可能規避了此類問題。
總結
本文簡單總結了我司在 PB 級資料下構建實時 ES 索引的一些思路,希望對大家有所幫助,文章只是簡單提到了 ES,canal,otter 等阿里中介軟體的應用,但未對這些中介軟體的詳細配置,原理等未作過多介紹,這些中介軟體的設計非常值得我們好好研究下,比如 ES 為了提高搜尋效率、優化儲存空間做了很多工作,再比如 canal 如何做高可用,otter 實現異地跨機房同步的原理等,建議感興趣的讀者可以之後好好研究一番,相信你會受益匪淺。
如果大家對方案有些疑問,歡迎大家積極留言探討,共同進步^_^,更多精品文章,歡迎大家掃碼關注「碼海」

巨人的肩膀
- Elasticsearch簡介及與MySQL查詢原理對比:https://www.jianshu.com/p/116cdf5836f2
- https://www.cnblogs.com/zhjh256/p/9261725.html
- otter安裝之otter-node安裝(單機多節點安裝):https://blog.csdn.net/u014642915/article/details/96500957
- MySQL和Lucene索引對比分析: https://developer.aliyun.com/article/50481
- 10 分鐘快速入門海量資料搜尋分析引擎 Elasticearch: https://www.modb.pro/db/29806
- ElasticSearch和Mysql查詢原理分析與對比:https://www.pianshen.com/article/4254917942/
- 帶你走進神一樣的Elasticsearch索引機制:https://zhuanlan.zhihu.com/p/137574234