GCP Workflows試してみた
TL;DR
- microservices間のトランザクションを想定してGCPのWorkflowsを試してみた
- responceやHTTP statusでworkflowを分岐してみた
- HTTP statusでのretryを試してみた
Workflows
サーバーレス ワークフローを使用して、Google Cloud と HTTP ベースの API サービスをオーケストレートします。
Workflowsを使うとyamlの定義に沿ってステップごとに適切にAPIをcallすることができるとのことなので下記のブログをものすごく参考にさせてもらいWorkflowsを試してみました。
Micorservicesで困ること
Micorservicesで困ることの1つにトランザクションの管理があります。
ECサイトの商品注文処理を例にするとざっくり注文処理の中には、
- 在庫を抑える
- 決済処理
- 注文確定
などがあります。
monolithなサービスの場合、ひとつのDBのトランザクションでアトミックな処理行うこともできます。仮に在庫を抑えた後で、決済処理に失敗した場合まとめてrollbackし、取り消すことが可能です。
しかしMicroservicesを採用した場合、注文を管理するorder service, 在庫を管理するstock service, 決済を管理するpayment serviceのように分かれて実装されることが多いかと思います。
その場合、仮に在庫を抑えた後で決済処理に失敗した時はすでに在庫を抑えるという処理は完了しているので、単純にrollbackすることはできません。また決済失敗といってもretry可能な一時的なエラーだったり、ネットワークの問題でcallした側はエラー,timeoutなどだったが、callされたservice側では正常に処理を完了していた。などのケースがありえます。そのため冪等性を担保したAPI実装や適切なretryなどが必要になってきます。
Workflowsで解決だ
上記のような注文処理をWorkflowsを使って処理してみます。
それぞれ
- 注文を管理するorder service
- 在庫を管理するstock service
- 決済を管理するpayment service
があるとして、フローは下記のようにしてみようと思います
- 注文を受け付ける
- 在庫を抑える
- ただし在庫が不足している場合は注文を取り消す
- 決済処理
- ただし決済が失敗(残高不足、無効なクレジットカードとか)した場合、抑えていた在庫を開放 + 注文を取り消す
- 注文確定
下準備
サンプルのcodeは下記になります
サンプルcodeをもとに準備を進めていきます- 必要に応じてGCP project作成
- サービスの有効化
gcloud services enable run.googleapis.com workflows.googleapis.com cloudbuild.googleapis.com
- Microservicesのbuild
PROJECT_ID=XXXXXXX make build_services
// gcloud builds submit --tag gcr.io/$(PROJECT_ID)/order-service services/order
- Microservicesのdeploy
PROJECT_ID=XXXXXXX make deploy_services
// gcloud run deploy stock-service \
// --image gcr.io/$(PROJECT_ID)/stock-service \
// --platform=managed --region=us-central1 \
// --no-allow-unauthenticated
- service accountの作成と設定
SERVICE_ACCOUNT_NAME="cloud-run-invoker"
SERVICE_ACCOUNT_EMAIL=${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \
--display-name "Cloud Run Invoker"
- 各呼び出しの権限付与
SERVICE_NAME="order-service"
gcloud run services add-iam-policy-binding $SERVICE_NAME \
--member=serviceAccount:$SERVICE_ACCOUNT_EMAIL \
--role=roles/run.invoker \
--platform=managed --region=us-central1
gcloud run services add-iam-policy-binding $SERVICE_NAME \
--member=serviceAccount:$SERVICE_ACCOUNT_EMAIL \
--role=roles/run.viewer \
--platform=managed --region=us-central1
※同様のことをSERVICE_NAME=stock-service, payment-serviceに書き換えて実行
- Workflowsのdeploy
まずは各ServiceのURLの取得
SERVICE_NAME="order-service"
ORDER_SERVICE_URL=$(gcloud run services list --platform managed \
--format="table[no-heading](URL)" --filter="SERVICE:${SERVICE_NAME}")
余分なものを省いたyamlは下記のようになります。
main:
params: [args]
steps:
- reserveStock:
call: http.post
args:
url: https://STOCK_SERVICE_URL/reserve
body:
"unit": ${args.unit}
auth:
type: OIDC
result: reserveStockResult
- switchByReserveStock:
switch:
- condition: ${reserveStockResult.body.status == "reserved"}
next: authorizePayment
next: voidOrder
- authorizePayment:
call: http.post
args:
url: https://PAYMENT_SERVICE_URL/authorize
body:
"price": ${args.price}
auth:
type: OIDC
result: authorizePaymentResult
- switchByAuthorizePayment:
switch:
- condition: ${authorizePaymentResult.body.status == "authorized"}
next: updateOrder
next: cancelReservedStock
- updateOrder:
call: http.post
args:
url: https://ORDER_SERVICE_URL/update
auth:
type: OIDC
result: updateOrderResult
next: finish
- cancelReservedStock:
call: http.post
args:
url: https://STOCK_SERVICE_URL/cancelReserve
auth:
type: OIDC
result: cancelReservedStockResult
next: voidOrder
- voidOrder:
call: http.post
args:
url: https://ORDER_SERVICE_URL/void
auth:
type: OIDC
result: voidOrderResult
next: finish
- finish:
return: ${reserveStockResult.body}
上記のworkflowが実行されると
- reserveStockが実行され、
https://STOCK_SERVICE_URL/reserve
をcallし、引数のunit分、在庫を抑える処理をします。結果はreserveStockResultに保存されます - switchByReserveStockで在庫を抑えた結果によりフローが分岐されます。成功の場合は、authorizePaymentの決済処理へ進み、在庫不足の場合は注文を取り消すためvoidOrderに進みます
- 同様にauthorizePaymentでも決済処理処理を行い、その結果をauthorizePaymentResultに保存し、switchByAuthorizePaymentへ進みます
- Priceが100000以下の場合は決済処理が成功し、updateOrder->finishへ進みます。決済が失敗すると抑えていた在庫を開放するため、cancelReservedStock->voidOrderへ進みます。
workflowのdeployは下記で行います
PROJECT_ID=XXXXXXX SERVICE_ACCOUNT=XXXXXXX make deploy_workflow
// gcloud workflows deploy workflow-test \
// --source=workflow.yml \
// --service-account=$(SERVICE_ACCOUNT)
deployが成功するとGCPのweb console上でフローがグラフィカルに見れます
これで準備は完了!
実際に実行してみる
workflowはorder serviceの /create
の中で呼び出されます。
まずは正常ケースを実行してみます
curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"unit":20, "price":100}' \
-s https://ORDER-SERVICE-URL/create | jq .
実行後ログを確認してみます
注文の受付->在庫抑え->決済処理->注文更新が順番に実行されていることがわかります
次に在庫不足となるようなrequestをしてみます。
curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \![](https://storage.googleapis.com/zenn-user-upload/sm3y7x8ip27zn2tkra2t0rn79m24)
-H "Content-Type: application/json" \
-d '{"unit":999, "price":100}' \
-s https://ORDER-SERVICE-URL/create | jq .
注文の受付->在庫不足->注文取消が順番に実行されています
次は在庫はOKだが、決済失敗するようなrequestをしてみます。
curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \![](https://storage.googleapis.com/zenn-user-upload/sm3y7x8ip27zn2tkra2t0rn79m24)
-H "Content-Type: application/json" \
-d '{"unit":5, "price":9999999}' \
-s https://ORDER-SERVICE-URL/create | jq .
注文の受付->在庫抑え->決済処理->残高不足->抑えていた在庫キャンセル->注文取り消しが順番に実行されています
なんて簡単…^^
retryするようにしてみる
Microserviceが不安定な場合一定回数、期間retryを試みるようにworkflowに指定をしてみます(retryされるAPIは冪等性を持っている前提)
main:
params: [args]
steps:
- reserveStock:
try:
call: http.post
args:
url: https://STOCK_SERVICE_URL/reserve
body:
"unit": ${args.unit}
"unstable": ${args.unstable}
auth:
type: OIDC
timeout: 10
result: reserveStockResult
retry:
predicate: ${custom_predicate}
max_retries: 10
backoff:
initial_delay: 1
max_delay: 30
multiplier: 2
custom_predicate:
params: [e]
steps:
- what_to_repeat:
switch:
- condition: ${e.code == 500}
return: True
- otherwise:
return: False
上記のようにtry
, retry
を指定することでretryをさせることができます。この場合はhttp status=500の場合retryをします。
- max_retries : 最大retry数
- initial_delay : 初回に遅延させる秒数
- max_delay : retry間の最大遅延秒数
- delay_multiplier : 前回の遅延秒数にこの値をかけて次の遅延秒数が計算される
そのため上記の設定の場合1,2,4,8,16,30,30...の秒数を置いてretryされるようです。
パラメータでunstable=true
と指定すると一定確率でInternalServerError
を返すようにしています。
下記のようにrequestすると一定確率でerrorになりretryされるはずです。
curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"unit":20, "price":100, "unstable":true}' \
-s https://ORDER-SERVICE-URL/create | jq .
ログを確認するとstock service is unstable
が2回発生後、在庫を抑えるのに成功。payment service is unstable
が3回発生後、決済処理成功し、注文も確定されているログが確認できます。
またまたなんて簡単…^^
まとめ
マイクロサービス間のトランザクションを想定してGCPのWorkflowsを試してみました。今まではこのような処理を行う時はアプリケーション側でstate管理をしてごにょごにょする必要があったと思いますが、Workflowsを使って同様のことをすることができました。
pros
- 各APIの実装とyamlを定義するだけで一連のwork flowを実行できる
- responce, http statusによって分岐が可能
- responce, http statusによってretry制御が可能
気になること
- yamlを適切に管理しないとすぐに秘伝化しそう
- ローカルや複数の開発環境からのworkflowはどうするか
- workflow途中からの復旧などはどうするか
- 運用中stepが増減した時の対応
- gRPC対応はいつかされる…?
- web consoleの「実行数」というタブになにも表示されない…?
などなど気になることはいくつかありますが、公開されているAPIを組み合わせてなにかつくるなどいろいろなユースケースに応用ができそうです。
Discussion