函式計算非同步任務能力介紹 - 任務觸發去重

語言: CN / TW / HK

作者:漸意

前言

無論是在大資料處理領域,還是在訊息處理領域,任務系統都有一個很關鍵的能力 - 任務觸發去重。這個能力在一些對準確性要求極高的場景(如金融領域)中是必不可少的。作為 Serverless 化任務處理平臺,Serverless Task 也需要提供這類保障,在使用者應用層面及自身系統內部兩個維度具備任務的準確觸發語義。本文主要針對訊息處理可靠性這一主題來介紹函式計算非同步任務功能的技術細節,並展示如何在實際應用中使用函式計算所提供的這方面能力來增強任務執行的可靠性。

淺談任務去重

在討論非同步訊息處理系統時,訊息處理的基本語義是無法繞開的話題。在一個非同步的訊息處理系統(任務系統)中,一條訊息的處理流程簡化如下圖所示:

1.png

圖 1

使用者下發任務 - 進入佇列 - 任務處理單元監聽並獲取訊息 - 排程到實際 worker 執行

在任務訊息流轉過程中,任何元件(環節)可能出現的宕機等問題會導致訊息的錯誤傳遞。一般的任務系統會提供至多 3 個層級的訊息處理語義:

  • At-Most-Once:保證訊息最多被傳遞一次。當出現網路分割槽、系統元件宕機時,可能出現訊息丟失;
  • At-Least-Once:保證訊息至少被傳遞一次。訊息傳遞鏈路支援錯誤重試,利用訊息重發機制保證下游一定收到上游訊息,但是在宕機或者網路分割槽的場景下,可能導致相同訊息傳遞多次。
  • Exactly-Once 機制則可以保證訊息精確被傳送一次,精確一次並不是意味著在宕機或網路分割槽的場景下沒有重傳,而是重傳對於接受方的狀態不產生任何改變,與傳送一次的結果一樣。在實際生產中,往往是依賴重傳機制 & 接收方去重(冪等)來做到 Exactly Once。

函式計算能夠提供任務分發的 Exactly Once 語義,即無論在何種情況下,重複的任務將被系統認為是相同的觸發,進而只進行一次的任務分發。

結合圖 1,如果要做到任務去重,系統至少需要提供兩個維度的保障:

  1. 系統側保障:任務排程系統自身的 failover 不影響訊息的傳遞正確性及唯一性;
  2. 提供給使用者一種機制,可以結合業務場景,做到整個業務邏輯的觸發+執行去重。

下面,我們將結合簡化的 Serverless Task 系統架構,談一談函式計算是如何做到上面的能力的。

函式計算非同步任務觸發去重的實現背景

函式計算的任務系統架構如下圖所示:

2.png

圖 2

首先,使用者呼叫函式計算 API 下發一個任務(步驟 1)進入系統的 API-Server 中,API-Server 進行校驗後將訊息傳入內部佇列(步驟 2.1)。後臺有一個非同步模組實時監聽內部佇列(步驟 2.2),之後呼叫資源管理模組獲取執行時資源(步驟 2.2-2.3)。獲取執行時資源後,排程模組將任務資料下發到 VM 級別的客戶端中(步驟 3.1),並由客戶端將任務轉發至實際的使用者執行資源(步驟 3.2)。為了做到上文中所提到的兩個維度的保障,我們需要在以下層面進行支援:

  1. 系統側保障:在步驟 2.1 - 3.1 中,任何一箇中間過程的 Failover 只能觸發一次步驟 3.2 的執行,即只會排程一次使用者例項的執行;
  2. 使用者側應用級別去重能力:能夠支援使用者多次反覆執行步驟 1,但實際只會觸發一次 步驟 3.2 的執行。

系統側優雅升級 & Failover 時的任務分發去重保證

當用戶的訊息進入函式計算系統中(即完成步驟 2.1)後,使用者的請求將收到 HTTP 狀態碼 202 的 Response,使用者可以認為已經成功提交一次任務。從該任務訊息進入 MQ 起,其生命週期便由 Scheduler 維護,所以 Scheduler 的穩定性及 MQ 的穩定性將直接影響系統 Exactly Once 的實現方案。

在大多數開源訊息系統中(如 MQ、Kafka)一般都提供訊息多副本儲存及唯一消費的語義。函式計算所使用的訊息佇列(最底層為 RocketMQ)也是同樣的,底層儲存的 3 副本實現使得我們無需關注訊息儲存方面的穩定性。除此之外,函式計算所使用的的訊息佇列還具有以下特性:

  1. 消費的唯一性:每一個佇列中的每一條訊息當被消費後,會進入“不可見模式”。在此模式下,其他消費者無法獲取該訊息;
  2. 每條訊息的實際消費者需要實時更新該模式的不可見時間;當消費者消費完成後,需要顯示的刪除該訊息。

因此,訊息在佇列中的的整個生命週期如下圖所示:

3.png

圖 3

Scheduler 主要負責訊息的處理,其任務主要有以下幾個部分組成:

  1. 根據函式計算負載均衡模組的排程策略,監聽自身所負責的佇列;
  2. 當佇列中出現訊息後,拉取訊息,並在記憶體中維持一個狀態:直到訊息消費完成(使用者例項返回函式執行結果)前,不斷更新訊息的可見時間,確保訊息不會再次在佇列中出現;
  3. 當任務執行完成後,顯示刪除該訊息。

在佇列的排程模型方面,函式計算對於普通使用者採用“單佇列”的管理模式;即每一個使用者的所有非同步執行請求由一個獨立佇列相互隔離,並且由一個 Scheduler 固定負責。這個負載的對映關係由函式計算的負載均衡服務進行管理,如下圖所示(我們在後續文章中還會更為詳細的介紹這部分內容):

4.png

圖 4

當 Scheduler 1 發生宕機或升級時,任務由兩種執行狀態:

  1. 如果訊息還未傳遞到使用者的執行例項中(圖 2 中的步驟 3.1 ~ 3.2),那麼當這臺 Scheduler 負責的佇列被其他 Scheduler 拾起後,訊息將在消費可見期後再次出現,因此 Scheduler 2 將再次獲取該訊息,做到後續的觸發。
  2. 如果訊息已經開始執行(步驟 3.2),當訊息在 Scheduler 2 中再次出現後,我們依賴使用者 VM 中的 Agent 進行狀態管理。此時 Scheduler 2 將向對應的 Agent 傳送執行請求;此時 Agent 發現該訊息已經存在於記憶體中,那麼將直接忽略執行請求,並將執行的結果在執行後通過此連結告知 Scheduler 2,進而完成 Failover 的恢復。

使用者側業務級別的分發去重實現

函式計算系統能夠做到對於單點故障下的每條訊息準確的消費能力,但是如果使用者側對於同一條業務資料反覆觸發函式執行的話,函式計算無法識別不同訊息是否在邏輯上是同一個任務。這種情況往往發生在網路分割槽。在圖 2 中,如果使用者呼叫 1 發生超時,此時有可能有兩種情況:

  1. 訊息未到達函式計算系統,任務未成功提交;
  2. 訊息已經到達函式計算併入隊,任務提交成功,但由於超時使用者無法得知提交成功的資訊。

大多數情況下使用者會對此次的提交進行重試。如果是第 2 種情況,那麼同一個任務將被提交併執行多次。因此函式計算需要提供一種機制,保證這種場景下業務的準確性。

函式計算提供了 TaskID 這一任務概念(StatefulAsyncInvocationID)。該 ID 全域性唯一。使用者每次提交任務均可以指定這樣一個 ID。當發生請求超時時,使用者可以進行無限次重試。所有的重複重試將在函式計算側進行校驗。函式計算內部使用 DB 對任務 Meta 資料進行儲存;當有相同 ID 進入系統時該次請求將被拒絕,並返回 400 錯誤。此時客戶端即可得知任務的提交情況。

在實際使用中以 Go SDK 為例,您可以編輯如下觸發任務的程式碼:

import fc "github.com/aliyun/fc-go-sdk"

func SubmitJob() {
    invokeInput := fc.NewInvokeFunctionInput("ServiceName", "FunctionName")
    invokeInput = invokeInput.WithAsyncInvocation().WithStatefulAsyncInvocationID("TaskUUID")
  invokeOutput, err := fcClient.InvokeFunction(invokeInput)
    ...
}

便提交了一個獨一無二的任務。

總結

本文介紹了函式計算 Serverless Task 對於任務觸發去重的相關技術細節,以便支援對於任務執行準確性有嚴格要求的場景。在使用 Serverless Task 後,您無需擔心任何系統元件的 Failover,您每次提交的任務將被準確執行一次。為了支援業務側語義的分發去重,您可以在提交任務時設定任務的全域性唯一 ID,使用函式計算提供的能力幫您對任務進行去重處理。

作者介紹:漸意,阿里雲 Serverless 高階開發工程師