分布式多步驟事務(wù)
本文介紹了如何使用Serverless 工作流提供長(zhǎng)流程分布式事務(wù)保證,幫助用戶聚焦于自身業(yè)務(wù)邏輯。
簡(jiǎn)介
復(fù)雜的業(yè)務(wù)場(chǎng)景例如電商網(wǎng)站、酒店、航班預(yù)定這類涉及訂單管理的應(yīng)用通常要訪問多個(gè)遠(yuǎn)程服務(wù),并且對(duì)操作事務(wù)性語(yǔ)義(即所有步驟全部成功或全部失敗,不存在中間狀態(tài))有較高要求。在流量較小、數(shù)據(jù)存儲(chǔ)集中的應(yīng)用中,事務(wù)性可以通過關(guān)系型數(shù)據(jù)庫(kù)提供的ACID特性滿足。然而在大流量場(chǎng)景下,為了高可用和可擴(kuò)展性,業(yè)務(wù)通常選擇向微服務(wù)的分布式架構(gòu)方向演進(jìn)。在這樣的架構(gòu)中提供多步驟事務(wù)性的保證通常需要引入隊(duì)列和數(shù)據(jù)庫(kù)來持久化消息以及展現(xiàn)流程狀態(tài),這類系統(tǒng)的開發(fā)和運(yùn)維會(huì)給業(yè)務(wù)方帶來額外的成本和負(fù)擔(dān)。而使用Serverless 工作流提供長(zhǎng)流程分布式事務(wù)保證會(huì)幫您解決這些問題。
場(chǎng)景描述
假設(shè)某應(yīng)用為其用戶提供預(yù)定火車票、航班和酒店的功能,要求三個(gè)步驟保證事務(wù)性。該功能需要三個(gè)遠(yuǎn)程調(diào)用實(shí)現(xiàn)(例如預(yù)定火車票需要調(diào)用12306接口),如果三個(gè)調(diào)用都成功則該訂單成功。然而實(shí)際上任何一個(gè)遠(yuǎn)程調(diào)用都有可能會(huì)失敗,因此該應(yīng)用需要對(duì)不同的失敗場(chǎng)景做出相應(yīng)的補(bǔ)償邏輯,回退已完成操作。如下圖所示:
- 如果預(yù)定火車票(BuyTrainTicket)成功,而預(yù)定航班(ReserveFlight)失敗,則需要取消已經(jīng)購(gòu)買的火車票(CancelTrainTicket),并告知您訂單失敗。
- 如果預(yù)定火車票(BuyTrainTicket)和預(yù)定航班(ReserveFlight)均成功,但是預(yù)訂酒店(ReserveHotel)失敗,則需要取消已經(jīng)預(yù)定的航班(CancelFlight)和火車票(CancelTrainTicket),并告知您訂單失敗。
Serverless 工作流實(shí)現(xiàn)
下文的示例將FC函數(shù)編排成一個(gè)Serverless 工作流流程從而實(shí)現(xiàn)了一個(gè)可靠的多步驟長(zhǎng)流程,該示例分為3步:
步驟1:創(chuàng)建FC函數(shù)
本步驟是模擬場(chǎng)景案例中提示的三個(gè)操作即預(yù)訂火車票、預(yù)訂航班及預(yù)定酒店。
- Service: fnf-demo
- Function: Operation
Operation函數(shù)模擬各操作(例如預(yù)定航班、預(yù)定酒店)的實(shí)現(xiàn),根據(jù)輸入決定該操作執(zhí)行結(jié)果(成功或失敗)。
import json
import logging
import uuid
def handler(event, context):
evt = json.loads(event)
logger = logging.getLogger()
id = uuid.uuid4()
op = "operation"
if 'operation' in evt:
op = evt['operation']
if op in evt:
result = evt[op]
if result == False:
logger.info("%s failed" % op)
exit()
logger.info("%s succeeded, id %s" % (op, id))
return '{"%s":"success", "%s_txnID": "%s"}' % (op, op, id)
步驟2:創(chuàng)建流程
使用Serverless工作流控制臺(tái)創(chuàng)建下面的流程。
- 配置流程RAM角色
{ "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": [ "fnf.aliyuncs.com" ] } } ], "Version": "1" }
- 流程定義
version: v1 type: flow steps: - type: task resourceArn: acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation name: BuyTrainTicket inputMappings: - target: operation source: buy_train_ticket - target: buy_train_ticket source: $input.buy_train_ticket_result catch: - errors: - FC.Unknown goto: OrderFailed - type: task resourceArn: acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation name: ReserveFlight inputMappings: - target: operation source: reserve_flight - target: reserve_flight source: $input.reserve_flight_result catch: # 捕獲ReserveFlight task拋出的FC.Unknown錯(cuò)誤,跳轉(zhuǎn)到CancelTrainTicket。 - errors: - FC.Unknown goto: CancelTrainTicket - type: task resourceArn: acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation name: ReserveHotel inputMappings: - target: operation source: reserve_hotel - target: reserve_hotel source: $input.reserve_hotel_result retry: # 對(duì)FC.Unknown類型的錯(cuò)誤最多指數(shù)退避重試3次,初始間隔1s,后續(xù)間隔=上次間隔*2。 - errors: - FC.Unknown intervalSeconds: 1 maxAttempts: 3 multiplier: 2 catch: # 捕獲ReserveHotel task拋出的FC.Unknown錯(cuò)誤,跳轉(zhuǎn)到CancelFlight。 - errors: - FC.Unknown goto: CancelFlight - type: succeed name: OrderSucceeded - type: task resourceArn: acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation name: CancelFlight inputMappings: - target: operation source: cancel_flight - target: reserve_flight_txnID source: $local.reserve_flight_txnID - type: task resourceArn: acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation name: CancelTrainTicket inputMappings: - target: operation source: cancel_train_ticket - target: reserve_flight_txnID source: $local.reserve_flight_txnID - type: fail name: OrderFailed
步驟3:執(zhí)行并查看結(jié)果
在控制臺(tái)上對(duì)創(chuàng)建好的流程(Flow)開始一個(gè)新的執(zhí)行(Execution)。StartExecution API要求傳入JSON格式的輸入。下面的JSON對(duì)象可以模擬每個(gè)步驟的成功或失敗(例如"reserve_hotel_result":"fail"代表模擬預(yù)定酒店這步失敗)。StartExecution是一個(gè)異步API,調(diào)用結(jié)束后,Serverless 工作流會(huì)返回一個(gè)執(zhí)行名字用來查詢流程執(zhí)行狀態(tài)。
{
"buy_train_ticket_result":"success",
"reserve_flight_result":"success",
"reserve_hotel_result":"fail"
}
流程執(zhí)行開始后,在Serverless 工作流控制臺(tái)可以查看流程的執(zhí)行過程和結(jié)果。從步驟信息頁(yè)簽?zāi)梢钥吹剑捎?span data-tag="ph" id="codeph-lt9-s10-wb0" class="ph">"reserve_hotel_result":"fail"
和ReserveHotel
函數(shù)調(diào)用失敗,Serverless 工作流按照流程定義,依次取消航班(CancelFlight)、取消火車票(CancelTrainTicket)。Serverless 工作流每個(gè)步驟轉(zhuǎn)換有持久化的保證,因此網(wǎng)絡(luò)中斷或進(jìn)程崩潰等失敗場(chǎng)景不會(huì)影響流程事務(wù)性的保證。
流程執(zhí)行會(huì)產(chǎn)生執(zhí)行歷史事件(event),這些事件可以通過控制臺(tái)、SDK或CLI調(diào)用GetExecutionHistory
API查詢。
錯(cuò)誤處理和重試
- 上面示例中的預(yù)定航班、預(yù)定酒店等遠(yuǎn)程調(diào)用都有可能受到網(wǎng)絡(luò)或服務(wù)錯(cuò)誤等原因?qū)е抡{(diào)用失敗,而增加對(duì)瞬時(shí)錯(cuò)誤的重試可以提高訂單流程成功率。Serverless 工作流在任務(wù)(Task)類型的步驟(Step)自帶重試功能,如預(yù)定酒店這個(gè)步驟用下面的寫法可以實(shí)現(xiàn)對(duì)FC.Unknown類型的錯(cuò)誤指數(shù)退避。假設(shè)重試到達(dá)最大次數(shù)后
ReserveHotel
都無法成功,按照該步驟中catch
的定義,ReserveHotel函數(shù)拋出的FC.Unknown錯(cuò)誤會(huì)被捕獲并將跳轉(zhuǎn)到CancelFlight
執(zhí)行定義好的補(bǔ)償邏輯。- type: task resourceArn: acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation name: ReserveHotel inputMappings: - target: operation source: reserve_hotel retry: # 對(duì)FC.Unknown類型的錯(cuò)誤最多指數(shù)退避重試3次,初始間隔1s,后續(xù)間隔=上次間隔*2。 - errors: - FC.Unknown intervalSeconds: 1 maxAttempts: 3 multiplier: 2 catch: # 捕獲ReserveHotel task拋出的FC.Unknown錯(cuò)誤,跳轉(zhuǎn)到CancelFlight。 - errors: - FC.Unknown goto: CancelFlight
- 下圖可以看到加入重試之后預(yù)訂酒店(ReserveHotel)任務(wù)執(zhí)行了多次直到最大重試數(shù)。
步驟間的數(shù)據(jù)傳遞
- 預(yù)定酒店失敗后需要取消航班和火車票,這兩部分別需要用到預(yù)定航班和預(yù)定火車票返回的交易ID(txnID),下面的
inputMapping
對(duì)象描述了如何將之前步驟產(chǎn)生的輸出傳入CancelFlight
這個(gè)步驟中。- type: task resourceArn: acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation name: CancelFlight inputMappings: - target: operation source: cancel_flight - target: reserve_flight_txnID source: $local.reserve_flight_txnID
- 流程執(zhí)行各步驟結(jié)束的輸出都會(huì)被放在
StepExited
事件詳情(EventDetail)的local對(duì)象中。{ "input":{ "operation":"reserve_hotel", "reserve_hotel_result":"fail" }, "local":{ "buy_train_ticket":"success", "buy_train_ticket_txnID":"d37412b3-bb68-4d04-9d90-c8c15643d45e", "reserve_flight_result":"success", "reserve_flight_txnID":"024caecf-cfa3-43a6-b561-9b6fe0571b55" }, "resourceArn":"acs:fc:{region}:{accountID}:services/fnf-demo/functions/Operation", "cause":"{\"errorMessage\":\"Process exited unexpectedly before completing request (duration: 12ms, maxMemoryUsage: 9.18MB)\"}", "error":"FC.Unknown", "retryCount":3, "goto":"CancelFlight" }
- 結(jié)合上面的
EventDetail
和inputMappings
的映射之后,傳入到CancelFlight
步驟的輸入變成如下JSON對(duì)象,這樣CancelFlight
函數(shù)的輸入會(huì)包含reserve_flight_txnID
字段。"input":{ "operation":"cancel_flight", "reserve_flight_txnID":"024caecf-cfa3-43a6-b561-9b6fe0571b55" }