Slack是如何實現分散式任務處理的擴充套件?

banq發表於2017-12-15

這裡介紹Slack公司是如何使用Kafka和Redis作為分散式任務佇列(類似國內噹噹網的elastic-job),以毫秒級可靠地處理數十億個任務。

Slack是一家提供協作工具的SaaS公司,提供聊天群組 + 大規模工具整合 + 檔案整合 + 統一搜尋四大功能,幫團隊打造了一條流暢的訊息匯流排,彌補了團隊協作中的溝通斷層,致力於形成團隊的知識庫,類似國內釘釘之類企業QQ之類定位。

下面是他們的分散式任務處理架構的經驗分享:

Slack是使用任務Job佇列(任務佇列/工作佇列)系統處理一些特殊業務邏輯,這些業務邏輯在Web請求的上下文中執行起來太費時。該系統是我們架構的關鍵組成部分,應用於每個Slack訊息、推送通知、URL展開、日曆提醒和計費計算。在我們最忙碌的日子裡,系統以每秒33,000的高峰處理超過14億份任務job。任務Job執行時間範圍從幾毫秒到(在某些情況下)幾分鐘。

任務Job的佇列實施可追溯到Slack早期時間,以後不斷增長,逐漸被廣泛用於整個公司。隨著時間的推移,我們遭遇到CPU、記憶體和網路資源的容量限制,我們開始擴充套件縮放系統,但原來的架構基本保持不變。

但是,大約一年前,Slack由於任務佇列問題而經歷了明顯的生產中斷。資料庫層中的資源爭用導致任務作業執行速度放緩,導致Redis達到其配置的最大記憶體限制。在這一點上,由於Redis沒有空閒的記憶體,我們不能再排隊加入新的任務,這意味著所有依賴於任務佇列的Slack操作都失敗了。更糟糕的是,我們的系統實際上需要一點空閒的Redis記憶體才能使任務Job從佇列中取出,所以即使解決了底層的資料庫爭用,作業佇列仍然被鎖定,需要大量的手動干預才能恢復。

這一事件導致整個任務架構進行重新評估。接下來介紹的是關於我們如何在此核心系統設計上做出重大改變的經驗故事,對周圍依賴系統的破壞最小,沒有進行“停止整個系統”的轉換或單向遷移,而且提供了未來改進的餘地。

初始任務佇列系統架構

在去年的這個時候,任務佇列架構可以按照如下方式進行勾畫,對於建立或使用Redis任務佇列的人來說,這將大致熟悉(banq注:類似國內噹噹網的elastic-job):

www ---> redis ----> worker

一個Job任務的執行過程:

1.將任務放入Redis的佇列時時,Web應用程式首先根據任務型別和引數建立一個識別符號。

2.入隊處理程式根據該識別符號的雜湊和給定作業的邏輯佇列來選擇相應配置的Redis主機之一。

3.使用儲存在Redis主機上的pub/sub資料結構,處理程式會執行有限的重複資料刪除 - 如果佇列中已存在具有相同ID的作業,則請求將被丟棄,否則作業將被新增到佇列中。

4.worker機器對Redis叢集進行了輪詢,尋找新的工作。當一個worker在一個監視的佇列中找到一個Job任務時,它將任務從待處理佇列移動到一個正在進行的任務列表中,併產生一個非同步任務來處理它。

5.一旦任務完成,worker從正在進行的任務清單中刪除任務。如果任務失敗,則worker將其移動到特定的佇列中,重試之前配置的次數,直到成功或移動到永久失敗的任務(手動檢查和修復)列表中。

架構問題

之前發生的中斷教訓使我們得出結論:擴充套件現有系統是站不住腳的,需要更多的基礎設計工作。

我們確定的一些約束是:

1.Redis沒有什麼操作上的空間,特別是在記憶體方面。如果我們排隊的速度超過了佇列取出速度一段時間以後,我們將耗盡記憶體,無法再取出任務(因為取出任務也需要有足夠的記憶體將任務移到處理列表中)。

2.Redis連線形成了一個完整的偶圖 - 每個任務佇列客戶端必須連線到每個Redis例項(因此具有正確和最新的資訊)。

worker無法獨立於Redis進行擴充套件 - 新增一個新worker導致額外的投票和載入Redis。此屬性導致了一個複雜的反饋情況,試圖增加執行能力可能會壓倒已經超載的Redis例項,從而放慢或停止整個進度。

3.之前關於使用Redis資料結構的決策意味著任務出列努力是與佇列長度成比例的。隨著佇列變長,他們變得更難以清空 - 另一個不幸的反饋迴圈。

4.提供給應用程式和平臺工程師的語義和服務質量保證不清楚,難以定義;任務佇列上的非同步處理對於我們的系統架構是非常重要的,但實際上工程師們不願意使用它。現有功能(如有限的重複資料刪除)的更改也是非常高風險的,因為許多作業都依靠它們正常執行。

這些問題中的每一個都提出了各種解決方案,包括進一步擴大現有系統的投入到完全徹底改寫。我們確定了我們認為能夠解決最迫切需求的三個方面的架構:

1. 使用持久的儲存(如Kafka)替換Redis記憶體中的資料結構,以提供在記憶體耗盡和任務丟失的緩衝區。

2.為任務開發一個新的排程程式,提高服務質量保證,並提供諸如限速和優先順序等理想特性。

3.將任務執行從Redis中分離出來,使我們能夠根據需要擴充套件任務執行,而不是進行困難且昂貴的權衡操作。

增量更改或完全重寫?

我們知道,實施所有這些潛在的架構增強功能將會促使Web應用程式和任務佇列的重大變化。團隊希望專注於最關鍵的問題,並獲得任何新的系統元件的生產經驗,而不是一次嘗試一切。漸進演進是最有效的方法。

我們決定解決的第一個問題是,我們無法保證佇列建立期間的寫入可用性。如果工作人員的排隊速度低於出列速度,Redis叢集本身最終會耗盡記憶體。在Slack的規模下,這是可能會很快發生。此時,Redis群集將無法接受任何其他任務。

我們曾想過用Kafka完全替代Redis,但很快就意識到這條路線需要對排程、執行和重複刪除作業的應用程式邏輯進行重大改變。本著追求最小可行的改變的精神,我們決定在Redis之前新增Kafka,而不是用Kafka徹底取代Redis。這將緩解我們的系統中的關鍵瓶頸,同時保留現有的應用程式入隊和出隊介面。

以下是使用Kafka及其在Redis之前的支援元件的作業佇列架構的增量更改草圖。


Slack是如何實現分散式任務處理的擴充套件?

將任務Job排入Kafka

我們面臨的第一個挑戰是如何有效地從我們的PHP / Hacklang網路應用程式找到任務然後放入到Kafka。儘管我們為此目的探討了現有的解決方案,但是沒有一個能夠滿足我們的需求,所以我們開發了Kafkagate,這是一個用Go編寫的新的無狀態服務,讓任務排隊到Kafka。

Kafkagate公開了一個簡單的HTTP POST介面,每個請求都包含一個Kafka主題,分割槽和內容。對於Kafka 使用Sarama golang驅動程式,它只是將傳入的HTTP請求中繼到Kafka中,並返回操作結果成功/失敗。通過這個設計,Kafkagate保持與各個客戶端的持續連線,並可以跟隨叢集領導者的變化,同時為我們的PHP / Hack網路應用程式提供一個低延遲的簡單介面。

Kafkagate專為:

1. 在可用性和一致性兩者中偏向前者: 在寫任務到Kafka時,我們只等待叢集領導者那邊承認這個請求,而不是把Job任務複製給其他佇列。這種選擇提供了儘可能低的延遲,但是如果Kafka佇列主機在複製之前意外死亡,確實會造成丟失任務的風險。對於大多數Slack的應用語義而言,這是一個正確的權衡,儘管我們也正在考慮向Kafkagate新增一個選項,以允許關鍵的應用程式等待某些操作的更強一致性保證。

2.簡單的客戶端語義: Kafkagate使用同步寫入到Kafka,它允許我們肯定地確認到達佇列的任務(儘管存在上述丟失寫入的風險),並在失敗或超時的情況下返回錯誤。這樣可以在不改變原有語義的情況下加強語義,使工程師可以放心地使用它,同時還能夠在將來修改佇列佇列設計。

3.最小延遲:為了減少花費在排隊工作上的時間,我們對效能進行了一些優化。例如,我們如何部署和路由到Kafkagates:Slack部署在AWS上,AWS 在每個獨立區域提供了幾個“ 可用區域 ”(AZ)。一個地區的地理資訊系統具有低延遲鏈路,並提供一定程度的隔離,大多數故障不會影響其他地理資訊系統。AZ之間的連線通常比保持在單個AZ中的連線更高的延遲,並且還會產生轉移成本。現在,任務佇列優先將請求路由到與主機排隊任務相同的AZ中的Kafkagate例項,同時仍允許故障轉移到其他AZ,這樣可以提高延遲和成本,同時仍允許容錯。

在未來,我們正在考慮進一步優化在網路應用主機上本地執行的Kafkagate服務,以避免在寫給Kafka時額外的網路跳躍。

將任務從卡夫卡中繼到Redis

架構中的下一個新元件解決了將任務從Kafka中繼到Redis中。JQRelay是一個用Go編寫的無狀態服務,將來自Kafka主題的任務轉發到其相應的Redis叢集。在設計這項服務時,我們必須考慮以下幾點:

1.資料編碼:在早期的系統中,Web應用程式(用PHP和Hack編寫)會在將任務儲存在Redis中時對JSON進行編碼。隨後,任務佇列工作者(也用PHP編寫)將解碼任務有效載荷以供執行。在新系統中,我們依靠JQRelay(用Go編寫)來解碼JSON編碼的任務,檢查任務,然後用JSON重新編碼並將其寫入相應的Redis叢集。聽起來很簡單吧?

事實證明,golang和PHP JSON編碼器都有一些意外奇怪現象,引起了我們的心痛。具體而言,在Go,<,>,和&字元被替換成對應的Unicode實體預設情況下,在PHP中,/字元以“escaped” \ 預設替換。這兩種行為都會導致代表兩個不同執行時間的JSON資料結構表示,這種情況在原來架構僅限於PHP的系統中是不存在的。

2.自配置:當JQRelay例項啟動時,它將嘗試獲取對應於Kafka主題的鍵/值條目的Consul鎖。如果獲得鎖定,則開始轉發來自該主題的所有分割槽的作業。如果失去鎖定,它將釋放所有資源並重新啟動,以便不同的例項可以提取該主題。我們在EC2自動擴充套件組中執行JQRelay,以便任何失敗的機器自動更換到服務中,並通過此鎖定流程。與此Consul鎖定策略結合使用時,我們確保作業佇列使用的所有Kafka主題都只有一箇中繼程式分配給它們,並且失敗會自動恢復。

3.處理失敗: JQRelay依靠Kafka提交偏移來跟蹤每個主題分割槽中的任務作業。如果任務成功寫入Redis,則分割槽使用者僅移動一下偏移。如果發生Redis問題,它將無限期地重試,直到Redis重新啟動(或Redis服務本身被更換)。任務具體的錯誤是通過將任務重新安排到Kafka來處理,而不是悄悄地放棄任務。通過這種方式,我們可以防止特定於任務的錯誤會阻止相應佇列上的所有任務的處理進度,我們保留這個任務,以便我們可以診斷並修復錯誤,而不會完全失去這個任務。

4.速率限制: JQRelay在寫入Redis時尊重Consul中配置的速率限制。它會依靠Consul watch API來對速率限制的變化做出反應。

Kafka叢集設定

我們的叢集執行Kafka 0.10.1.2版本,有16個經紀人,並在i3.2xlarge EC2機器上執行。每個主題都有32個分割槽,複製因子為3,保留期為2天。我們使用支援rack的複製(其中“rack”對應於AWS可用性區域)進行容錯,並且啟用了不乾淨的領導者選舉。

1.負載測試:我們建立了一個負載測試環境,以強化我們的Kafka叢集,然後將其投入生產。作為負載測試的一部分,我們按預期的生產速率將任務排入各種卡夫卡主題。這個負載測試使我們能夠適當規模化我們的Kafka生產叢集,從而擁有足夠的空間來處理單個佇列主機的失敗、叢集領導層的變化和其他行政行為,併為我們未來增長的Slack服務提供空間。

2.失敗測試:瞭解不同的Kafka叢集故障情況在應用程式中將如何表現是很重要的,例如連線失敗,任務作業入隊失敗,失蹤的任務和重複的任務。為此,我們對以下故障情況進行了測試:

(1)努力殺死一個broker佇列主機

(2)在一個單一的AZ中殺死兩個broker並優雅地殺死他們

(3)殺死所有三個broker,迫使卡夫卡挑一個不潔的領導人

(4)重新啟動群集

在所有這些情況下,系統都按照我們的預期執行,並達到了可用性目標。

3.資料遷移:我們使用我們的負載測試設定來確定跨代理安全資料遷移的最佳節流率。此外,我們嘗試在遷移期間使用較低的保留期(因為我們在成功執行後不需要保留一份工作)。

我們每天有14億個任務,我們寧願有選擇性地將broker業務的分割而不是主題遷移。這是未來工作的一部分。

生產部署

推出新系統包括以下步驟:

1.雙寫:我們開始兩次寫入任務到舊和新的系統(每個任務是排隊到雙方的Redis和Kakfa)。然而,JQRelay在“陰影”模式下運作,從卡夫卡(Kafka)中讀取所有的工作。這個設定讓我們安全地測試從web應用程式到JQRelay的實際生產流量的新排隊路徑。

2.保證系統正確性:為確保新系統的正確性,我們追蹤和比較了通過系統各個部分的工作數量:從Web應用程式到Kafkagate,Kafkagate到Kafka,最後是Kafka到Redis。

3.心跳:為了確保新系統為50個Redis群集和1600個Kafka分割槽(50個主題×32個分割槽)進行端到端的工作,我們每隔一分鐘就為每個Kafka分割槽加入心跳機制。然後,我們監測並警告這些心跳的端到端流量和時間。

4. 最終推出:一旦我們確信我們的系統正確性,我們就在內部為Slack啟用了幾個星期。在沒有問題的情況下,我們針對客戶的各種任務型別逐一推出。

結論

將Kafka新增到任務佇列中,在保護我們的基礎設施免於侷限於Redis記憶體方面的限制取得了巨大的成功。讓我們來重新構建佇列:在舊系統中,如果Web應用程式的排隊速率比作業佇列出隊速率更高,則Redis叢集本身最終會耗盡記憶體並導致中斷。在新系統中,當應用程式被寫入持久儲存(Kafka)時,Web應用程式可以維持其高入隊率。相反,我們調整JQRelay中的速率限制,以匹配出隊速率或暫停佇列到Redis。

從更廣的角度來看,這項工作還改善了任務佇列的可操作性,可配置的速率限制和任務排隊超出執行能力的持久儲存 - 比我們以前處理的更細的工具。更清晰的客戶端語義將幫助我們的應用程式和平臺團隊更加自信地使用任務佇列。而且基礎架構團隊還可以繼續改進任務佇列,從將JQRelay的速率限制與Redis記憶體容量繫結到改進系統排程和執行方面的更大目標。

Scaling Slack’s Job Queue – Several People Are Cod

[該貼被admin於2017-12-18 17:48修改過]

相關文章