- 華為Serverless核心技術與實踐
- 劉方明等
- 3363字
- 2022-05-06 18:20:04
3.1.2 有狀態函數編程模型的實現
本節具體介紹有狀態編程模型的實現方式,以及基于有狀態函數實現的各種編排模型。
3.1.2.1 狀態和函數的關系描述
如圖3-1描述了有狀態函數編程模型中函數與狀態之間的關系。函數用來處理狀態,是處理狀態的接口實現,例如,函數a、b、c都是狀態A的操作接口實現。不處理狀態的函數可以將其狀態視為空。函數之間支持直接調用。

圖3-1 有狀態函數編程模型中函數與狀態之間的關系
3.1.2.2 狀態的定義和操作
明確了狀態和函數的關系之后,我們首先來看如何進行狀態的定義和操作。
1. 狀態的定義
狀態用來表示在業務處理中跨函數調用的過程數據,跨函數調用是指通過invoke API或restful trigger調用函數。開發者需要定義狀態,狀態以系統唯一的stateid為標識。
定義狀態即定義狀態的數據結構,然后用于初始化context.state即可。

2. 狀態的操作
開發者可以在函數中通過context參數(context.state)訪問狀態。執行函數前,系統會自動將狀態實例加載到context.state中。當函數執行結束后,系統會自動保存狀態。系統會為每一個狀態設置一個默認的老化超時時間,如果老化時間間隔內沒有操作該狀態,則會刪除該狀態。開發者可以通過settimeout接口來設置這個超時時間。當然,開發者也可以主動終止并刪除狀態。這兩種機制可以類比為單機編程中對數據資源的回收和主動釋放。系統對狀態進行如下保證。
? 原子性:系統保證函數對狀態的所有操作要么全部完成,要么全部不完成。如果在執行過程中發生錯誤,則退回到函數開始前的狀態。
? 一致性:不存在函數執行過程的中間狀態,系統保證狀態的一致性要求。
? 隔離性:對同一個狀態的并發操作不會互相干擾,不會出現不一致的現象。對不同狀態的操作可以并發執行。系統為每個狀態維護一個隊列,對該狀態的操作請求進行排隊并按順序執行。
? 持久性:如果函數執行結束,那么對狀態操作的結果就是持久性的。接下來的其他操作或故障不應該對其執行結果產生任何影響。
表3-2是有狀態函數操作的主要API。
表3-2 有狀態函數操作的主要API

invoke操作是有狀態函數直接調用的接口。當用戶不指定stateid的值時,系統會新創建一個狀態,并返回狀態的ID。開發者可以通過YR.getYRFuture(future)獲取狀態的ID和返回值value。偽代碼邏輯如下。

有狀態函數幫助開發者屏蔽了狀態管理的復雜過程,為開發者提供了原生的單機編程體驗。
? 對狀態的操作如同操作變量一樣簡單。
? 對同一個狀態的并發操作保證一致性,不同狀態的操作可以并行執行。
? 系統對開發者屏蔽了容器崩潰異常。容器崩潰后,系統會自動重新執行,并保證對有狀態函數的調用滿足At-least-once execution要求。相比之下,如果實現Exactly-once execution,則系統實現復雜且性能開銷較大;如果沒有確保At-least-once execution,則編寫操作的正確性就很難保證。最終選擇At-least-once execution,這需要在編寫操作正確性和系統開銷之間做出折中選擇,相應的代價是如果出現重復的請求執行,則需要開發者保證操作的冪等性。
3.1.2.3 函數的定義和操作
函數的定義方式與現有的Serverless平臺基本保持一致,只是增加了對狀態的操作,如表3-2所示的狀態操作API。以下是有狀態函數及其調用的示例代碼。
? 有狀態函數jshandler定義了一個簡單的計數函數counter。


? 有狀態函數調用函數caller,通過invoke函數調用了上面的jshandler函數。

3.1.2.4 通過有狀態函數支持函數編排
對于無狀態函數,函數編排可以解決應用的有狀態問題。函數編排實際上是對函數的執行進行狀態機管理。例如,AWS通過step function進行函數編排,開發者需要通過類似DSL語言對函數的執行過程進行編排。微軟則定義了一種特殊的Durable函數進行編排工作,支持不同的編排模型。相比之下,華為元戎的有狀態函數由于原生支持狀態操作,因此可以不用重新定義函數類型進行編排操作。本節將介紹如何使用有狀態函數來實現多種模式的函數編排。
函數編排應當遵循以下原則。
? 函數應當被視為“黑盒”。
? 替換原則,即編排也是一個函數。
? 編排應該避免雙花問題,即重復計費。
微軟官方文檔中提到的Durable函數支持的函數編排有6種通用應用模式中,華為元戎的有狀態函數可以直接支持其中的3種模式(函數鏈、聚合器、異步HTTP API),而其他3種模式(扇出/扇入、監視、人機交互)由于“雙花問題”,即重復消費(使用)問題,需要對函數進行簡單拆分后方可支持這3種模式,下面我們將詳細講解如何使用有狀態函數實現6種通用應用模式。
1. 編排函數相關接口
在介紹6種通用模式的實現之前,首先介紹與編排函數相關的接口。
開發者需要實現的handler函數如下所示。

當其返回Future時,系統會等待Future完成,再將結果返回給被調用者。利用這個特性,可以將等待Future完成這一操作從Function函數轉移到系統中,從而避免雙花問題。
開發者可以調用invoke、onComplete、getYRFuture和wait函數,如下所示。

2. 通用模式
下面介紹幾種通用模式的實現。
(1)函數鏈。對多個函數進行順序調用,即流水線任務順序執行可形成函數鏈,如圖3-2所示。

圖3-2 函數鏈
這個過程中可能有分支,但整體上是順序進行的。根據函數鏈中的函數調用是否存在數據依賴關系可以細化出多種模式,舉例如下。
模式一:F2依賴F1的執行結果,F3依賴F2的執行結果,F4依賴F3的執行結果,實現代碼如下。

模式二:基于模式一進行拓展,函數調用關系不變,F3依賴F1和F2執行完成后再觸發,其余與模式一保持一致,實現代碼如下。

模式三:基于模式一繼續拓展,函數調用關系不變,某些調用不存在數據依賴關系,例如,F3不依賴F1和F2的執行結果,只需要等待F1和F2完成,此模式的實現與模式一、模式二相比,需要引入一個新的函數接口wait,實現該模式的代碼如下。

(2)扇出/扇入。扇出/扇入是指多個函數同時執行,然后等待所有函數返回執行結果后再執行下一步,如圖3-3所示。例如,用戶完成支付需要通過短信、微信、郵件等多種方式通知用戶。此外,fork/join也符合這種模式。

圖3-3 扇入/扇出
直觀的方式是,使用一個有狀態函數實現扇出/扇入模式,示例代碼如下。

以上這種做法會導致雙花問題。由于使用了getFuture接口,導致在F2執行的時候,即使F1沒有工作也要等待并占用資源,同樣在F3執行的時候,F2也要等待并占用資源。為了解決這個雙花問題,可以考慮對函數編排進行拆分,需要注意的是,對函數編排進行拆分時需要遵守“黑盒原則”,即不能修改F1、F2、F3的函數實現,例如,不能向F1的函數實現中添加invoke(F2),示例代碼如下。

handler返回Future前會等待其完成,因此ENTRY會等待F2_WRAPPER完成才返回給調用者,F2_WRAPPER會等待F3_WRAPPER完成才返回給調用者,F3_WRAPPER會等待F3完成才返回給調用者,這些等待過程都是在系統層面完成的,不占用Function的執行時間,從而避免了雙花問題。
(3)聚合器。聚合器用于對數據進行聚合處理,聚合是一個典型的有狀態處理過程,例如,在監控數據聚合時,函數需要等待數據上報后再進行聚合,然后持久化到時序數據庫中,如圖3-4所示。

圖3-4 聚合
由于聚合是典型的有狀態處理過程,所以使用有狀態函數實現該模式的示例代碼非常直觀。

(4)異步HTTP API。異步HTTP API模式解決的問題是如何協同long-running任務的狀態。實現此模式的常見方法是使HTTP端點觸發長期運行的操作,然后將客戶端重定向到狀態端點,客戶端輪詢該端點以了解操作完成時的情況,如圖3-5所示。

圖3-5 異步HTTP API
在這種模式下,可通過圖3-5中的GetStatus函數查看Start和DoWork的執行狀態。
例如,通過HTTP請求異步觸發函數。

通過函數的id可以查詢函數的執行狀態。

在這種模式下執行的函數需要保存執行狀態,并提供狀態訪問的接口函數,GetStatus函數訪問相應接口即可。
(5)監視。監視模式是工作流中一種靈活的循環過程,如輪詢直到滿足特定條件。我們可以通過函數監視任務來實現監視模式,如圖3-6所示。這種模式常用來監控內部任務的執行情況并進行處理,例如,數據從TP數據庫遷移到AP數據庫,如果遷移無法在8點前完成,需要終止遷移,可采用該模式來實現。

圖3-6 監視
可以使用一個有狀態函數實現該模式,示例代碼如下。

和前面造成雙花問題的模式一樣,這里由于調用了3次getYRFuture函數,所以也存在雙花問題。因此,可將上述函數拆分為多個函數,以避免雙花問題,拆分后監視模式的狀態機描述如圖3-7所示。

圖3-7 監視模式的狀態機描述
根據圖3-7的拆分方案,entry將調用獲取和判斷任務狀態的wrapper函數及執行后續操作的wapper函數,wrapper函數獲取并等待future執行,示例代碼如下。


(6)人機交互。工作流中的某些步驟需要人工干預才能繼續,例如,在審批流程中,主管審批后才能繼續進行后續流程。人機交互模式如圖3-8所示。

圖3-8 人機交互模式
使用一個有狀態函數實現該模式,示例代碼如下。

由于在此模式下需要調用wait函數,和getYRFuture函數一樣,這也會造成雙花問題。同樣,還是將上述函數進行拆分,以避免雙花問題。我們把上述函數拆分為entry和wait兩個函數,示例代碼如下。
