作者:不瞋 (阿裡雲 Serverless 技術負責人)
當我們建構一個應用,總是希望它是響應迅速,成本低廉的。而在實際中,我們的系統卻面臨各種各樣的挑戰,例如不可預測的流量高峰,依賴的下遊服務變得緩慢,少量請求卻消耗大量 CPU/記憶體資源。這些因素常常導緻整個系統被拖慢,甚至不能響應請求。為了讓應用服務總是響應迅速,很多時候不得不預留更多的計算資源,但大部分時候,這些計算資源都是閑置的。一種更好的做法是将耗時緩慢,或者需要消耗大量資源的處理邏輯從請求處理主邏輯中剝離出來,交給更具資源彈性的系統異步執行,不但讓請求能夠被迅速處理傳回給使用者,也節省了成本。
一般來說,長耗時,消耗大量資源,或者容易出錯的邏輯,非常适合從請求主流程中剝離出來,異步執行。例如新使用者注冊,注冊成功後,系統通常會發送一封歡迎郵件。發送歡迎郵件的動作就可以從注冊流程中剝離出來。另一個例子是使用者上傳圖檔,圖檔上傳後通常需要生成不同大小的縮略圖。但圖檔處理的過程不必包含在圖檔上傳處理流程中,使用者上傳圖檔成功後就可以結束流程,生成縮略圖等處理邏輯可以作為異步任務執行。這樣應用伺服器避免被圖檔處理等計算密集型任務壓垮,使用者也能更快的得到響應。常見的異步執行任務包括:
- 發送電子郵件/即時消息
- 檢查垃圾郵件
- 文檔處理(轉換格式,導出,……)
- 音視訊,圖檔處理(生成縮略圖,加水印,鑒黃,轉碼,……)
- 調用外部的三方服務
- 重建搜尋索引
- 導入/導出大量資料
- 網頁爬蟲
- 資料清洗
- ……
Slack,他們的業務場景中一共有超過100種不同類型的異步任務。一個功能完備的異步任務處理系統能帶來顯著的收益:
- 更快的系統響應時間。将長耗時的,重資源消耗的邏輯從請求處理流程中剝離,在别的地方異步執行,能有效的降低請求響應延時,帶來更好的使用者體驗。
- 更好的處理大量突發性請求。在電商等很多場景下,常常有大量突發性請求對系統造成沖擊。同樣的,如果将重資源消耗邏輯從請求處理流程中剝離,在别的地方異步執行,那麼相同資源容量的系統能響應更大峰值的請求流量。
- 更低的成本。異步任務的執行時長通常在數百毫秒到數小時之間,根據不同的任務類型,合理的選擇任務執行時間和更彈性的使用資源,就能實作更低的成本。
- 更完善的重試政策和錯誤處理能力。任務保證被可靠的執行(at-least-once),并且按照配置的重試政策進行重試,進而實作更好的容錯能力。例如調用第三方的下遊服務,如果能變成異步任務,設定合理的重試政策,即使下遊服務偶爾不穩定,也不影響任務的成功率。
- 更快的完成任務處理。多個任務的執行是高度并行化的。通過伸縮異步任務處理系統的資源,海量的任務能夠在合理的成本内更快的完成。
- 更好的任務優先級管理和流控。任務根據類型,通常按照不同的優先級處理。異步任務管理系統能幫助使用者更好的隔離不同優先級的任務,既讓高優先級任務能更快的被處理,又讓低優先級任務不至于被餓死。
- 更多樣化的任務觸發方式。任務的觸發方式是多種多樣的,例如通過 API 直接送出任務,或是通過事件觸發,或是定時執行等等。
- 更好的可觀測性。異步任務處理系統通常會提供任務日志,名額,狀态查詢,鍊路追蹤等能力,讓異步任務更好的被觀測、更容易診斷問題。
- 更高的研發效率。使用者專注于任務處理邏輯的實作,任務排程,資源擴縮容,高可用,流控,任務優先級等功能都由任務處理系統完成,研發效率大幅提高。
任務處理系統架構
任務處理系統通常包括三部分:任務 API 和可觀測,任務分發和任務執行。我們首先介紹這三個子系統的功能,然後再讨論整個系統面臨的技術挑戰和解決方案。

任務 API/Dashboard
該子系統提供一組任務相關的 API,包括任務建立、查詢、删除等等。使用者通過 GUI,指令行工具,後者直接調用 API 的方式使用系統功能。以 Dashboard 等方式呈現的可觀測能力也非常重要。好的任務處理系統應當包括以下可觀測能力:
- 日志:能夠收集和展示任務日志,使用者能夠快速查詢指定任務的日志。
- 名額:系統需要提供排隊任務數等關鍵名額,幫助使用者快速判斷任務的執行情況。
- 鍊路追蹤:任務從送出到執行過程中,各個環節的耗時。比如在隊列中排隊的時間,實際執行的時間等等。下圖展示了Netflix Cosmos 平台的 tracing 能力。
任務分發
任務分發負責任務的排程分發。一個能應用于生産環境的任務分發系統通常要具備以下功能:
- 任務的可靠分發:任務一旦送出成功後,無論遇到任何情況,系統都應當保證該任務被排程執行。
- 任務的定時/延時分發:很多類型的任務,希望在指定的時間執行,例如定時發送郵件/消息,或者定時生成資料報表。另一種情況是任務可以延時較長一段時間執行也沒問題,例如下班前送出的資料分析任務在第二天上班前完成即可,這類任務可以放到淩晨資源消耗低峰的時候執行,通過錯峰執行降低成本。
- 任務去重:我們總是不希望任務被重複執行。除了造成資源浪費,任務重複執行可能造成更嚴重的後果。比如一個計量任務因為重複執行算錯了賬單。要做到任務隻執行一次(exactly-once),需要在任務送出,分發,執行全鍊路上的每個環節都做到,包括使用者在實作任務處理代碼時也要在執行成功,執行失敗等各種情況下,做到 exactly-once。如何實作完整的 exactly-once 比較複雜,超出了本文的讨論範圍。很多時候,系統提供一個簡化的語義也很有價值,即任務隻成功執行一次。任務去重需要使用者在送出任務時指定任務 ID,系統通過 ID來判斷該任務是否已經被送出和成功執行過。
- 任務錯誤重試:合理的任務重試政策對高效、可靠的完成任務非常關鍵。任務的重試要考慮幾個因素:1)要比對下遊任務執行系統的處理能力。比如收到下遊任務執行系統的流控錯誤,或者感覺到任務執行成為瓶頸,需要指數退避重試。不能因為重試反而加大了下遊系統的壓力,壓垮下遊;2)重試的政策要簡單清晰,易于使用者了解和配置。首先要對錯誤進行分類,區分不可重試錯誤,可重試錯誤,流控錯誤。不可重試錯誤是指确定性失敗的錯誤,重試沒有意義,比如參數錯誤,權限問題等等。可重試錯誤是指導緻任務失敗的因素具有偶然性,通過重試任務最終會成功,比如網絡逾時等系統内部錯誤。流控錯誤是一種比較特殊的可重試錯誤,通常意味着下遊已經滿負荷,重試需要采用退避模式,控制發送給下遊的請求量。
- 任務的負載均衡:任務的執行時間變化很大,短的幾百毫秒,長的數十小時。簡單的 round-robin 方式分發任務,會導緻執行節點負載不均。實踐中常見的模式是将任務放置到隊列中,執行節點根據自身任務執行情況主動拉取任務。使用隊列儲存任務,讓根據節點的負載把任務分發到合适的節點上,讓節點的負載均衡。任務負載均衡通常需要分發系統和執行子系統配合實作。
- 任務按優先級分發:任務處理系統通常對接很多的業務場景,他們的任務類型和優先級各不相同。位于業務核心體驗相關的任務執行優先級要高于邊緣任務。即使同樣是消息通知,淘寶上買家收到一個商品評論通知的重要性肯定低于新冠疫情中的核酸檢測通知。但另一方面,系統也要保持一定程度的公平,不要讓高優先級任務總是搶占資源,而餓死低優先級任務。
- 任務流控:任務流控典型的使用場景是削峰填谷,比如使用者一次性送出數十萬的任務,期望在幾個小時内慢慢處理。是以系統需要限制任務的分發速率,比對下遊任務執行的能力。任務流控也是保證系統可靠性的重要手段,某類任務送出量突然爆發式增長,系統要通過流控限制其對系統的沖擊,減小對其他任務的影響。
- 批量暫停和删除任務:在實際生産環境,提供任務批量暫停和删除非常重要。使用者總是會出現各種狀況,比如任務的執行出現了某些問題,最好能暫停後續任務的執行,人工檢查沒有問題後,再恢複執行;或者臨時暫停低優先級任務,釋放計算資源用于執行更高優先級的任務。另一種情況是送出的任務有問題,執行沒有意義。是以系統要能讓使用者非常友善的删除正在執行和排隊中的任務。任務的暫停和删除需要分發和執行子系統配合實作。
任務分發的架構可分為拉模式和推模式。拉模式通過任務隊列分發任務。執行任務的執行個體主動從任務隊列中拉取任務,處理完畢後再拉取新任務。相對于拉模式,推模式增加了一個配置設定器的角色。配置設定器從任務隊列中讀取任務,進行排程,推送給合适的任務執行執行個體。
拉模式的架構清晰,基于 Redis 等流行軟體可以快速搭建任務分發系統,在簡單任務場景下表現良好。但如果要支援任務去重,任務優先級,批量暫停或删除,彈性的資源擴縮容等複雜業務場景需要的功能,拉模式的實作複雜度會迅速增加。實踐中,拉模式面臨以下一些主要的挑戰:
- 資源自動伸縮和負載均衡複雜。任務執行執行個體和任務隊列建立連接配接,拉取任務。當任務執行執行個體規模較大時,對任務隊列的連接配接資源會造成很大的壓力。是以需要一層映射和配置設定,任務執行個體隻和對應的任務隊列連接配接。下圖是 等開源項目提供了按排隊任務數等名額伸縮的模式。AWS 也結合 CloudWatch 提供了[類似的解決方案](https://d1.awsstatic.com/architecture-diagrams/ArchitectureDiagrams/autoscaling-asynchronous-job-queues.pdf">Keda。
- K8s 一般需要配合隊列來實作異步任務,隊列資源的管理需要使用者自行負責。
- K8s 原生的作業排程和啟動時間比較慢,而且送出作業的 tps 一般小于 200,是以不适合高 tps,短延時的任務。
注意:K8s 中的作業(Job)和本文讨論的任務(task)有一些差別。K8s 的 Job 通常包含處理一個或者多個任務。本文的任務是一個原子的概念,單個任務隻在一個執行個體上執行。執行時長從幾十毫秒到數小時不等。
大規模多租戶異步任務處理系統實踐
接下來,筆者以阿裡雲函數計算的異步任務處理系統為例,探讨大規模多租戶異步任務處理系統的一些技術挑戰和應對政策。在阿裡雲函數計算平台上,使用者隻需要建立任務處理函數,然後送出任務即可。整個異步任務的處理是彈性、高可用的,具備完整的可觀測能力。在實踐中,我們采用了多種政策來實作多租戶環境的隔離、伸縮、負載均衡和流控,平滑處理海量使用者的高度動态變化的負載。
動态隊列資源伸縮和流量路由
如前所述,異步任務系統通常需要隊列實作任務的分發。當任務進行中台對應很多業務方,那麼為每一個應用/函數,甚至每一個使用者都配置設定單獨的隊列資源就不再可行。因為絕大多數應用都是長尾的,調用低頻,會造成大量隊列,連接配接資源的浪費。并且輪詢大量隊列也降低了系統的可擴充性。
但如果所有使用者都共享同一批隊列資源,則會面臨多租戶場景中經典的“noisy neighbor”問題,A 應用突發式的負載擠占隊列的處理能力,影響其他應用。
實踐中,函數計算建構了動态隊列資源池。一開始資源池内會預置一些隊列資源,并将應用哈希映射到部分隊列上。如果某些應用的流量快速增長時,系統會采取多種政策:
- 如果應用的流量持續保持高位,導緻隊列積壓,系統将為他們自動建立單獨的隊列,并将流量分流到新的隊列上。
- 将一些延時敏感,或者優先級高的應用流量遷移到其他隊列上,避免被高流量應用産生的隊列積壓影響。
- 允許使用者設定任務的過期時間,對于有實時性要求的任務,在發生積壓時快速丢棄過期任務,確定新任務能更快的處理。
負載随機分片
在一個多租環境中,防止“破壞者”對系統造成災難性的破壞是系統設計的最大挑戰。破壞者可能是被 DDoS 攻擊的使用者,或者在某些 corner case 下正好觸發了系統 bug 的負載。下圖展示了一種非常流行的架構,所有使用者的流量以 round-robin 的方式均勻的發送給多台伺服器。當所有使用者的流量符合預期時,系統工作得很好,每台伺服器的負載均勻,而且部分伺服器當機也不影響整體服務的可用性。但當出現“破壞者”後,系統的可用性将出現很大的風險。
如下圖所示,假設紅色的使用者被 DDoS 攻擊或者他的某些請求可能觸發伺服器當機的 bug,那麼他的負載将可能打垮所有的伺服器,造成整個服務不可用。
上述問題的本質是任何使用者的流量都會被路由到所有伺服器上,這種沒有任何負載隔離能力的模式在面臨“破壞者”時相當脆弱。對于任何一個使用者,如果他的負載隻會被路由到部分伺服器上,能不能解決這個問題?如下圖所示,任何使用者的流量最多路由到2台伺服器上,即使造成兩台伺服器當機,綠色使用者的請求仍然不受影響。這種将使用者的負載映射到部分而非全部伺服器的負載分片模式,能夠很好的實作負載隔離,降低服務不可用的風險。代價則是系統需要準備更多的備援資源。
接下來,讓我們調整下使用者負載的映射方式。如下圖所示,每個使用者的負載均勻的映射到兩台伺服器上。不但負載更加均衡,更棒的是,即使兩台伺服器當機,除紅色之外的使用者負載都不受影響。如果我們把分區的大小設為2,那麼從3台伺服器中選擇2台伺服器的組合方式有 C_{3}^{2}=3 種,即3種可能的分區方式。通過随機算法,我們将負載均勻的映射到分區上,那麼任意一個分區不可服務,則最多影響1/3的負載。假設我們有100台伺服器,分區的大小仍然是2,那麼分區的方式有C_{100}{2}=4950種,單個分區不可用隻會影響1/4950=0.2%的負載。随着伺服器的增多,随機分區的效果越明顯。對負載随機分區是一個非常簡潔卻強大的模式,在保障多租系統的可用性中起到了關鍵的作用。
自适應下遊處理能力的任務分發
函數計算的任務分發采用了推模式,這樣使用者隻需要專注于任務處理邏輯的開發,平台和使用者的邊界也很清晰。在推模式中,有一個任務配置設定器的角色,負責從任務隊列拉取任務并配置設定到下遊的任務處理執行個體上。任務配置設定器要能根據下遊處理能力,自适應的調整任務分發速度。當使用者的隊列産生積壓時,我們希望不斷增加 dispatch worker pool 的任務分發能力;當達到下遊處理能力的上限後,worker pool 要能感覺到該狀态,保持相對穩定的分發速度;當任務處理完畢後,work pool 要縮容,将分發能力釋放給其他任務處理函數。
在實踐中,我們借鑒了 tcp 擁塞控制算法的思想,對 worker pool 的擴縮容采取 AIMD 算法(Additive Increase Multiplicative Decrease,和性增長,乘性降低)。當使用者短時間内送出大量任務時,配置設定器不會立即向下遊發送大量任務,而是按照“和性增長”政策,線性增加分發速度,避免對下遊服務的沖擊。當收到下遊服務的流控錯誤後,采用“乘性減少”的政策來,按照一定的比例來縮容 worker pool。其中流控錯誤需要滿足錯誤率和錯誤數的門檻值後才觸發縮容,避免 worker pool 的頻繁擴縮容。
向上遊的任務生産方發送背壓(back pressure)
如果任務的處理能力長期落後于任務的生産能力,隊列中積壓的任務會越來越多,雖然可以使用多個隊列并進行流量路由來減小租戶之間的互相影響。但任務積壓超過一定門檻值後,應當更積極的向上遊的任務生産方回報這種壓力,例如開始流控任務送出的請求。在多租共享資源的場景下,背壓的實施會更加有挑戰。例如A,B應用共享任務分發系統的資源,如果A應用的任務積壓,如何做到:
- 公平。盡可能流控A而不是B應用。流控本質是一個機率問題,為每一個對象計算流控機率,機率越準确,流控越公平。
- 及時。背壓要傳遞到系統最外層,例如在任務送出時就對A應用流控,這樣對系統的沖擊最小。
如何在多租場景中識别到需要流控的對象很有挑戰,我們在實踐中借鑒了Sample and Hold算法,取得了較好的效果。感興趣的讀者可以參考相關論文。
異步任務處理系統的能力分層
根據前述對異步任務處理系統的架構和功能的分析,我們将異步任務處理系統的能力分為以下三層:
- Level 1:一般需 1-5 人研發團隊,系統是通過整合 K8s 和消息隊列等開源軟體/雲服務的能力搭建的。系統的能力受限于依賴的開源軟體/雲服務,難以根據業務需求進行定制。資源的使用偏靜态,不具備資源伸縮,負載均衡的能力。能夠承載的業務規模有限,随着業務規模和複雜度增長,系統開發和維護的代價會迅速增加。
- Level 2:一般需 5-10人研發團隊,在開源軟體/雲服務的基礎之上,具備一定的自主研發能力,滿足常見的業務需求。不具備完整的任務優先級、隔離、流控的能力,通常是為不同的業務方配置不同的隊列和計算資源。資源的管理比較粗放,缺少實時資源伸縮和容量管理能力。系統缺乏可擴充性,資源精細化管理能力,難以支撐大規模複雜業務場景。
- Level 3:一般需 10+ 人研發團隊,能夠打造平台級的系統。具備支撐大規模,複雜業務場景的能力。采用共享資源池,在任務排程,隔離流控,負載均衡,資源伸縮等方面能力完備。平台和使用者界限清晰,業務方隻需要專注于任務處理邏輯的開發。具備完整的可觀測能力。
Level 1 | Level 2 | Level 3 | |
任務的可靠分發 | 支援 | 支援 | 支援 |
任務定時/延時發送 | 取決于選擇的消息隊列能力。一般支援定時任務,但不支援延時任務 | 支援 | 支援 |
任務去重 | 不支援 | 支援 | 支援 |
任務錯誤自動重試 | 有限支援。一般依賴于 K8s Jobs 内置的重試政策。對于未使用 K8s Jobs 的任務,則需使用者在任務處理邏輯中自行實作 | 有限支援。一般依賴于 K8s Jobs 内置的重試政策。對于未使用 K8s Jobs 的任務,則需使用者在任務處理邏輯中自行實作 | 支援。平台和使用者界限清晰,根據使用者設定的政策重試 |
任務負載均衡 | 有限支援。在任務執行執行個體規模小的情況下通過消息隊列實作 | 有限支援。在任務執行執行個體規模小的情況下通過消息隊列實作 | 支援。系統具備大規模節點的負載均衡能力 |
任務優先級 | 不支援 | 有限支援。允許使用者為高優先級任務預留資源,或者限制低優先級任務的資源使用 | 支援。高優先級任務可搶占低優先級任務資源,同時系統會兼顧公平,避免低優先級任務被餓死 |
任務流控 | 不支援 | 不支援。一般是為不同任務類型或者業務方配置獨立的隊列和計算資源 | 在系統的每個環節具備流控能力,系統不會因為任務爆發式送出雪崩 |
任務批量暫停/删除 | 不支援 | 有限支援。取決于是否為不同任務類型或者業務方配置獨立的隊列和計算資源 | 支援 |
共享資源池 | 有限支援。依賴 K8s 的排程能力。一般是為各個業務方搭建不同的叢集 | 有限支援。依賴 K8s 的排程能力。一般是為各個業務方搭建不同的叢集 | 支援。不同類型的任務,不同業務場景共享同一個資源池 |
資源彈性伸縮 | 不支援。K8s 的 HPA 通常難以滿足任務場景下的伸縮要求 | 不支援。K8s 的 HPA 通常難以滿足任務場景下的伸縮要求 | 支援。根據排隊任務數,節點資源使用率等多元度實時伸縮 |
任務資源隔離 | 支援。依賴容器的資源隔離能力 | 支援。依賴容器的資源隔離能力 | 支援。依賴容器的資源隔離能力 |
任務資源配額 | 不支援 | 支援 | 支援 |
簡化任務處理邏輯編碼 | 不支援。任務處理邏輯需要自行拉取任務,執行任務 | 不支援。任務處理邏輯需要自行拉取任務,執行任務 | 支援 |
系統平滑更新 | 不支援 | 不支援 | 支援 |
執行結果通知 | 不支援 | 不支援 | 支援 |
可觀測性 | 依賴 K8s,消息隊列等開源軟體自身的可觀測能力。具備基本的任務狀态查詢 | 依賴 K8s,消息隊列等開源軟體自身的可觀測能力。具備基本的任務狀态查詢 | 具備從任務到系統各個層面的完整可觀測能力 |
結論
異步任務是建構彈性、高可用,響應迅速應用的重要手段。本文對異步任務的适用場景和收益進行了介紹,并讨論了典型異步任務系統的架構、功能和工程實踐。要實作一個能夠滿足多種業務場景需求,彈性可擴充的異步任務處理平台具有較高的複雜度。而阿裡雲函數計算 FC 為使用者提供了開箱即用的,接近于Level ß3能力的異步任務處理服務。使用者隻需要建立任務處理函數,通過控制台,指令行工具,API/SDK,事件觸發等多種方式送出任務,就可以彈性、可靠、可觀測完備的方式處理任務。函數計算異步任務覆寫任務處理時長從毫秒到24小時的場景,被阿裡雲資料庫自制服務 DAS,支付寶小程式壓測平台,網易雲音樂,新東方,分衆傳媒,米連等集團内外客戶廣泛應用。
附錄
- 函數計算異步任務和 K8S Jobs 的能力對比。
對比項 | 函數計算異步任務 | K8S Jobs |
适用場景 | 适合任務執行時長數十毫秒的實時任務和任務執行時長幾十小時的離線任務 | 适合任務送出速度要求不高,任務負載比較固定,任務實時性要求不高的離線任務 |
任務可觀測能力 | 支援。提供日志,任務排隊數等名額,任務鍊路耗時,任務狀态查詢等豐富可觀測能力 | 自行整合開源軟體實作。 |
任務執行個體自動擴縮容 | 支援。根據任務排隊數,執行個體資源使用率自動擴縮容 | 不支援。一般通過任務隊列,自行實作自動擴縮容和執行個體負載均衡,複雜度高 |
任務執行個體伸縮速度 | 毫秒級 | 分鐘級 |
任務執行個體資源使用率 | 使用者隻需要選擇合适的執行個體規格,執行個體自動伸縮,按實際處理任務的時長計量,資源使用率高 | 需在作業(Job)送出時确定執行個體的規格和數目。執行個體難以自動伸縮和負載均衡,資源使用率低 |
任務送出速度 | 單個使用者支援每秒送出數萬任務 | 整個叢集每秒最多啟動數百作業(Jobs) |
任務定時/延時送出 | 支援 | 支援定時任務,不支援延時任務 |
任務去重 | 支援 | 不支援 |
暫停/恢複任務執行 | 支援 | Alpha 狀态(K8S v1.21) |
終止指定任務 | 支援 | 有限支援。通過終止任務執行個體間接實作 |
任務流控 | 支援。可在使用者,任務處理函數等不同粒度進行流控 | 不支援 |
任務結果自動回調 | 支援 | 不支援 |
開發運維成本 |
- 網易雲音樂 Serverless Jobs 實踐,音頻處理算法業務落地速度10x提升
- 其他異步任務案例