GCP Workflows試してみた

10 min read読了の目安(約9000字

TL;DR

  • microservices間のトランザクションを想定してGCPのWorkflowsを試してみた
  • responceやHTTP statusでworkflowを分岐してみた
  • HTTP statusでのretryを試してみた

Workflows

サーバーレス ワークフローを使用して、Google Cloud と HTTP ベースの API サービスをオーケストレートします。

https://cloud.google.com/workflows

Workflowsを使うとyamlの定義に沿ってステップごとに適切にAPIをcallすることができるとのことなので下記のブログをものすごく参考にさせてもらいWorkflowsを試してみました。

https://medium.com/google-cloud-jp/gcp-saga-microservice-7c03a16a7f9d

Micorservicesで困ること

Micorservicesで困ることの1つにトランザクションの管理があります。
ECサイトの商品注文処理を例にするとざっくり注文処理の中には、

  1. 在庫を抑える
  2. 決済処理
  3. 注文確定

などがあります。
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
    があるとして、フローは下記のようにしてみようと思います
  1. 注文を受け付ける
  2. 在庫を抑える
    • ただし在庫が不足している場合は注文を取り消す
  3. 決済処理
    • ただし決済が失敗(残高不足、無効なクレジットカードとか)した場合、抑えていた在庫を開放 + 注文を取り消す
  4. 注文確定

下準備

サンプルのcodeは下記になります

https://github.com/ogataka50/workflows-test
サンプル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

https://github.com/ogataka50/workflows-test/blob/main/workflow.yml.template
上記のworkflow.yml.templateをもとに各ServiceのURLを書き換えてworkflow.ymlを作成します
まずは各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が実行されると

  1. reserveStockが実行され、https://STOCK_SERVICE_URL/reserveをcallし、引数のunit分、在庫を抑える処理をします。結果はreserveStockResultに保存されます
  2. switchByReserveStockで在庫を抑えた結果によりフローが分岐されます。成功の場合は、authorizePaymentの決済処理へ進み、在庫不足の場合は注文を取り消すためvoidOrderに進みます
  3. 同様にauthorizePaymentでも決済処理処理を行い、その結果をauthorizePaymentResultに保存し、switchByAuthorizePaymentへ進みます
  4. 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を組み合わせてなにかつくるなどいろいろなユースケースに応用ができそうです。

参考