Cherami:Uber Engineering的可持續和可擴展的任務隊列

分類:技術 時間:2017-01-13

Cherami是一個分布式、可擴展、可持續和高可用性的消息隊列系統,我們在 Uber Engineering 開發并用于傳輸異步任務。我們將這個任務隊列系統,以一只英雄傳信鴿的名字命名,希望這個系統具備同樣的彈性和容錯能力,允許Uber的任務關鍵業務邏輯組件依賴于它的消息傳遞。

Cher Ami第一次世界大戰中的美國陸軍傳信鴿 。盡管她腿部中彈,但依然成功傳遞了報信, 拯救了194條生命。

引言

允許分布式系統中的一個任務隊列解耦組件以異步方式進行通信。通信雙方可以單獨擴展,以增加負載平滑或節流的功能。在復雜的分布式系統中,任務隊列必不可少。在Uber的基礎設施生態系統中,Cherami充當簡單隊列服務( Simple Queue Service,SQS )的角色。將我們自己的系統與現有的基礎設施更好地集成,同時解決一些獨特的產品開發需求,如支持多個消費者組,提高可用性,特別是在網絡分區中。

Cherami的用戶被定義為生產者或消費者。生產者將任務排隊。消費者是異步接收和處理排隊任務的工作進程。Cherami的交付模式是典型的 競爭消費者 模式,其中同一消費者組中的消費者接收不相交的任務集(除了失敗情況,這會導致重新交付)。使用這種模式,多個工作程序能夠并行運作。工作程序的數量獨立于Cherami內部的任何分區或分片機制,并且可以簡單地通過添加或刪除工作程序來擴展和縮小。如果工作程序無法執行任務,則另一個工作程序可以重新傳送并重試該任務。

Cherami還支持多個消費者組,其中每個消費者組接收隊列中的所有任務。每個消費者組都與一個死信隊列( dead letter queue )相關聯。超過最大重試計數的任務(例如,“poison pill”)駐留在此隊列中,以便消費者組可以繼續處理其他消息。這些消費者處理簡單消息總線,從中區分Cherami,通常用于大數據攝取和分析(如 Apache Kafka ),使Cherami在任務隊列用例中處于優勢。

生產者將任務排到隊列A和B。隊列A將提要發送到兩個消費者組,這兩個消費者組都接收所有任務,分布在相應組內的消費者。隊列B僅將提要發送到一個消費者組。

在未使用Cherami之前,Uber使用 Redis 支持的 Celery 隊列用于所有任務隊列用例。Celery和Redis的組合幫助了Uber迅速擴張,達到巔峰。至于缺點,那就是Celery只有Python,而我們越來越依賴Go和Java來構建性能更高的后端服務。此外,Redis存儲是內存支持的,這不符合我們對可持續或可擴展的要求。

為了Uber的未來,我們需要一個長期解決方案。所以我們構建了Cherami來滿足這些要求:

  1. 持續性、無損性和硬件故障容錯性
  2. 網絡分區期間 可用性和一致性(AP vs CP) 之間的靈活性
  3. 輕松調整每個隊列吞吐量的擴展能力
  4. 完全支持競爭消費者消費模式
  5. 語言無關性

為了滿足這些要求,Cherami的設計遵循這些設計原則:

  1. 最終我們選擇了一致性作為核心原則。這允許高可用性和持續性,為此,我們犧牲了訂購保證。無論如何,這意味著我們可以在災難性故障或網絡分區期間繼續接受請求,并通過消除對像 Zookeeper 這樣一致的元數據存儲的需求,進一步提高可用性。

  2. 我們選擇不支持分區消費者模式,不向用戶公開分區。這簡化了消費者工作程序的管理,因為工作程序不需要協調要使用哪個分區。它還簡化了配置,因為生產者和消費者都可以獨立擴展。

在下面的章節中,我們進一步闡述Cherami的關鍵設計元素,并解釋我們如何應用設計原則和權衡。

Cherami的關鍵設計元素

1.故障恢復和復制

為了真正的無損性和可用性,Cherami必須容忍硬件故障。實際上,這需要Cherami通過不同的硬件復制每條消息,以便消息能夠可靠地讀取;但當硬件暫時或永久失效時,Cherami也必須能夠接收新消息。

Cherami的容錯性源于利用消息系統的附加屬性和消息傳輸中的流水線。消息隊列中的每個消息都是一個自包含的元素,一旦創建就不會被修改。換句話說,消息隊列只能添加(append-only)而不能修改。如果包含隊列的存儲主機失效,我們可以選擇不同的存儲主機并繼續寫入,從而保證入列操作繼續可用。

append-only屬性允許隊列在硬件故障期間仍然可以進行發布。

Cherami隊列由一個或多個區段組成,這些區段是隊列中獨立支持附加消息的概念性子流。通過稱為輸入主機的角色將擴展區復制到存儲層。創建擴展時,其元數據包含不可變的主機信息元組(輸入主機和存儲主機列表)。在每個存儲主機中,擴展的復制副本稱為副本,存儲主機可以托管不同擴展數據塊的多個副本。如果單個存儲主機發生故障,我們不會丟失消息,因為該區段仍可從其他副本讀取。

Cherami處理存儲主機故障。

生產者連接到特定輸入主機以發布到屬于某個隊列的區段。在從生產者接收到消息時,通過 WebSocket 連接到所有區段副本的同時輸入到主機消息管道。并從同一連接中的相應副本接收確認(ack)。

流水線意味著輸入主機在寫下一條消息之前不等待ack,并且在輸入主機和所有副本之間沒有消息重排序或消息跳過。這也適用于從每個副本返回ack;ack按照相應的寫入順序排列。輸入主機跟蹤所有ack。只有當接收到相同消息的所有存儲主機收到ack時,輸入主機確認生產者。這個最后的ack意味著消息已經持久地存儲在所有副本中。

由于流水線屬性,在每個區段內消息將被排序。這將確保所有副本的消息是一致的,除了存儲主機尚未持久消息的尾部。

輸入主機只接收來自所有存儲主機的前三個消息的ack。它將前三個消息發送給生產者,因為這些消息被保證被完全復制。

當任何副本失敗時,輸入主機不能從該副本接收任何進一步寫入的ack。因此,這個意義上來說不再是可追加的。如果輸入主機失效,我們將失去來自存儲主機的空中ack。在這兩種情況下,副本的尾部可能不一致:一個或多個消息不會在所有副本中被復制。為了從這種不一致性中恢復,而不是試圖掃描和修復尾部,這是一個復雜的操作,我們簡單地聲明這個區段“密封”它是可讀的,但不允許更多的寫入。

密封后,Cherami為此隊列創建一個新的區段,并且一個信令通道通知生產者重新連接并發布到新的區段。如果隊列只包含一個打開區段,那么密封該隊列將使隊列在創建新的區段之前暫時無法在很短時間內發布。為了避免在故障期間出現峰值延遲,隊列通常設置最小數量的區段,以便在密封一個區段并創建新區段時,發布可以繼續。

我們選擇使用密封作為恢復機制,因為它易于實施。復制的原因是,在失敗之后,副本將包含未發布給發布者的消息,并且如果輸入主機已經失效,則不可能確定哪些消息是未封裝的。因此,在讀取路徑中,我們將必須傳遞一切,包括這些未封裝的消息。當消息隊列失敗時,發布者通常會重試,所以這些部分消息可以發布到一個新的區段,使得消費者能夠重復接收。

2.寫入的縮放

Cherami中的區段是非共享子流。Cherami觀察每個區段的吞吐量。由于對特定隊列的寫入負載增加,并且某些區段超過其吞吐量限制,Cherami就會自動為該隊列創建附加區段。新區段接收部分寫入負載,減輕現有區段上的負載。

隨著寫入負載的減少,Cherami密封一些區段,而不用新的替換。這樣,Cherami減少了維持開放區段所需的一些開銷(內存、網絡連接和其他維護)。

3.消費處理

同一消費者組中的消費者從同一隊列接收任務,但也可以從一個或多個區段接收。當消費者收到消息并成功處理消息時,消費者使用ack答復Cherami。如果Cherami在一些配置的時間后沒有獲得確認,它會重新提交消息以重試。當消費者崩潰時,或者當下游依賴不可用時,或者當單個任務花費太長時,或者當由于死鎖而導致處理被阻塞時,消費者的確認可能被延遲或丟失。消費者還可以否定地確認或否認消息,觸發立即重新傳遞。Nacks允許消費者組處理某些成員不能處理的任務(例如,由于本地故障、消費者組到新任務模式的部分/滾動升級)。

因為不同的消費者可以花費不同的時間來處理消息,所以ack到達Cherami的順序,與副本提供的順序并不同。一些消息系統存儲每個消息的已讀/未讀狀態(也稱為可見性狀態)。但是,為了做到這一點,我們需要在磁盤上使用隨機寫入更新這些狀態,并為多個消費者組中處理每一個復雜的操作。

Cherami采取了不同的方法。在每個消費者組中,對于每個區段,我們維護一個ack偏移量,這是一個消息序列號,在其下所有消息都被確認。我們有一個稱為輸出主機的角色,消費者為了接收交付而連接到它。輸出主機按順序從存儲主機讀取消息,將它們緩存在內存中。它記錄正在傳輸的消息(傳遞給消費者,但尚未確認),并在可能時更新ack偏移量。輸出主機還跟蹤定時和nack,以便消息可以根據需要重新傳遞給另一個消費者。在Cherami中,一個擴展可以由多個消費者組同時使用,因此多個輸出主機可能從同一區段讀取。

輸出主機處理工作程序的亂序ack。

此外,系統為將每個消息配置重新傳遞有限次數。如果達到重新傳遞的限制次數,消息將被發布到死信隊列( dead letter queue,DLQ )并被標記為acked,使得ack偏移可以提前。這樣,沒有“毒丸”消息阻塞隊列中其他消息的處理。使用者組所有者可以手動檢查DLQ中的消息,然后使用以下兩種方式之一處理消息:清除或合并它們。清除它們將刪除消息,并且當它們無效或者沒有值(例如它們是時間敏感的)時是適當的。否則,所有者可以將它們合并回消費者組,這在消費者軟件已經修復可處理先前不能處理的消息時或者當暫時故障條件已經消退時是適當的。

4. 存儲

Cherami中的消息持久存儲在磁盤上。在存儲主機上,為了性能和索引功能,我們選擇了 RocksDB 作為存儲引擎。我們使用一個獨立的RocksDB實例,每個盤區有一個共享 LRU 塊緩存。消息存儲在數據庫中,增加的序列號作為鍵,消息本身作為值。因為鍵總是增加,RocksDB優化其壓縮,所以我們不會經歷寫入放大。當輸出主機從一個區段讀取消息時,它只是尋找其服務的消費者組的ack偏移量,并按序列號重復讀取更多消息。

使用RocksDB,我們也可以輕松實現定時器隊列,這是每個消息與延遲時間相關聯的隊列。在這種情況下,消息僅在指定的延遲后傳送。對于定時器隊列,我們構造包含高位位中的遞送時間和低位位中的序列號的密鑰。由于RocksDB提供了一個排序的迭代器,按照交付時間的順序迭代密鑰,而低位的序列號確保密鑰的唯一性:

系統架構

Cherami由幾個不同的角色組成。除了我們已經介紹的輸入、存儲和輸出角色之外,還有控制器和前端。典型的Cherami部署包括每個角色的幾個實例:

Cherami系統組件的相互作用。

不同的角色可以存在于同一物理主機上,甚至可以鏈接到單個二進制文件中。在Uber,每個角色在一個單獨的Docker容器中運行。輸入、存儲和輸出形成系統的數據平面。控制器和前端控制平面功能和元數據操作。

控制器

控制器是最好的協調器,它具備協調所有其他組件的智能。它主要確定何時創建和在哪里放置(對于哪個輸入和哪些存儲主機)區段。它還確定哪些輸出主機處理使用者組的消耗。

所有數據平面角色通過RPC調用向控制器報告負載信息。有了這個信息,控制器做出放置決定和平衡負載。有幾個這種控制器角色的實例。Ringpop還執行分布式健康檢查和隸屬功能。

前端

前端主機公開 TChannelThrift API,執行隊列和消費者組的 GRUD 操作。它們還公開用于數據平面路由的API。當生產者想要將消息發布到隊列中時,它調用路由API來發現哪些輸入主機包含隊列的區段。接下來,生產者使用WebSocket連接連接到那些輸入主機,并在已建立的流中發布消息。

類似地,當消費者想從隊列消費消息時,它首先調用路由API來發現哪些輸出主機管理隊列的區段的消費。然后,生產者使用WebSocket連接到那些輸出主機,并提取消息。創建新區段時,Cherami會向生產者和使用者發送通知,以便它們可以連接到新區段。我們開發了客戶端庫,使得這些交互得以簡化。

Cassandra和排隊

最后,Cherami將元數據存儲在 Cassandra 上,這是單獨部署的。元數據包含關于隊列,所有其區段以及所有消費者組信息的信息,例如每個消費者組每個區段的ACK偏移量。我們選擇Cassandra不僅因為Cassandra是一個高度可用的數據存儲系統,而且還因為它的可調諧一致性模型。這種靈活性允許我們在這樣的分區事件期間提供可以是分區容忍的而不是順序保留(AP隊列)或者順序保留(CP隊列)但在次分區中不可用的隊列。兩種類型的隊列的處理的主要區別是,區段創建是否需要條件更新操作。

AP隊列

對于AP隊列,創建區段不需要Cassandra中的仲裁級一致性。當發生網絡分區時,可以在分區的兩側創建區段。讓我們調用分區A和B。分區A中的生產者可以發布到該分區中的區段,而分區B中的生產者可以發布到分區B中的區段。因此,寫入不會被網絡分區阻塞。對于讀取,分區A中的消費者只能使用該分區中的區段,并且分區B中的消費者也是如此。不過,當網絡分區恢復時,消費者能夠訪問所有的區段。這里的代價是消息最終是一致的:不可能建立消息的全局排序,因為可以隨時隨地創建區段。在我們的實現中,當我們寫入區段元數據時,我們使用Cassandra一致性級別“ONE”。

CP隊列

對于CP隊列,區段創建需要是可線性化的:在網絡分區的情況下,我們必須確保只有一個分區可以創建一個區段以來繼承先前密封的區段。為了確保這一點,我們使用Cassandra的輕量級事務,以便如果同時由于任何原因創建了多個區段,則只有一個可以用于CP隊列。

Cherami的總結

Cherami是一個競爭消費者消息隊列,具有持久性、容錯性、高可用性和可擴展性。我們通過在存儲主機之間復制消息來實現持久性和容錯性,并通過利用消息隊列的僅附加屬性和選擇最終一致性作為我們的基本模型來實現高可用性。Cherami也是可擴展的,因為設計沒有單一的瓶頸。

Cherami是 Uber Engineering西雅圖辦公室 耗費半年從零開始進行設計并構建的。目前,每天Cherami在Uber Engineering的 許多微服務 中持續傳輸數億個任務,應用于這些用例:旅程后處理、欺詐檢測、用戶通知、激勵活動和許多其他用例。

Cherami完全 用Go語言編寫 ,這種語言使得構建高性能和并發系統軟件變得很有趣。此外,Cherami使用Uber已經開源的幾個庫:用于RPC的 TChannel 和用于健康檢查和組成員資格的 Ringpop 。Cherami依賴于多種第三方開源技術: Cassandra 用于元數據存儲,RocksDB用于消息存儲,以及GitHub上提供的許多其他第三方Go軟件包。我們計劃在不久的將來 開源 Cherami。

感謝陳興璐對本文的審校。

給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至[email protected]。也歡迎大家通過新浪微博(@InfoQ,@丁曉昀),微信(微信號: InfoQChina )關注我們。


Tags: 消息系統

文章來源:http://www.infoq.com/cn/articles/cherami-uber-engi


ads
ads

相關文章
ads

相關文章

ad