Drivetribe採取CQRS和Apache Flink的經驗分享

banq發表於2017-03-10
Drivetribe是由前Top Gear三劍客克拉克森、哈蒙德和梅創辦的線上垂直汽車社群, Aris Koliopoulos作為其高階軟體工程師,所在團隊負責從無到有建立這樣一個社群產品,目標是從一開始就可以處理高使用者量和大規模執行,因為這是一項明星產品,一旦上線會立即吸引大量粉絲,因此不得不對Drivetribe架構進行及早決策。

在這篇文章中,我們將解釋如何Apache Flink等技術建立drivetribe.com,也會討論如何為終端使用者提供更好的體驗。


Drivetribe簡介
首先,談一點點關於drivetribe是什麼樣社群網站。當使用者建立帳戶後,可加入“Tribes部落”,這是有特定主題的團體,舉辦者可以是三個聯合創始人之一或其他部落格,專家和汽車愛好者們。

部落的主題涵蓋了從復古、越野、到世界各地旅行。在部落中,使用者可以釋出自己的內容和評論,轉發,或“bump撞”(我們的術語為“喜歡”)由其他使用者釋出的內容。

使用者的home feed會凸顯其最活躍的部落,也顯示了他們在部落中新的他們會感興趣的新帖子。這需要我們進行個性化使用者的內容。當從零開始(手上最小的訓練資料)啟動一個像我們這樣的產品,對內容方面有關排序模型的靈活性是必不可少的。

架構概述
drivetribe是一家資料驅動的公司,我們想捕捉一切並計數計量。當第一次思考我們的架構時,我們認為傳統的方法:無狀態伺服器加上包含可變狀態的資料庫,在狀態轉換時並不記錄歷史轉換日誌,這樣的架構一旦出錯,就無法追蹤錯誤原因,這種不夠靈活性完全不符合我們的要求。

我們是一個Scala開發者團隊,我們感謝不變性。此外,在嘗試不同的演算法的能力和快速迭代的規模也是非常重要的。這導致了我們的第一個決定:利用事件溯源Event-Sourcing和命令查詢職責分離(cqrs)。

事件日誌的存在作為事實真相的來源在產生物化檢視(materialised views)時提供了極大的靈活性。不同的團隊可以並行工作在相同的資料。演算法可以很容易地更換和應用到整個事件的歷史追溯。同樣的方法可以應用到bug修正。沒有任何服務中斷刪除情況下,每一個服務的下游消費可以載入和升級。

cqrs提供單獨的讀路徑和寫路徑分離,易於最佳化和獨立擴充套件。

這個過程是有一個陡峭的學習曲線。需要找出有多少元件需要配置、部署、維護和縮放。微妙的錯誤可能危及分散式程式的正確性,在系統級別上就無法保證一致性。

Golden Hammer是一個著名的反模式工程,我們的方法也不是適合每一個分散式系統。然而,它具有大型釋出平臺平臺的潛力,因此我們決定冒險去實現它。

剛開始建立這樣一個體繫結構,我們需要做出一些重要的決定。Apache卡夫卡™是可擴充套件、容錯、可靠的分散式日誌。理所當然選擇它作為我國架構的骨架。

系統的入口點由一個一大堆無狀態RESTful伺服器組成。他們消費來自Redis從Elasticsearch的物化檢視和產生的資料以訊息的形式放入卡夫卡。這是相當簡單的。

這個架構最具挑戰性的部分是選擇下游分散式的卡夫卡消費者。評估備選方案時有幾個選項:Apache Storm,Apache samza,Apache Spark,和Apache Flink。我們評估是基於一些標準系統:

1.可證明的可擴充套件性和魯棒性
2.高效能
3.豐富的、可擴充套件的API
4.管理狀態和與其他部分整合測試能力(即卡夫卡,redis,和ElasticSearch整合)

其他因素也被考慮在內,如活躍的開源社群,如果有需要商業支援的能力。

最初的評估和試驗階段後,Flink贏得了比賽。這是唯一的勝出者,時刻關注狀態的資料流。

Apache Flink在drivetribe
在平臺上產生資訊的每一個位元組都經過卡夫卡Kafka到弗林克Flink。使用者的行動是由卡夫卡的訊息表示,Flink是負責消費這些資訊並更新我們的內部資料模型。

我們執行大量Flink的job,從簡單到複雜的狀態還原持久。持久化是在平常的情況下,持久一個資料點到服務Serving層已經足夠。

持久化使用者的評論,或“撞”時,Flink的低延遲使我們忽視了API級別的最終一致性。

然而,Flink真正的力量在於兩個關鍵特徵:狀態的計算和定義的拓撲結構的能力。在drivetribe,我們計數統計和彙總統計任何資料。他們中的一些是使用者可見的(即凸點數、評論數、印象數,每天bumps數)。

其它許多其他屬性則是用於我們內部的排序模型。新聞內容是演算法生成的,透過Flink實現我們的排名模型。此外,我們最近推出了透過Flink的流資料協同過濾能力實現了產品推薦。

我們計算統計平臺的每個實體。在預設情況下,需要大量的狀態。Flink的狀態流的抽象定義視窗和folding使得工作輕鬆。我們只需要定義集合一個適當的資料型別(例如,計數器可以建模為Monoid),然後Flink照顧雜湊分割槽金鑰空間和執行分散式計算。這使我們能夠計算出關於內容的後設資料,如點選率,“魅力”,合作等,使用此後設資料餵食內部排序和預測模型。

當然,程式碼是漏洞百出,演算法也可以提高,可以增強其特徵。事實上,事實源頭是一個日誌,包含所有發生事件,這是一套改變我們的實現部署的新的工作過程,使用新的演算法產生事件流,。在幾個小時內,這個平臺提供了完全可更新的檢視。對於一個如我們年輕的初創公司,能隨時改變我們的想法和提高迭代速度的能力是非常有價值的。

回放卡夫卡時提出了一系列的挑戰。從一開始的每一個狀態轉移都變得可見了,雖然很方便觀察,但是對於一個執行系統是不可接受的。為了解決這個問題,我們採用了黑色/白色架構(也被稱為藍色/綠色)。

從概念上講,drivetribe有兩個系統,黑色和白色。每個人都有自己的Flink叢集,Elasticsearch叢集,Redis叢集和RESTful伺服器。他們都有卡夫卡。下圖描述了上述的結構。

[img index=1]

這種方法使我們能夠在服務層平行於正在執行的系統重建Flink狀態和下游的物化檢視。一旦這個過程完成,一個簡單的負載平衡器重定向流量到新系統,無需停機。適合重大變化的部署,無計劃的停機,或痛苦的資料庫遷移。

一個主要挑戰是重播卡夫卡要不違反因果一致性,從而才能夠產生確定的狀態。事件通常有某種因果關係;使用者在加入部落之前需要被建立,和在收到“bumps”,瀏覽或評論之前帖子必須被建立。

卡夫卡只能保證一個主題topic分割槽內的有序。假設帶key的訊息,所有的訊息都對應一個key,這個key是按順序遞交傳遞的(因此,對同一個key發生“刪除”後不會發生“更新”事件)。然而,Flink的job常常消費多個主題,Flink允許使用任何欄位作為流的Key。當處理訊息到達時,就是假設在所有訊息傳遞路徑都有相同延遲(這實際是不可能的),訊息之間因果關係還是會被破壞。任何流如果消費多個主題,或者使用不同的多於一個的key分割槽資訊,都需要考慮到這種情況。

三分之一個挑戰還來自重複的存在。Flink雖然能保證“精確一次exactly-once”傳遞其內部狀態,以整個管道考慮這是非常困難的:一個客戶端兩次傳送相同的請求,或是卡夫卡的生產者會傳送兩次訊息,這意味著卡夫卡已經存在重複。一個接收者將兩次向外部系統提供處理後的資訊,一個簡單的計數器可能會計算錯誤的結果。簡單的處理方法是,每一個訊息有一個唯一id,每次請求都是冪等的。。我們對計數實現帶有語義的資料結構,我們對外部系統只執行冪等更新。

另一個挑戰是我們所謂的“克拉克森明星效應The Clarkson Effect”。傑瑞米·克拉克森是非常受歡迎的–它的多級別超過普通使用者級,這同樣適用於部落和他的帖子。當試圖分配key空間,流計算可以分佈到多個節點,我們很自然地實現了傾斜計算的問題。當需要計算瀏覽量和他的部落已收到的總數時,這成為問題,例如。如果每一個瀏覽都訪問rocksdb,這個訪問資料庫過程自然變得緩慢和積聚背壓back pressure 。為了緩解這一問題,我們利用Flink的低階API開發了一個記憶體緩衝區計算:預彙總或區域性彙總計算,使用處理時間觸發傳播結果到下游。

最後,直到多訊息commit在卡夫卡成為現實,平臺是不支援開箱事務的。然而,在產品各方面要求事務語義。例如,當使用者被刪除,每一個部落/後/評論/等與他們的需求也被刪除。這是一套需要致力於在多個主題單元資訊。部分傳播可能會導致不一致的狀態,當使用者被刪除,但他的帖子依然出現在平臺上。

雖然卡夫卡不支援事務,但Redis是支援。訊息是作為一個組提交到Redis,然後在Flink自定義Redis源獲取訊息推到卡夫卡。因為卡夫卡的語義一般是“至少一次”,訊息是冪等的真的很重要。如果訊息生產過程未能提交訊息到Redis的事務作為一個整體將視為失敗。如果消費程式崩潰,Flink將重新啟動並重新處理訊息。如果我們認為Redis是事務日誌,這基本上被認為是一個向前滾動的語義Saga事務。

Flink的開發經驗
Flink的高階別API使定義狀態流的計算簡單容易地操作Scala集合。Flink的低階別API允許有經驗的使用者擴充套件和每個流的用例最佳化。

在生產執行Flink我們努力地學習一些技巧:

1. 儘可能多執行job。理想情況下,每一個圖一個job。無論多好的測試程式碼,仍然會在生產環節有失敗,實際上是提供一種可降級的服務水平很重要。此外,每個圖都有不同的要求,能夠管理就很重要,還要能獨立擴充套件和獨立解決自己的問題。

2. 注意每個key的狀態大小。當每個key增加幾百KB時候流效能會降低。此外,推送大量訊息到下游最終會導致網路飽和堵塞。

3. 當使用卡卡savepoint儲存點注意有狀態的操作。運算子名稱必須是唯一的,所有的類引用需要保持不變。否則,反序列化將會失敗,狀態將不得不從頭開始建立。

4.注意檢查點checkpoint管理。檢查點不是自增的,如果沒有後臺程式清理很容易耗盡磁碟空間。

5. 花時間去建立一個適當的部署管道。持續整合不是像停止和啟動一個無狀態伺服器那麼簡單。這需要時間來完善。

6. 連線流會很重。如果目標是很多資料,考慮denormalising上游。

7. 應該使用訊息key和有效載荷payload,才能利用卡夫卡的壓縮主題功能。這可以節省儲存空間,減少加工時間和提高排序保證。更新事件應該包含完整的更新payload(如更新使用者配置檔案),而不是一個差異版本。

8. 收集和分析資料,否則很難除錯生產中出現的問題。


Drivetribe’s Modern Take On CQRS With Apache Flink

相關文章