1. 程式人生 > >一篇文了解分散式佇列程式設計:從模型、實戰到優化

一篇文了解分散式佇列程式設計:從模型、實戰到優化

分散式佇列程式設計

本文由美團點評技術團隊出品,一篇文助你掌握分散式佇列程式設計的要義。從模型到實戰再到優化,基本涵蓋你可能踩的坑與其解決辦法。

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

使用分散式佇列的時候,沒有意識到它是佇列。

有具體需求的時候,忘記了分散式佇列的存在。

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

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

老司機介紹

劉丁,新美大廣告平臺CRM系統技術負責人,曾就職於Amazon、Tripadvisor。2014年加入美團,先後負責美團推薦系統、智慧篩選系統架構,作為技術負責人主導了美團廣告系統的開發和上線。目前致力於推進新美大廣告運營的標準化、自動化和智慧化。

新美大廣告平臺是美團、大眾點評雙平臺的營銷推廣平臺,幫助商戶推廣店鋪品牌及提升客流量。

1、模型篇

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

何時選擇分散式佇列

通訊是人們最基本的需求,同樣也是計算機最基本的需求。對於工程師而言,在程式設計和技術選型的時候,更容易進入大腦的概念是RPC、RESTful、Ajax、Kafka。在這些具體的概念後面,最本質的東西是“通訊”。

所以,大部分建模和架構都需要從“通訊”這個基本概念開始。當確定系統之間有通訊需求的時候,工程師們需要做很多的決策和平衡,這直接影響工程師們是否會選擇分散式佇列程式設計模型作為架構。從這個角度出發,影響建模的因素有四個:When、Who、Where、How。

When:同步VS非同步

通訊的一個基本問題是:發出去的訊息什麼時候需要被接收到?這個問題引出了兩個基礎概念:“同步通訊”和“非同步通訊”。根據理論抽象模型,同步通訊和非同步通訊最本質的差別來自於時鐘機制的有無。同步通訊的雙方需要一個校準的時鐘,非同步通訊的雙方不需要時鐘。

現實的情況是,沒有完全校準的時鐘,所以沒有絕對的同步通訊。同樣,絕對非同步通訊意味著無法控制一個發出去的訊息被接收到的時間點,無期限的等待一個訊息顯然毫無實際意義。

所以,實際程式設計中所有的通訊既不是“同步通訊”也不是“非同步通訊”;或者說,既是“同步通訊”也是“非同步通訊”。特別是對於應用層的通訊,其底層架構可能既包含“同步機制”也包含“非同步機制”。判斷“同步”和“非同步”訊息的標準問題太深,而不適合繼續展開。作者這裡給一些啟發式的建議:

發出去的訊息是否需要確認,如果不需要確認,更像是非同步通訊,這種通訊有時候也稱為單向通訊(One-WayCommunication)。

如果需要確認,可以根據需要確認的時間長短進行判斷。時間長的更像是非同步通訊,時間短的更像是同步通訊。當然時間長短的概念是純粹的主觀概念,不是客觀標準。

發出去的訊息是否阻塞下一個指令的執行,如果阻塞,更像是同步,否則,更像是非同步。

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

Who:傳送者接收者解耦

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

無論是傳送方還是接收方,只需要跟訊息中介軟體通訊,介面統一。統一意味著降低開發成本。

在不影響效能的前提下,同一套訊息中介軟體部署,可以被不同業務共享。共享意味著降低運維成本。

傳送方或者接收方單方面的部署拓撲的變化不影響對應的另一方。解藕意味著靈活和可擴充套件。

Where:訊息暫存機制

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

How:如何傳遞

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

可用性,如何保障通訊的高可用。

可靠性,如何保證訊息被可靠地傳遞。

持久化,如何保證訊息不會丟失。

吞吐量和響應時間。

跨平臺相容性。

除非工程師對造輪子有足夠的興趣,並且有充足的時間,採用一個滿足各項指標的分散式佇列程式設計模型就是一個簡單的選擇。

分散式佇列程式設計定義

很難給出分散式佇列程式設計模型的精確定義,由於本文偏重於應用,作者並不打算完全參照某個標準的模型。總體而言:分散式佇列程式設計模型包含三類角色:傳送者(Sender)、分散式佇列(Queue)、接收者(Receiver)。傳送者和接收者分別指的是生產訊息和接收訊息的應用程式或服務。

需要重點明確的概念是分散式佇列,它是提供以下功能的應用程式或服務:

接收“傳送者”產生的訊息實體;

傳輸、暫存該實體;

為“接收者”提供讀取該訊息實體的功能。

特定的場景下,它當然可以是Kafka、RabbitMQ等訊息中介軟體。但它的展現形式並不限於此,例如:

佇列可以是一張資料庫的表,傳送者將訊息寫入表,接收者從資料表裡讀訊息。

如果一個程式把資料寫入Redis等記憶體Cache裡面,另一個程式從Cache裡面讀取,快取在這裡就是一種分散式佇列。

流式程式設計裡面的的資料流傳輸也是一種佇列。

典型的MVC(Model–view–controller)設計模式裡面,如果Model的變化需要導致View的變化,也可以通過佇列進行傳輸。這裡的分散式佇列可以是資料庫,也可以是某臺伺服器上的一塊記憶體。

抽象模型

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

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

基礎模型中,只有一個傳送者、一個接收者和一個分散式佇列。如下圖所示:

分散式佇列

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

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

分散式佇列

釋出訂閱模型(PubSub)

如果只有一類傳送者,傳送者將產生的訊息實體按照不同的主題(Topic)分發到不同的邏輯佇列。每種主題佇列對應於一類接收者。這就變成了典型的釋出訂閱模型。在該模型,三個角色一般稱為釋出者(Publisher),分散式佇列(Queue),訂閱者(Subscriber)。

釋出訂閱模型PubSub

MVC模型

如果傳送者和接收者存在於同一個實體中,但是共享一個分散式佇列。這就很像經典的MVC模型。

MVC模型

程式設計模型

為了讓讀者更好地理解分散式佇列程式設計模式概念,這裡將其與一些容易混淆的概念做一些對比 。

分散式佇列模型程式設計和非同步程式設計

分散式佇列程式設計模型的通訊機制一般是採用非同步機制,但是它並不等同於非同步程式設計。

首先,並非所有的非同步程式設計都需要引入佇列的概念,例如:大部分的作業系統非同步I/O操作都是通過硬體中斷( Hardware Interrupts)來實現的。

其次,非同步程式設計並不一定需要跨程序,所以其應用場景並不一定是分散式環境。

最後,分散式佇列程式設計模型強調發送者、接收者和分散式佇列這三個角色共同組成的架構。這三種角色與非同步程式設計沒有太多關聯。

分散式佇列模式程式設計和流式程式設計

隨著Spark Streaming,Apache Storm等流式框架的廣泛應用,流式程式設計成了當前非常流行的程式設計模式。但是本文所闡述的分散式佇列程式設計模型和流式程式設計並非同一概念。

首先,本文的佇列程式設計模式不依賴於任何框架,而流式程式設計是在具體的流式框架內的程式設計。

其次,分散式佇列程式設計模型是一個需求解決方案,關注如何根據實際需求進行分散式佇列程式設計建模。流式框架裡的資料流一般都通過佇列傳遞,不過,流式程式設計的關注點比較聚焦,它關注如何從流式框架裡獲取訊息流,進行map、reduce、 join等轉型(Transformation)操作、生成新的資料流,最終進行彙總、統計。

2、實戰篇

這裡所有的專案都是作者在新美大工作的真實案例。實戰篇的關注點是訓練建模思路,所以這些例子都按照挑戰、構思、架構三個步驟進行講解。受限於保密性要求,有些細節並未給出,但這些細節並不影響講解的完整性。

另一方面,特別具體的需求容易讓人費解,為了使講解更加順暢,作者也會採用一些更通俗易懂的例子。通過本篇的講解,希望和讀者一起去實踐“如何從需求出發去構架分散式佇列程式設計模型”。

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

資訊採集處理

資訊採集處理應用廣泛,例如:廣告計費、使用者行為收集等。作者碰到的具體專案是為廣告系統設計一套高可用的採集計費系統。

典型的廣告CPC、CPM計費原理是:收集使用者在客戶端或者網頁上的點選和瀏覽行為,按照點選和瀏覽進行計費。計費業務有如下典型特徵:

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

挑戰

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

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

構思

採集的高可用性意味著我們需要多臺伺服器同時採集,為了避免單IDC故障,採集伺服器需要部署在多IDC裡面。

實現一個高可用、高吞吐量、高一致性的資訊傳遞系統顯然是一個挑戰,為了控制專案開發成本,採用開源的訊息中介軟體進行訊息傳輸就成了必然選擇。

完整性約束要求集中進行計費,所以計費系統發生在核心IDC。

計費服務並不關心採集點在哪裡,採集服務也並不關心誰進行計費。

根據以上構思,我們認為採集計費符合典型的“生產者消費者模型”。

架構

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

  • 使用者點選瀏覽收集服務(Click/View Collector)作為生產者部署在多個機房裡,以提高收集服務可用性。
  • 每個機房裡採集到的資料通過訊息佇列中介軟體傳送到核心機房IDC_Master。
  • Billing服務作為消費者部署在核心機房集中計費。

架構

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

  • 提高可擴充套件性,如果一個Billing部署例項在效能上無法滿足要求,可以對採集的資料進行主題分割槽(Topic Partition)計費,即採用釋出訂閱模式以提高可擴充套件性(Scalability)。
  • 全域性排重和反作弊。採用集中計費架構解決了點選瀏覽排重的問題,另一方面,這也給反作弊提供了全域性資訊。
  • 提高計費系統的可用性。採用下文單例服務優化策略,在保障計費系統集中性的同時,提高計費系統可用性。

分散式快取更新(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可以控制對同一鍵值的在短期內的更新頻率(參見下文排重優化)。

後臺任務處理

典型的後臺任務處理應用包括工單處理、火車票預訂系統、機票選座等。我們所面對的問題是為運營人員建立工單。一次可以為多個運營人員建立多個工單。這個應用場景和火車票購買非常類似。工單相對來說更加抽象,所以,下文會結合火車票購買和運營人員工單分配這兩種場景同時講解。

典型的工單建立要經歷兩個階段:資料篩選階段、工單建立階段。例如,在火車票預訂場景,資料篩選階段使用者選擇特定時間、特定型別的火車,而在工單建立階段,使用者下單購買火車票。

挑戰

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

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

構思

如果將使用者篩選的最終規則做為訊息儲存下來,併發送給工單建立系統。此時,工單建立系統將具備建立工單所需的全域性資訊,具備在滿足各種約束的條件下進行統籌優化的能力。如果工單建立階段採用單例項部署,就可以避免資料鎖定問題,同時也意味著沒有鎖衝突,所以也不會有死鎖或任務延遲問題。

居於以上思路,在多工單處理系統的模型中,篩選階段的規則建立系統將充當生產者角色,工單建立系統將充當消費者角色,篩選規則將作為訊息在兩者之間進行傳遞。這就是典型的分散式佇列程式設計架構。根據工單建立量的不同,可以採用資料庫或開源的分散式訊息中介軟體作為分散式佇列。

架構

該架構流程如下圖:

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

9

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

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

3.優化篇

接下來重點闡述工程師運用分散式佇列程式設計構架的時候,在生產者、分散式佇列以及消費者這三個環節的注意點以及優化建議。

確定採用分散式佇列程式設計模型之後,主體架構就算完成了,但工程師的工作還遠遠未結束。天下事必做於細,細節是一個不錯的架構向一個優秀的系統進階的關鍵因素。優化篇選取了作者以及其同事在運用分散式佇列程式設計模型架構時所碰到的典型問題和解決方案。

這裡些問題出現的頻率較高,如果你經驗不夠,很可能會“踩坑”。希望通過這些講解,幫助讀者降低分散式佇列程式設計模型的使用門檻。本文將對分散式佇列程式設計模型的三種角色:生產者(Producer),分散式佇列(Queue),消費者(Consumer)分別進行優化討論。

生產者優化

在分散式佇列程式設計中,生產者往往並非真正的生產源頭,只是整個資料流中的一個節點,這種生產者的操作是處理-轉發(Process-Forward)模式。

這種模式給工程師們帶來的第一個問題是吞吐量問題。這種模式下執行的生產者,一邊接收上游的資料,一邊將處理完的資料傳送給下游。本質上,它是一個非常經典的數學問題,其抽象模型是一些沒有蓋子的水箱,每個水箱接收來自上一個水箱的水,進行處理之後,再將水傳送到下一個水箱。

工程師需要預測水源的流量、每個環節水箱的處理能力、水龍頭的排水速度,最終目的是避免水溢位水箱,或者儘可能地減小溢位事件的概率。實際上流式程式設計框架以及其開發者花了大量的精力去處理和優化這個問題。下文的快取優化和批量寫入優化都是針對該問題的解決方案。

第二個需要考慮的問題是持久化。由於各種原因,系統總是會宕機。如果資訊比較敏感,例如計費資訊、火車票訂單資訊等,工程師們需要考慮系統宕機所帶來的損失,找到讓損失最小化的解決方案。持久化優化重點解決這一類問題。

快取優化

處於“處理-轉發”模式下執行的生產者往往被設計成請求驅動型的服務,即每個請求都會觸發一個處理執行緒,執行緒處理完後將結果寫入分散式佇列。如果由於某種原因佇列服務不可用,或者效能惡化,隨著新請求的到來,生產者的處理執行緒就會產生堆積。這可能會導致如下兩個問題:

  • 系統可用性降低。由於每個執行緒都需要一定的記憶體開銷,執行緒過多會使系統記憶體耗盡,甚至可能產生雪崩效應導致最終完全不可用。
  • 資訊丟失。為了避免系統崩潰,工程師可能會給請求驅動型服務設定一個處理執行緒池,設定最大處理執行緒數量。這是一種典型的降級策略,目的是為了系統崩潰。但是,後續的請求會因為沒有處理執行緒而被迫阻塞,最終可能產生資訊丟失。例如:對於廣告計費採集,如果採集系統因為執行緒耗盡而不接收客戶端的計費行為,這些計費行為就會丟失。

緩解這類問題的思路來自於CAP理論,即通過降低一致性來提高可用性。生產者接收執行緒在收到請求之後第一時間不去處理,直接將請求快取在記憶體中(犧牲一致性),而在後臺啟動多個處理執行緒從快取中讀取請求、進行處理並寫入分散式佇列。

與執行緒所佔用的記憶體開銷相比,大部分的請求所佔記憶體幾乎可以忽略。通過在接收請求和處理請求之間增加一層記憶體快取,可以大大提高系統的處理吞吐量和可擴充套件性。這個方案本質上是一個記憶體生產者消費者模型。

批量寫入優化

如果生產者的請求過大,寫分散式佇列可能成為效能瓶頸,有如下幾個因素:

  • 佇列自身效能不高。
  • 分散式佇列程式設計模型往往被應用在跨機房的系統裡面,跨機房的網路開銷往往容易成為系統瓶頸。
  • 訊息確認機制往往會大大降低佇列的吞吐量以及響應時間。

如果在處理請求和寫佇列之間新增一層快取,訊息寫入程式批量將訊息寫入佇列,可以大大提高系統的吞吐量。原因如下:

  • 批量寫佇列可以大大減少生產者和分散式佇列的互動次數和訊息傳輸量。特別是對於高吞吐小載荷的訊息實體,批量寫可以顯著降低網路傳輸量。
  • 對於需要確認機制的訊息,確認機制往往會大大降低佇列的吞吐量以及響應時間,某些高敏感的訊息需要多個訊息中介軟體代理同時確認,這近一步惡化效能。在生產者的應用層將多條訊息批量組合成一個訊息體,訊息中介軟體就只需要對批量訊息進行一次確認,這可能會數量級的提高訊息傳輸效能。

持久化優化

通過新增快取,消費者服務的吞吐量和可用性都得到了提升。但快取引入了一個新問題——記憶體資料丟失。對於敏感資料,工程師需要考慮如下兩個潛在問題:

  • 如果記憶體中存在未處理完的請求,而某些原因導致生產者服務宕機,記憶體資料就會丟失而可能無法恢復。
  • 如果分散式佇列長時間不可用,隨著請求數量的不斷增加,最終系統記憶體可能會耗盡而崩潰,記憶體的訊息也可能丟失。

所以快取中的資料需要定期被持久化到磁碟等持久層裝置中,典型的持久化觸發策略主要有兩種:

  • 定期觸發,即每隔一段時間進行一次持久化。
  • 定量觸發,即每當快取中的請求數量達到一定閾值後進行持久化。
  • 是否需要持久化優化,以及持久化策略應該由請求資料的敏感度、請求量、持久化效能等因素共同決定。

中介軟體選型

分散式佇列不等同於各種開源的或者收費的訊息中介軟體,甚至在一些場景下完全不需要使用訊息中介軟體。但是,訊息中介軟體產生的目的就是解決訊息傳遞問題,這為分散式佇列程式設計架構提供了很多的便利。在實際工作中,工程師們應該將成熟的訊息中介軟體作為佇列的首要備選方案。

本節對訊息中介軟體的功能、模型進行闡述,並給出一些訊息中介軟體選型、部署的具體建議。

中介軟體的功能

明白一個系統的每個具體功能是設計和架構一個系統的基礎。典型的訊息中介軟體主要包含如下幾個功能:

  • 訊息接收
  • 訊息分發
  • 訊息儲存
  • 訊息讀取

概念模型

抽象的訊息中介軟體模型包含如下幾個角色:

  • 傳送者和接收者客戶端(Sender/Receiver Client),在具體實施過程中,它們一般以庫的形式嵌入到應用程式程式碼中。
  • 代理伺服器(Broker Server),它們是與客戶端程式碼直接互動的服務端程式碼。
  • 訊息交換機(Exchanger),接收到的訊息一般需要通過訊息交換機(Exchanger)分發到具體的訊息佇列中。
  • 訊息佇列,一般是一塊記憶體資料結構或持久化資料。
  • 概念模型如下圖:

10

  • 為了提高分發效能,很多訊息中介軟體把訊息代理伺服器的拓撲圖傳送到傳送者和接收者客戶端(Sender/Receiver Client),如此一來,傳送源可以直接進行訊息分發。

選型標準

要完整的描述訊息中介軟體各個方面非常困難,大部分良好的訊息中介軟體都有完善的文件,這些文件的長度遠遠超過本文的總長度。但如下幾個標準是工程師們在進行訊息中介軟體選型時經常需要考慮和權衡的。

效能

效能主要有兩個方面需要考慮:吞吐量(Throughput)和響應時間(Latency)。

不同的訊息佇列中介軟體的吞吐量和響應時間相差甚遠,在選型時可以去網上檢視一些效能對比報告。

對於同一種中介軟體,不同的配置方式也會影響效能。主要有如下幾方面的配置:

  • 是否需要確認機制,即寫入佇列後,或從佇列讀取後,是否需要進行確認。確認機制對響應時間的影響往往很大。
  • 能否批處理,即訊息能否批量讀取或者寫入。批量操作可以大大減少應用程式與訊息中介軟體的互動次數和訊息傳遞量,大大提高吞吐量。
  • 能否進行分割槽(Partition)。將某一主題訊息佇列進行分割槽,同一主題訊息可以有多臺機器並行處理。這不僅僅能影響訊息中介軟體的吞吐量,還決定著訊息中介軟體是否具備良好的可伸縮性(Scalability)。
  • 是否需要進行持久化。將訊息進行持久化往往會同時影響吞吐量和響應時間。
可靠性

可靠性主要包含:可用性、持久化、確認機制等。

高可用性的訊息中介軟體應該具備如下特徵:

  • 訊息中介軟體代理伺服器(Broker)具有主從備份。即當一臺代理服務宕機之後,備用伺服器能接管相關的服務。
  • 訊息中介軟體中快取的訊息是否有備份、並持久化。
  • 根據CAP理論,高可用、高一致性以及網路分裂不可兼得。根據作者的觀察,大部分的訊息中介軟體在面臨網路分裂的情況下下,都很難保證資料的一致性以及可用性。 很多訊息中介軟體都會提供一些可配置策略,讓使用者在可用性和一致性之間做權衡。

高可靠的訊息中介軟體應該確保從傳送者接收到的訊息不會丟失。中介軟體代理伺服器的宕機並不是小概率事件,所以儲存在記憶體中的訊息很容易發生丟失。大部分的訊息中介軟體都依賴於訊息的持久化去降低訊息丟失損失,即將接收到的訊息寫入磁碟。即使提供持久化,仍有兩個問題需要考慮:

  • 磁碟損壞問題。長時間來看,磁碟出問題的概率仍然存在。
  • 效能問題。與操作記憶體相比,磁碟I/O的操作效能要慢幾個數量級。頻繁持久化不僅會增加響應時間,也會降低吞吐量。
  • 解決這兩個問題的一個解決方案就是:多機確認,定期持久化。即訊息被快取在多臺機器的記憶體中,只有每臺機器都確認收到訊息,才跟傳送者確認(很多訊息中介軟體都會提供相應的配置選項,讓使用者設定最少需要多少臺機器接收到訊息)。由於多臺獨立機器同時出故障的概率遵循乘法法則,指數級降低,這會大大提高訊息中介軟體的可靠性。

確認機制本質上是通訊的握手機制(Handshaking)。如果沒有該機制,訊息在傳輸過程中丟失將不會被發現。高敏感的訊息要求選取具備確認機制的訊息中介軟體。當然如果沒有接收到訊息中介軟體確認完成的指令,應用程式需要決定如何處理。典型的做法有兩個:

  • 多次重試。
  • 暫存到本地磁碟或其它持久化媒介。
客戶端介面所支援語言

採用現存訊息中介軟體就意味著避免重複造輪子。如果某個訊息中介軟體未能提供對應語言的客戶端介面,則意味著極大的成本和相容性問題。

投遞策略(Delivery policies)

投遞策略指的是一個訊息會被髮送幾次。主要包含三種策略:最多一次(At most Once )、最少一次(At least Once)、僅有一次(Exactly Once)。

在實際應用中,只考慮訊息中介軟體的投遞策略並不能保證業務的投遞策略,因為接收者在確認收到訊息和處理完訊息並持久化之間存在一個時間視窗。例如,即使訊息中介軟體保證僅有一次(Exactly Once),如果接收者先確認訊息,在持久化之前宕機,則該訊息並未被處理。

從應用的角度,這就是最多一次(At most Once)。反之,接收者先處理訊息並完成持久化,但在確認之前宕機,訊息就要被再次傳送,這就是最少一次(At least Once)。 如果訊息投遞策略非常重要,應用程式自身也需要仔細設計。

消費者優化

消費者是分散式佇列程式設計中真正的資料處理方,資料處理方最常見的挑戰包括:有序性、序列化(Serializability)、頻次控制、完整性和一致性等。

挑戰

有序性

在很多場景下,如何保證佇列資訊的有序處理是一個棘手的問題。如下圖,假定分散式佇列保證請求嚴格有序,請求ri2和ri1都是針對同一資料記錄的不同狀態,ri2的狀態比ri1的狀態新。T1、T2、T3和T4代表各個操作發生的時間,並且 T1 < T2 < T3 < T4(”<“代表早於)。

採用多消費者架構,這兩條記錄被兩個消費者(Consumer1和Consumer2)處理後更新到資料庫裡面。Consumer1雖然先讀取ri1但是卻後寫入資料庫,這就導致,新的狀態被老的狀態覆蓋,所以多消費者不保證資料的有序性。

11

序列化

很多場景下,序列化是資料處理的一個基本需求,這是保證資料完整性、可恢復性、事務原子性等的基礎。為了在平行計算系統裡實現序列化,一系列的相關理論和實踐演算法被提出。對於分散式佇列程式設計架構,要在在多臺消費者實現序列化非常複雜,無異於重複造輪子。

頻次控制

有時候,消費者的消費頻次需要被控制,可能的原因包括:

  • 費用問題。如果每次消費所引起的操作都需要收費,而同一個請求訊息在佇列中儲存多份,不進行頻次控制,就會導致無謂的浪費。
  • 效能問題。每次消費可能會引起對其他服務的呼叫,被呼叫服務希望對呼叫量有所控制,對同一個請求訊息的多次訪問就需要有所控制。
完整性和一致性

完整性和一致性是所有多執行緒和多程序的程式碼都面臨的問題。在多執行緒或者多程序的系統中考慮完整性和一致性往往會大大地增加程式碼的複雜度和系統出錯的概率。

單例服務優化

幾乎所有序列化理論真正解決的問題只有一個:效能。 所以,在效能允許的前提下,對於消費者角色,建議採用單例項部署。通過單例項部署,有序性、序列化、完整性和一致性問題自動獲得瞭解決。另外,單例項部署的消費者擁有全部所需資訊,它可以在頻次控制上採取很多優化策略。

天下沒有免費的午餐。同樣,單例項部署並非沒有代價,它意味著系統可用性的降低,很多時候,這是無法接受的。解決可用性問題的最直接的思路就是冗餘(Redundancy)。最常用的冗餘方案是Master-slave架構,不過大部分的Master-slave架構都是Active/active模式,即主從伺服器都提供服務。

例如,資料庫的Master-slave架構就是主從伺服器都提供讀服務,只有主伺服器提供寫服務。大部分基於負載均衡設計的Master-slave叢集中,主伺服器和從伺服器同時提供相同的服務。這顯然不滿足單例服務優化需求。

有序性和序列化需要Active/passive架構,即在某一時刻只有主例項提供服務,其他的從服務等待主例項失效。這是典型的領導人選舉架構,即只有獲得領導權的例項才能充當實際消費者,其他例項都在等待下一次選舉。採用領導人選舉的Active/passive架構可以大大緩解純粹的單例項部署所帶來的可用性問題。

令人遺憾的是,除非工程師們自己在消費者例項裡面實現Paxos等演算法,並在每次訊息處理之前都執行領導人選舉。否則,理論上講,沒有方法可以保障在同一個時刻只有一個領導者。而對每個訊息都執行一次領導人選舉,顯然效能不可行。

實際工作中,最容易出現的問題時機發生在領導人交接過程中,即前任領導人例項變成輔助例項,新部署例項開始承擔領導人角色。為了平穩過渡,這兩者之間需要有一定的通訊機制,但是,無論是網路分割槽(Network partition)還是原領導人服務崩潰都會使這種通訊機制變的不可能。

對於完整性和一致性要求很高的系統,我們需要在選舉制度和交接制度這兩塊進行優化。

領導人選舉架構

典型的領導人選舉演算法有Paxos、ZAB( ZooKeeper Atomic Broadcast protocol)。為了避免重複造輪子,建議採用ZooKeeper的分散式鎖來實現領導人選舉。典型的ZooKeeper實現演算法如下:

Let ELECTION be a path of choice of the application. To volunteer to be a leader:

1.Create znode z with path “ELECTION/guid-n_” with both SEQUENCE and EPHEMERAL flags;

2.Let C be the children of “ELECTION”, and i be the sequence number of z;

3.Watch for changes on “ELECTION/guid-n_j”, where j is the largest sequence number such that j < i and n_j is a znode in C;

Upon receiving a notification of znode deletion:

1.Let C be the new set of children of ELECTION;

2.If z is the smallest node in C, then execute leader procedure;

3.Otherwise, watch for changes on “ELECTION/guid-n_j”, where j is the largest sequence number such that j < i and n_j is a znode in C;

領導人交接架構

領導人選舉的整個過程發生在ZooKeeper叢集中,各個消費者例項在這場選舉中只充當被告知者角色(Learner)。領導人選舉演算法,只能保證最終只有一個Leader被選舉出來,並不保障被告知者對Leader的理解是完全一致的。

本質上,上文的架構裡,選舉的結果是作為令牌(Token)傳遞給消費者例項,消費者將自身的ID與令牌進行對比,如果相等,則開始執行消費操作。所以當發生領導人換屆的情況,不同的Learner獲知新Leader的時間並不同。

例如,前任Leader如果因為網路問題與ZooKeeper叢集斷開,前任Leader只能在超時後才能判斷自己是否不再承擔Leader角色了,而新的Leader可能在這之前已經產生。另一方面,即使前任Leader和新Leader同時接收到新Leader選舉結果,某些業務的完整性要求迫使前任Leader仍然完成當前未完成的工作。

以上的講解非常抽象,生活中卻給了一些更加具體的例子。眾所周知,美國總統候選人在選舉結束後並不直接擔任美國總統,從選舉到最終承擔總統角色需要一個過渡期。對於新當選Leader的候選人而言,過渡期間稱之為加冕階段(Inauguration)。對於即將卸任的Leader,過渡期稱為交接階段(HandOver)。

所以一個基於領導人選舉的消費者從加冕到卸任經歷三個階段:Inauguration、Execution、HandOver。在加冕階段,新領導需要進行一些初始化操作。Execution階段是真正的佇列訊息處理階段。在交接階段,前任領導需要進行一些清理操作。

類似的,為了解決領導人交接問題,所有的消費者從程式碼實現的角度都需要實現類似ILeaderCareer介面。這個介面包含三個方發inaugurate(),handOver()和execute()。某個部署例項(Learner)在得知自己承擔領導人角色後,需要呼叫inaugurate()方法,進行加冕。主要的消費邏輯通過不停的執行execute()實現,當確認自己不再承擔領導人之後,執行handOver()進行交接。

12

如果承擔領導人角色的消費者,在執行execute()階段得知自己將要下臺,根據訊息處理的原子性,該領導人可以決定是否提前終止操作。如果整個訊息處理是一個原子性事務,直接終止該操作可以快速實現領導人換屆。否則,前任領導必須完成當前訊息處理後,才進入交接階段。這意味著新的領導人,在inaugurate()階段需要進行一定時間的等待。

排重優化

頻次控制是一個經典問題。對於分散式佇列程式設計架構,相同請求重複出現在佇列的情況並不少見。如果相同請求在佇列中重複太多,排重優化就顯得很必要。分散式快取更新是一個典型例子,所有請求都被髮送到佇列中用於快取更新。如果請求符合典型的高斯分佈,在一段時間內會出現大量重複的請求,而同時多執行緒更新同一請求快取顯然沒有太大的意義。

排重優化是一個演算法,其本質是基於狀態機的程式設計,整個講解通過模型、構思和實施三個步驟完成。

模型

進行排重優化的前提是大量重複的請求。在模型這一小節,我們首先闡述重複度模型、以及不同重複度所導致的消費模型,最後基於這兩個模型去講解排重狀態機。

重複度模型

首先我們給出最小重複長度的概念。同一請求最小重複長度:同一請求在佇列中的重複出現的最小間距。例如,請求ri第一次出現在位置3,第二次出現在10,最小重複長度等於7。

是否需要進行排重優化取決於佇列中請求的重複度。由於不同請求之間並不存在重複的問題,不失一般性,這裡的模型只考了單個請求的重複度,重複度分為三個類:無重複、稀疏重複、高重複。

  • 無重複:在整個請求過程,沒有任何一個請求出現一次以上。
  • 稀疏重複:主要的請求最小重複長度大於消費佇列長度。
  • 高重複:大量請求最小重複長度小於消費佇列長度。

對於不同的重複度,會有不同的消費模型。

無重複消費模型

在整個佇列處理過程中,所有的請求都不相同,如下圖:

13

稀疏重複消費模型

當同一請求最小重複長度大於消費者佇列長度,如下圖。假定有3個消費者,Consumer1將會處理r1,Consumer2將會處理r2,Consumer3將會處理r3,如果每個請求處理的時間嚴格相等,Consumer1在處理完r1之後,接著處理r4,Consumer2將會處理r2之後會處理r1。雖然r1被再次處理,但是任何時刻,只有這一個消費者在處理r1,不會出現多個消費者同時處理同一請求的場景。

架構

高重複消費模型

如下圖,仍然假定有3個消費者,佇列中前面4個請求都是r1,它會同時被3個消費者執行緒處理:

高重複消費模型

顯然,對於無重複和稀疏重複的分散式佇列,排重優化並不會帶來額外的好處。排重優化所針對的物件是高重複消費模型,特別是對於並行處理消費者比較多的情況,重複處理同一請求,資源消耗極大。

排重狀態機

排重優化的主要物件是高重複的佇列,多個消費者執行緒或程序同時處理同一個冪等請求只會浪費計算資源並延遲其他待請求處理。所以,排重狀態機的一個目標是處理唯一性,即:同一時刻,同一個請求只有一個消費者處理。

如果消費者獲取一條請求訊息,但發現其他消費者正在處理該訊息,則當前消費者應該處於等待狀態。如果對同一請求,有一個消費者在處理,一個消費者在等待,而同一請求再次被消費者讀取,再次等待則沒有意義。

所以,狀態機的第二個目標是等待唯一性,即:同一時刻,同一個請求最多隻有一個消費者處於等待狀態。總上述,狀態機的目標是:處理唯一性和等待唯一性。我們把正在處理的請求稱為頭部請求,正在等待的請求稱為尾部請求。

由於狀態機的處理單元是請求,所以需要針對每一個請求建立一個排重狀態機。基於以上要求,我們設計的排重狀態機包含4個狀態Init,Process,Block,Decline。各個狀態之間轉化過程如下圖:

排重狀態機

  1. 狀態機建立時處於Init狀態。
  2. 對Init狀態進行Enqueue操作,即接收一個請求,開始處理(稱為頭部請求),狀態機進入Process狀態。
  3. 狀態機處於Process狀態,表明當前有消費者正在處理頭部請求。此時,如果進行Dequeue操作,即頭部請求處理完成,返回Init狀態。如果進行Enqueue操作,即另一個消費者準備處理同一個請求,狀態機進入Block狀態(該請求稱為尾部請求)。
  4. 狀態機處於Block狀態,表明頭部請求正在處理,尾部請求處於阻塞狀態。此時,進行Dequeue操作,即頭部請求處理完成,返回Process狀態,並且尾部請求變成頭部請求,原尾部請求消費者結束阻塞狀態,開始處理。進行Enqueue操作,表明一個新的消費者準備處理同一個請求,狀態機進入Decline狀態。
  5. 狀態機進入Decline狀態,根據等待唯一性目標,處理最新請求的消費者將被拋棄該訊息,狀態機自動轉換回Block狀態。

構思

狀態機描述的是針對單個請求操作所引起狀態變化,排重優化需要解決佇列中所有請求的排重問題,需要對所有請求的狀態機進行管理。這裡只考慮單虛擬機器內部對所有請求狀態機的管理,對於跨虛擬機器的管理可以採用類似的方法。對於多狀態機管理主要包含三個方面:一致性問題、完整性問題和請求快取驅逐問題。

一致性問題

一致性在這裡要求同一請求的不同消費者只會操作一個狀態機。由於每個請求都產生一個狀態機,系統將會包含大量的狀態機。為了兼顧效能和一致性,我們採用ConcurrentHashMap儲存所有的狀態機。用ConcurrentHashMap而不是對整個狀態機佇列進行加鎖,可以提高並行處理能力,使得系統可以同時操作不同狀態機。

為了避免處理同一請求的多消費者執行緒同時對ConcurrentHashMap進行插入所導致狀態機不一致問題,我們利用了ConcurrentHashMap的putIfAbsent()方法。程式碼方案如下,key2Status用於儲存所有的狀態機。

消費者在處理請求之前,從狀態機佇列中讀取排重狀態機TrafficAutomate。如果沒有找到,則建立一個新的狀態機,並通過putIfAbsent()方法插入到狀態機佇列中。

排重狀態機

完整性問題

完整性要求保障狀態機Init,Process,Block,Decline四種狀態正確、狀態之間的轉換也正確。由於狀態機的操作非常輕量級,兼顧完整性和降低程式碼複雜度,我們對狀態機的所有方法進行加鎖。

請求快取驅逐問題(Cache Eviction)

如果不同請求的數量太多,記憶體永久儲存所有請求的狀態機的記憶體開銷太大。所以,某些狀態機需要在恰當的時候被驅逐出記憶體。這裡有兩個思路:

  • 當狀態機返回Init狀態時,清除出佇列。
  • 啟動一個後臺執行緒,定時掃描狀態機佇列,採用LRU等標準快取清除機制。
標識問題

每個請求對應於一個狀態機,不同的狀態機採用不同的請求進行識別。

對於同一狀態機的不同消費者,在單虛擬機器方案中,我們採用執行緒id進行標識。

實施

排重優化的主要功能都是通過排重狀態機(TrafficAutomate)和狀態機佇列(QueueCoordinator)來實施的。排重狀態機描述的是針對單個請求的排重問題,狀態機佇列解決所有請求狀態機的排重問題。

狀態機實施(TrafficAutomate)

根據狀態機模型,其主要操作為enQueue和deQueue,其狀態由頭部請求和尾部請求的狀態共同決定,所以需要定義兩個變數為head和tail,用於表示頭部請求和尾部請求。為了確保多執行緒操作下狀態機的完整性(Integraty),所有的操作都將加上鎖。

enQueue操作

當一個消費者執行enQueue操作時:如果此時尾部請求不為空,根據等待唯一性要求,返回DECLINE,當前消費者應該拋棄該請求;如果頭部請求為空,返回ACCPET,當前消費者應該立刻處理該訊息;否則,返回BLOCK,該消費者應該等待,並不停的檢視狀態機的狀態,一直到頭部請求處理完成。enQueue程式碼如下:

18

deQueue操作

對於deQueue操作,首先將尾部請求賦值給頭部請求,並將尾部請求置為無效。deQueue程式碼如下:

19

狀態機佇列實施(QueueCoordinator)
介面定義

狀態機佇列集中管理所有請求的排重狀態機,所以其操作和單個狀態機一樣,即enQueue和deQueuqe介面。這兩個介面的實現需要識別特定請求的狀態機,所以它們的入參應該是請求。為了相容不同型別的請求訊息,我們採用了Java泛型程式設計。介面定義如下:

20

enQueue操作

enQueue操作過程如下:

首先,根據傳入的請求key值,獲取狀態機, 如果不存在則建立一個新的狀態機,並儲存在ConcurrentHashMap中。

接下來,獲取執行緒id作為該消費者的唯一標識,並對對應狀態機進行enQueue操作。

如果狀態機返回值為ACCEPT或者DECLINE,返回業務層處理程式碼,ACCEPT意味著業務層需要處理該訊息,DECLINE表示業務層可以拋棄當前訊息。如果狀態機返回值為Block,則該執行緒保持等待狀態。

在某些情況下,頭部請求執行緒可能由於異常,未能對狀態機進行deQueue操作(作為元件提供方,不能假定所有的規範被使用方實施)。為了避免處於阻塞狀態的消費者無期限地等待,建議對狀態機設定安全超時時限。超過了一定時間後,狀態機強制清空頭部請求,返回到業務層,業務層開始處理該請求。

程式碼如下:

21

deQueue操作

deQueue操作首先從ConcurrentHashMap獲取改請求所對應的狀態機,接著獲取該執行緒的執行緒id,對狀態機進行deQueue操作。

enQueue程式碼如下:

22

原始碼

完整原始碼可以在QueueCoordinator獲取。連結:

https://github.com/dinglau2008/QueueCoordinator/tree/master/src

文章出處:美團點評技術團隊