第七章:druid.io實踐分享之realtime+kafka 一

目前使用druid已經有3年了,在整個國內互聯網廣告行業瞭解下來,我們算較早使用的團隊。

其優勢太明顯了,就是快,絕大多數的場景都可以在毫秒或秒級響應(特別是數據量足夠大的情況下,還能保持良好的速度)。
其二就是提供的功能特別能解決我們業務上的問題。
其三整個系統相對來說還是比較封閉的,減少了不必要的依賴,json的靈活性提供了更好的二次開發的潛力。
其四整體源碼風格是函數式,可以使之前面向對象開發的工程師進行提升(在後期源碼剖析再去好好體會)。

但如何發揮到最好、最穩定,需要更多細節的調整。
我會從整體部署、實時節點、歷史節點、broker節點來依次介紹。

從整體部署來看:
第一:druid.io 屬於IO和CPU雙重密集型引擎,所以對內存、CPU、硬盤IO都有特定要求,特別提醒,如果資金充足,可以直接上SSD(內存和CPU同理)。

第二:部署的整個過程需要多次練習,並記錄成流程規範,因爲整體集羣涉及的層次較多,如果不進行流程規範化,會導致因人不同,使線上操作出現問題

第三:相應的監控機制,要配合到位,後期在整體部署中,逐步進行實施自動化方式

第四:各節點類型服務啓動時,沒有明確的先後順序

整體完整結構如下:
這裏寫圖片描述

目前作者成立的https://imply.io 提供了更加完善的解決方案,有興趣的可以參考下。

首先我們來分析下實時節點。
對實時節點(realtime)我考慮從兩個方面來分享:
一個方面:數據本身要求
一個方面:realtime運行階段出現的一些問題(如:segment堆積等)

首先我會對背景進行介紹下,我們是一個CPA廣告聯盟,有impression、click、conversion等類型的數據。對我們而言conversion數據最重要,因爲我們是conversion來計費的。所以就有一個高標準conversion類型數據不能丟失且不能重複,對另外兩類型數據在一定範圍內可以接受少數丟失和少數重複。我們是基於6.0的版本來分析的。

初期druid的實時數據獲取的方式是通過realtime節點結合kafka的方式,所以提供了不丟失數據的優勢,如果對實時數據的各方面要求很高的前提下,realtime節點結合kafka的方式還是會帶來諸多問題(這也是新版本中已經不推薦使用的原因,個人見解任何系統只要使用了kafka集羣都會有這樣的情況發生,只要努力去解決就好)。
場景如下:
場景一:realtime保證了不少數據,但沒保證不多數據
場景二:realtime節點是單點特性,這樣一旦一個節點出問題,對數據敏感的話,會立馬發現問題
場景三:realtime節點沒有提供安全關閉的邏輯(官方是提供直接kill方式)
場景四:realtime在某些場景中,還是會掉數據
另外也存在少量的bug,這個可以忽略下(在6.0版本之後都得到了修復)。我將一一對這些場景的產生及解決方式進行闡述。

場景一:

確實只要你的數據進入了kafka集羣,數據是真的不丟失(當然如果kafka的硬盤已經滿了這種情況,一般都會用監控的方式去規避吧)。但是爲了支持多個realtime節點,kafka裏的topic必須進行分區,不然無法整體提升併發的能力。如下圖:
這裏寫圖片描述
並且官方文檔給出的kafka使用有一個參數
"auto.commit.enable": "false"
也就是交由realtime節點自行管理。這個參數設置成false後,realtime會在把一個時間段的數據持久化之後,纔會給kafka集羣發一個commit命令。

正是因爲這個邏輯,所以在線上運行階段,當consumer組裏某個一個消費者出現某種問題,會導致沒有及時對topic消費進行響應(但是已經正常消費數據了),這時候kafka會對分區進行重新調整,導致其它消費者會根據上次的offset進行消費數據,從而最終導致數據重複。如下圖:
這裏寫圖片描述

場景二:

總體來說也是由於場景一帶來的副作用,由於分區導致,每一個partition被一個cousumer消費,所以多個realtime節點就是單點特性,kill任意一臺realtime節點反過來也會激活kafka集羣對partition再分配的處理。所以會出現這樣的場景一旦啓動realtime節點,就永久無法正常shutdown(只能用kill方式)。並且多realtime節點一起啓動時,在啓動的過程中,每個節點啓動間隔不能太長(原因大家可以想想,所以一般會使用一個遠程啓動腳本方式,統一進行遠程啓動)。首先我先啓動realtime1節點,如下圖:
這裏寫圖片描述
3個partition先分配給realtime1,分別從offset=300、200、100開始消費。過1分鐘後再啓動realtime2節點,這樣會觸發partition動態分配事件,啓動時假設realtime1已經消費到offset=312、222、133,但還沒有提交commit(所以kafka那邊的offset值是不變的)
如下圖:
這裏寫圖片描述
假設重新分配後,partition-3分配給了realtime2,那麼realtime2節點將從offset=100的位置開始進行消費,如下圖:
這裏寫圖片描述
這就是realtime節點間啓動間隔不能太長的原因。

場景三:

跟場景二有關,就是沒有提供安全關閉的命令(只能kill),這會讓實際操作過程中,有種不安全感,雖然kill掉後,可以通過kafka來恢復,但是在這種場景下,還是會導致數據重複,例如:當realtime已經對數據持久化了,但還沒來得及返回offset告知kafka集羣(是將 auto.commit.enable = false),這時realtime節點被kill掉後。如下圖:
這裏寫圖片描述
kill命令發生在持久化之後,commit之前。如下圖:
這裏寫圖片描述
再重啓realtime節點就會引發數據重複消費。
可能大家會發現,如果kill命令發生在內存Cache到持久化之間,會不會重複?當auto.commit.enable = false情況下,這塊的數據是會從kafka裏恢復回來的。

解決方案

其實場景一、二和三,總結起來其實是一個類別的情況,需要整體一起考慮去解決。
後面版本中,官方了給出了一個方案類似於雙機熱備,簡單的說,就是兩個realitme節點對應一個partition,但我沒有去嘗試過,這裏主要介紹下我們在當時如何解決這樣的場景的(特別是kafka、realtime集羣在擴容或者刪除節點的時候,也會帶來這樣的問題)。

首先給出一個最簡單方式,在採集層做數據的backup,如果你的segment是設置的一個小時,那麼就按小時進行check。發現異常,就用backup數據進行修復還原。
我們嘗試下來後,發現客戶在使用的過程中,會反饋爲什麼上一個小時的數據有變化,給用戶的體驗不好(當然後期我們可以做到提前發消息通知用戶告知),站在用戶的角度出發希望數據不要經常變動,這樣用戶會質疑你的系統,而對我們開發人員來說,可以讓數據補進當前這個時間段裏,總體來說沒有讓數據丟失。其次按每小時check後臺邏輯複雜,人工干預工作量很大。再次就是當我們開發在進行快速迭代和上線的時候,勢必帶來相關數據修復工作,影響效率(最常見的就是增加緯度信息時)。

後來我們討論了後,重新訂了一個新方案

  1. 增加安全關閉邏輯
  2. realtime集羣增加一層Cache,用於重複數據過濾
  3. 改auto.commit.enable = true,並將zk同步時間稍微縮
  4. 定期記錄partition的offset值,用於回滾

注:這些方式不一定是最通用最完美的方式,但在當時一定是適合我們需要的方式。

以下是安全關閉部分

  1. 啓動時 RealtimeManager 爲每個datasouce, 啓動一個線程, 建立一個FireChief
  2. FireChief 持有RealtimePlumber 和Firehose
  3. FireChief消費數據邏輯
    plumber.startJob();
    while(notSafeCloseFlag && firehose.hasNext())
    定時 persistent
    plumber.finishJob();
  4. plumber.finishJob邏輯
    basePersistent 下面的datasource下的文件夾表示sink, 現在系統中一個小時一個Sink
    一個plumber 持有多個sink.
    Sink持有多個FireHydrant, 但只有一個是激活狀態, 用於接收數據
  5. 一個FireHydrant保存在0, 1, 2 文件夾下。
    對應一個IncrementalIndex和一個Segment
    index 表示內存中的對象
    segment 表示持久化對象
    hasSwapped表示十分已經持久化
    swap操作: 建立segment, 將index設置成null
  6. stop邏輯, 見流程圖
    RealtimeManager層次圖:
    這裏寫圖片描述
    stop關閉流程圖:
    這裏寫圖片描述

關於Cache層解決方式,很多人都提過有單點故障,但這個時候需要具體分析了,首先我們這裏主要解決是conversion問題,就數量級來說conversion比impression、click要小太多了;其次Cache的conversion不是永久的(屬於一個區間內),不會出現數據量過於膨脹;再次邏輯簡單因爲conversion只有主鍵存儲及判斷,不會負載很重。要而且經過了時間證明,運行了兩年多零故障率。結構如下圖:
這裏寫圖片描述

後面兩點,屬於小調整,這樣的好處:
一是重複數據量較小,在面對每天10多億數據的流轉過程中,不影響體驗
二是回滾offset的範圍可控

通過此方式,我們進行不斷的調整、測試、驗證和細節優化,最終做到了整體數據丟失率99.99%(由測試部門給出的),conversion數據丟失率是0的效果。並且也讓人工干預的操作減少很多。

場景四:

數據量進來的速率高於消費的速率時,就會引發數據丟失,主要原因就是windowPeriod這個參數設置,假設segment是設置成1個小時,其windowPeriod設置成PT10m,當前時間是13點,那麼就表示在13點10分之前(包含10分)還可以繼續消費12點-13點之間的數據,大家應該知道在海量數據傳輸時,不可能在13點整點就能消費完成12點-13點之間的數據,所以druid在配置時,提供了一個緩衝區間,我們在整個運行過程中出現過兩次,第一次是就是速率突然暴漲導致,第二次是因爲數據的時間導致。

這樣的場景在實際的運營過程中,不會常碰到,但不排除惡意攻擊或者刷流量的情況。
一般解決方式:
第一:流控報警
第二:時間重置

流控主要是定時監控Kafka集羣裏的offset的差值,發現差值變大就要報警。這時的策略就是數據採集服務層降低寫入速度(這部分最後也可以變成自動化方式進行),讓realtime節點集羣的消費得到緩解。然後快速定位到底是達到了當前消費極限還是其它問題。
時間重置通過讀取windowPeriod的值,來進行換算,將此記錄放到下一個時間段裏,
舉例說明:windowPeriod設置成10分鐘,當前的click數據的時間是17:59:44,當前系統時間是18:10:23,這時滿足18:10:23-17:59:44>10分鐘,將對此click數據的時間重置成18:10:23,進入下一個時間段。
這種方式,特別適合自動化補數,提高效率。

以上就是數據本身的要求所帶來的全部內容。

下一篇我將介紹realtime結合kafka在運行階段會出現場景及解決方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章