分佈式隊列編程---基礎篇

一、簡介

作爲一種基礎的抽象數據結構,隊列被廣泛應用在各類編程中。大數據時代對跨進程、跨機器的通訊提出了更高的要求,和以往相比,分佈式隊列編程的運用幾乎已無處不在。但是,這種常見的基礎性的事物往往容易被忽視,使用者往往會忽視兩點:

使用分佈式隊列的時候,沒有意識到它是隊列。
有具體需求的時候,忘記了分佈式隊列的存在。

文章首先從最基礎的需求出發,詳細剖析分佈式隊列編程模型的需求來源、定義、結構以及其變化多樣性。通過這一部分的講解,作者期望能在兩方面幫助讀者:一方面,提供一個系統性的思考方法,使讀者能夠將具體需求關聯到分佈式隊列編程模型,具備進行分佈式隊列架構的能力;另一方面,通過全方位的講解,讓讀者能夠快速識別工作中碰到的各種分佈式隊列編程模型。

文章的第二部分實戰篇。根據作者在新美大實際工作經驗,給出了隊列式編程在分佈式環境下的一些具體應用。這些例子的基礎模型並非首次出現在互聯網的文檔中,但是所有的例子都是按照挑戰、構思、架構三個步驟進行講解的。這種講解方式能給讀者一個“從需求出發去構架分佈式隊列編程”的旅程。

二、分佈式隊列編程模型

模型篇從基礎的需求出發,去思考何時以及如何使用分佈式隊列編程模型。建模環節非常重要,因爲大部分中高級工程師面臨的都是具體的需求,接到需求後的第一個步驟就是建模。通過本篇的講解,希望讀者能夠建立起從需求到分佈式隊列編程模型之間的橋樑。

三、何時選擇分佈式隊列

通訊是人們最基本的需求,同樣也是計算機最基本的需求。對於工程師而言,在編程和技術選型的時候,更容易進入大腦的概念是RPC、RESTful、Ajax、Kafka。在這些具體的概念後面,最本質的東西是“通訊”。所以,大部分建模和架構都需要從“通訊”這個基本概念開始。當確定系統之間有通訊需求的時候,工程師們需要做很多的決策和平衡,這直接影響工程師們是否會選擇分佈式隊列編程模型作爲架構。從這個角度出發,影響建模的因素有四個:When、Who、Where、How。

3.1、When:同步VS異步

通訊的一個基本問題是:發出去的消息什麼時候需要被接收到?這個問題引出了兩個基礎概念:“同步通訊”和“異步通訊”。根據理論抽象模型,同步通訊和異步通訊最本質的差別來自於時鐘機制的有無。同步通訊的雙方需要一個校準的時鐘,異步通訊的雙方不需要時鐘。現實的情況是,沒有完全校準的時鐘,所以沒有絕對的同步通訊。同樣,絕對異步通訊意味着無法控制一個發出去的消息被接收到的時間點,無期限的等待一個消息顯然毫無實際意義。所以,實際編程中所有的通訊既不是“同步通訊”也不是“異步通訊”;或者說,既是“同步通訊”也是“異步通訊”。特別是對於應用層的通訊,其底層架構可能既包含“同步機制”也包含“異步機制”。判斷“同步”和“異步”消息的標準問題太深,而不適合繼續展開。作者這裏給一些啓發式的建議:

發出去的消息是否需要確認,如果不需要確認,更像是異步通訊,這種通訊有時候也稱爲單向通訊(One-Way Communication)。
如果需要確認,可以根據需要確認的時間長短進行判斷。時間長的更像是異步通訊,時間短的更像是同步通訊。當然時間長短的概念是純粹的主觀概念,不是客觀標準。
發出去的消息是否阻塞下一個指令的執行,如果阻塞,更像是同步,否則,更像是異步。

無論如何,工程師們不能生活在混沌之中,不做決定往往是最壞的決定。當分析一個通訊需求或者進行通訊構架的時候,工程師們被迫作出“同步”還是“異步”的決定。當決策的結論是“異步通訊”的時候,分佈式隊列編程模型就是一個備選項。

3.2、Who:發送者接收者解耦

在進行通訊需求分析的時候,需要回答的另外一個基本問題是:消息的發送方是否關心誰來接收消息,或者反過來,消息接收方是否關心誰來發送消息。如果工程師的結論是:消息的發送方和接收方不關心對方是誰、以及在哪裏,分佈式隊列編程模型就是一個備選項。因爲在這種場景下,分佈式隊列架構所帶來的解耦能給系統架構帶來這些好處:

無論是發送方還是接收方,只需要跟消息中間件通訊,接口統一。統一意味着降低開發成本。
在不影響性能的前提下,同一套消息中間件部署,可以被不同業務共享。共享意味着降低運維成本
發送方或者接收方單方面的部署拓撲的變化不影響對應的另一方。解藕意味着靈活和可擴展。

3.3、Where:消息暫存機制

在進行通訊發送方設計的時候,令工程師們苦惱的問題是:如果消息無法被迅速處理掉而產生堆積怎麼辦、能否被直接拋棄?如果根據需求分析,確認存在消息積存,並且消息不應該被拋棄,就應該考慮分佈式隊列編程模型構架,因爲隊列可以暫存消息

3.4、How:如何傳遞

對通訊需求進行架構,一系列的基礎挑戰會迎面而來,這包括:

可用性,如何保障通訊的高可用。
可靠性,如何保證消息被可靠地傳遞。
持久化,如何保證消息不會丟失
吞吐量和響應時間。
跨平臺兼容性。
除非工程師對造輪子有足夠的興趣,並且有充足的時間,採用一個滿足各項指標的分佈式隊列編程模型就是一個簡單的選擇。

四、分佈式隊列編程定義

很難給出分佈式隊列編程模型的精確定義,由於本文偏重於應用,作者並不打算完全參照某個標準的模型。總體而言:分佈式隊列編程模型包含三類角色:發送者(Sender)、分佈式隊列(Queue)、接收者(Receiver)。發送者和接收者分別指的是生產消息和接收消息的應用程序或服務。
需要重點明確的概念是分佈式隊列,它是提供以下功能的應用程序或服務:1. 接收“發送者”產生的消息實體;2. 傳輸、暫存該實體;3. 爲“接收者”提供讀取該消息實體的功能。特定的場景下,它當然可以是Kafka、RabbitMQ等消息中間件。但它的展現形式並不限於此,例如:

隊列可以是一張數據庫的表,發送者將消息寫入表,接收者從數據表裏讀消息。
如果一個程序把數據寫入Redis等內存Cache裏面,另一個程序從Cache裏面讀取,緩存在這裏就是一種分佈式隊列。
流式編程裏面的的數據流傳輸也是一種隊列。
典型的MVC(Model–view–controller)設計模式裏面,如果Model的變化需要導致View的變化,也可以通過隊列進行傳輸。這裏的分佈式隊列可以是數據庫,也可以是某臺服務器上的一塊內存。

五、抽象模型

最基礎的分佈式隊列編程抽象模型是點對點模型,其他抽象構架模型居於改基本模型上各角色的數量和交互變化所導致的不同拓撲圖。具體而言,不同數量的發送者、分佈式隊列以及接收者組合形成了不同的分佈式隊列編程模型。記住並理解典型的抽象模型結構對需求分析和建模而言至關重要,同時也會有助於學習和深入理解開源框架以及別人的代碼。

5.1、點對點模型(Point-to-point)

基礎模型中,只有一個發送者、一個接收者和一個分佈式隊列。如下圖所示:
這裏寫圖片描述

5.2、生產者消費者模型(Producer–consumer)

如果發送者和接收者都可以有多個部署實例,甚至不同的類型;但是共用同一個隊列,這就變成了標準的生產者消費者模型。在該模型,三個角色一般稱之爲生產者(Producer)、分佈式隊列(Queue)、消費者(Consumer)。

這裏寫圖片描述

5.3、發佈訂閱模型(PubSub)

如果只有一類發送者,發送者將產生的消息實體按照不同的主題(Topic)分發到不同的邏輯隊列。每種主題隊列對應於一類接收者。這就變成了典型的發佈訂閱模型。在該模型,三個角色一般稱之爲發佈者(Publisher),分佈式隊列(Queue),訂閱者(Subscriber)。
這裏寫圖片描述

5.4、MVC模型

如果發送者和接收者存在於同一個實體中,但是共享一個分佈式隊列。這就很像經典的MVC模型。
這裏寫圖片描述

六、編程模型

6.1、分佈式隊列模型編程和異步編程

分佈式隊列編程模型的通訊機制一般是採用異步機制,但是它並不等同於異步編程。
首先,並非所有的異步編程都需要引入隊列的概念,例如:大部分的操作系統異步I/O操作都是通過硬件中斷( Hardware Interrupts)來實現的。
其次,異步編程並不一定需要跨進程,所以其應用場景並不一定是分佈式環境。
最後,分佈式隊列編程模型強調發送者、接收者和分佈式隊列這三個角色共同組成的架構。這三種角色與異步編程沒有太多關聯。

6.2、分佈式隊列模式編程和流式編程

隨着Spark Streaming,Apache Storm等流式框架的廣泛應用,流式編程成了當前非常流行的編程模式。但是本文所闡述的分佈式隊列編程模型和流式編程並非同一概念。
首先,本文的隊列編程模式不依賴於任何框架,而流式編程是在具體的流式框架內的編程。
其次,分佈式隊列編程模型是一個需求解決方案,關注如何根據實際需求進行分佈式隊列編程建模。流式框架裏的數據流一般都通過隊列傳遞,不過,流式編程的關注點比較聚焦,它關注如何從流式框架裏獲取消息流,進行map、reduce、 join等轉型(Transformation)操作、生成新的數據流,最終進行彙總、統計。

七、分佈式隊列編程實戰篇

這裏所有的項目都是作者在新美大工作的真實案例。實戰篇的關注點是訓練建模思路,所以這些例子都按照挑戰、構思、架構三個步驟進行講解。受限於保密性要求,有些細節並未給出,但這些細節並不影響講解的完整性。另一方面,特別具體的需求容易讓人費解,爲了使講解更加順暢,作者也會採用一些更通俗易懂的例子。通過本篇的講解,希望和讀者一起去實踐“如何從需求出發去構架分佈式隊列編程模型”。

需要聲明的是,這裏的解決方案並不是所處場景的最優方案。但是,任何一個稍微複雜的問題,都沒有最優解決方案,更談不上唯一的解決方案。實際上,工程師每天所追尋的只是在滿足一定約束條件下的可行方案。當然不同的約束會導致不同的方案,約束的鬆弛度決定了工程師的可選方案的寬廣度。

7.1、信息採集處理

信息採集處理應用廣泛,例如:廣告計費、用戶行爲收集等。作者碰到的具體項目是爲廣告系統設計一套高可用的採集計費系統。
典型的廣告CPC、CPM計費原理是:收集用戶在客戶端或者網頁上的點擊和瀏覽行爲,按照點擊和瀏覽進行計費。計費業務有如下典型特徵:

採集者和處理者解耦,採集發生在客戶端,而計費發生在服務端。
計費與錢息息相關。
重複計費意味着災難。
計費是動態實時行爲,需要接受預算約束,如果消耗超過預算,則廣告投放需要停止。
用戶的瀏覽和點擊量非常大。

挑戰

計費業務的典型特徵給我們帶來了如下挑戰:

高吞吐量--廣告的瀏覽和點擊量非常巨大,我們需要設計一個高吞吐量的採集架構。
高可用性--計費信息的丟失意味着直接的金錢損失。任何處理服務器的崩潰不應該導致系統不可用。
高一致性要求--計費是一個實時動態處理過程,但要受到預算的約束。收集到的瀏覽和點擊行爲如果不能快速處理,可能會導致預算花超,或者點擊率預估不準確。所以採集到的信息應該在最短的時間內傳輸到計費中心進行計費。
完整性約束--這包括反作弊規則,單個用戶行爲不能重複計費等。這要求計費是一個集中行爲而非分佈式行爲。
持久化要求--計費信息需要持久化,避免因爲機器崩潰而導致收集到的數據產生丟失。

構思

採集的高可用性意味着我們需要多臺服務器同時採集,爲了避免單IDC故障,採集服務器需要部署在多IDC裏面。
實現一個高可用、高吞吐量、高一致性的信息傳遞系統顯然是一個挑戰,爲了控制項目開發成本,採用開源的消息中間件進行消息傳輸就成了必然選擇。
完整性約束要求集中進行計費,所以計費系統發生在覈心IDC。
計費服務並不關心採集點在哪裏,採集服務也並不關心誰進行計費。
根據以上構思,我們認爲採集計費符合典型的“生產者消費者模型”。

架構

採集計費系統架構圖如下:

  • 用戶點擊瀏覽收集服務(Click/View Collector)作爲生產者部署在多個機房裏,以提高收集服務可用性。
  • 每個機房裏採集到的數據通過消息隊列中間件發送到核心機房IDC_Master。
  • Billing服務作爲消費者部署在覈心機房集中計費。

這裏寫圖片描述

採用此架構,我們可以在如下方面做進一步優化:

  • 提高可擴展性,如果一個Billing部署實例在性能上無法滿足要求,可以對採集的數據進行主題分區(Topic
    Partition)計費,即採用發佈訂閱模式以提高可擴展性(Scalability)。
  • 全局排重和反作弊。採用集中計費架構解決了點擊瀏覽排重的問題,另一方面,這也給反作弊提供了全局信息。
  • 提高計費系統的可用性。採用下文單例服務優化策略,在保障計費系統集中性的同時,提高計費系統可用性

7.2、分佈式緩存更新(Distributed Cache Replacement)

緩存是一個非常寬泛的概念,幾乎存在於系統各個層級。典型的緩存訪問流程如下:

  • 接收到請求後,先讀取緩存,如果命中則返回結果。
  • 如果緩存不命中,讀取DB或其它持久層服務,更新緩存並返回結果

這裏寫圖片描述

對於已經存入緩存的數據,其更新時機和更新頻率是一個經典問題,即緩存更新機制(Cache Replacement Algorithms )。典型的緩存更新機制包括:近期最少使用算法(LRU)、最不經常使用算法(LFU)。這兩種緩存更新機制的典型實現是:啓動一個後臺進程,定期清理最近沒有使用的,或者在一段時間內最少使用的數據。由於存在緩存驅逐機制,當一個請求在沒有命中緩存時,業務層需要從持久層中獲取信息並更新緩存,提高一致性。

挑戰

分佈式緩存給緩存更新機制帶來了新的問題:

  • 數據一致性低。分佈式緩存中鍵值數量巨大,從而導致LRU或者LFU算法更新週期很長。在分佈式緩存中,拿LRU算法舉例,其典型做法是爲每個Key值設置一個生存時間(TTL),生存時間到期後將該鍵值從緩存中驅逐除去。考慮到分佈式緩存中龐大的鍵值數量,生存時間往往會設置的比較長,這就導致緩存和持久層數據不一致時間很長。如果生存時間設置過短,大量請求無法命中緩存被迫讀取持久層,系統響應時間會急劇惡化
  • 新數據不可用。在很多場景下,由於分佈式緩存和持久層的訪問性能相差太大,在緩存不命中的情況下,一些應用層服務不會嘗試讀取持久層,而直接返回空結果。漫長的緩存更新週期意味着新數據的可用性就被犧牲了。從統計的角度來講,新鍵值需要等待半個更新週期纔會可用。

    構思

根據上面的分析,分佈式緩存需要解決的問題是:在保證讀取性能的前提下,儘可能地提高老數據的一致性和新數據的可用性。如果仍然假定最近被訪問的鍵值最有可能被再次訪問(這是LRU或者LFU成立的前提),鍵值每次被訪問後觸發一次異步更新就是提高可用性和一致性最早的時機。無論是高性能要求還是業務解耦都要求緩存讀取和緩存更新分開,所以我們應該構建一個單獨的集中的緩存更新服務。集中進行緩存更新的另外一個好處來自於頻率控制。由於在一段時間內,很多類型訪問鍵值的數量滿足高斯分佈,短時間內重複對同一個鍵值進行更新Cache並不會帶來明顯的好處,甚至造成緩存性能的下降。通過控制同一鍵值的更新頻率可以大大緩解該問題,同時有利於提高整體數據的一致性,參見“排重優化”。

綜上所述,業務訪問方需要把請求鍵值快速傳輸給緩存更新方,它們之間不關心對方的業務。要快速、高性能地實現大量請求鍵值消息的傳輸,高性能分佈式消息中間件就是一個可選項。這三方一起組成了一個典型的分佈式隊列編程模型。

架構

如下圖,所有的業務請求方作爲生產者,在返回業務代碼處理之前將請求鍵值寫入高性能隊列。Cache Updater作爲消費者從隊列中讀取請求鍵值,將持久層中數據更新到緩存中。

這裏寫圖片描述

採用此架構,我們可以在如下方面做進一步優化:

  • 提高可擴展性,如果一個Cache Updater在性能上無法滿足要求,可以對鍵值進行主題分區(Topic
    Partition)進行並行緩存更新,即採用發佈訂閱模式以提高可擴展性(Scalability)。
  • 更新頻率控制。緩存更新都集中處理,對於發佈訂閱模式,同一類主題(Topic)的鍵值集中處理。Cache
    Updater可以控制對同一鍵值的在短期內的更新頻率(參見下文排重優化)。

7.3、後臺任務處理

典型的後臺任務處理應用包括工單處理火車票預訂系統機票選座等。我們所面對的問題是爲運營人員創建工單。一次可以爲多個運營人員創建多個工單。這個應用場景和火車票購買非常類似。工單相對來說更加抽象,所以,下文會結合火車票購買和運營人員工單分配這兩種場景同時講解。典型的工單創建要經歷兩個階段:數據篩選階段、工單創建階段。例如,在火車票預訂場景,數據篩選階段用戶選擇特定時間、特定類型的火車,而在工單創建階段,用戶下單購買火車票

挑戰

工單創建往往會面臨如下挑戰:

  • 數據一致性問題。以火車票預訂爲例,用戶篩選火車票和最終購買之間往往有一定的時延,意味着兩個操作之間數據是不一致的。在篩選階段,工程師們需決定是否進行車票鎖定,如果不鎖定,則無法保證出票成功。反之,如果在篩選地時候鎖定車票,則會大大降低系統效率和出票吞吐量。
  • 約束問題。工單創建需要滿足很多約束,主要包含兩種類型:動態約束,與操作者的操作行爲有關,例如購買幾張火車票的決定往往發生在篩選最後階段。隱性約束,這種約束很難通過界面進行展示,例如一個用戶購買了5張火車票,這些票應該是在同一個車廂的臨近位置
  • 優化問題。工單創建往往是約束下的優化,這是典型的統籌優化問題,而統籌優化往往需要比較長的時間。
  • 響應時間問題。對於多任務工單,一個請求意味着多個任務產生。這些任務的創建往往需要遵循事務性原則,即All or Nothing。在數據層面,這意味着工單之間需要滿足串行化需求(Serializability)。大數據量的串行化往往意味着鎖衝突延遲甚至失敗。無論是延遲機制所導致的長時延,還是高創建失敗率,都會大大傷害用戶體驗。

構思

如果將用戶篩選的最終規則做爲消息存儲下來,併發送給工單創建系統。此時,工單創建系統將具備創建工單所需的全局信息,具備在滿足各種約束的條件下進行統籌優化的能力。如果工單創建階段採用單實例部署,就可以避免數據鎖定問題,同時也意味着沒有鎖衝突,所以也不會有死鎖或任務延遲問題。
居於以上思路,在多工單處理系統的模型中,篩選階段的規則創建系統將充當生產者角色,工單創建系統將充當消費者角色,篩選規則將作爲消息在兩者之間進行傳遞。這就是典型的分佈式隊列編程架構。根據工單創建量的不同,可以採用數據庫或開源的分佈式消息中間件作爲分佈式隊列。

架構

該架構流程如下圖:

  • 用戶首選進行規則創建,這個過程主要是一些搜索篩選操作;
  • 用戶點擊工單創建,TicketRule Generator將把所有的篩選性組裝成規則消息併發送到隊列裏面去;
  • Ticket Generator作爲一個消費者,實時從隊列中讀取工單創建請求,開始真正創建工單。

這裏寫圖片描述

採用該架構,我們在數據鎖定、運籌優化、原子性問題都能得到比較好成果:

  • 數據鎖定推遲到工單創建階段,可以減少數據鎖定範圍,最大程度的降低工單創建對其他在線操作的影響範圍。
  • 如果需要進行統籌優化,可以將Ticket Generator以單例模式進行部署(參見單例服務優化)。這樣,Ticket
    Generator可以讀取一段時間內的工單請求,進行全局優化。例如,在我們的項目中,在某種條件下,運營人員需要滿足分級公平原則,即相同級別的運營人員的工單數量應該接近,不同級別的運營人員工單數量應該有所區分。如果不集中進行統籌優化,實現這種優化規則將會很困難。
  • 保障了約束完整性。例如,在我們的場景裏面,每個運營人員每天能夠處理的工單是有數量限制的,如果採用並行處理的方式,這種完整性約束將會很難實施

八、參考資料

[1] RabbitMQ, Highly Available Queues.
[2] IBM Knowledge Center, Introduction to message queuing.
[3] Wikipedia, Serializability.
[4] Hadoop, ZooKeeper Recipes and Solutions.
[5] Apache Kafka.
[6] Lamport L, Paxos Made Simple.

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