GCP でマイクロサービス Saga パターン編

Shingo
google-cloud-jp
Published in
27 min readDec 22, 2020

--

この記事は Google Cloud Japan Customer Engineer Advent Calendar 2020 の 22 日目の記事です。

こんにちわ。今日はマイクロサービスの設計パターンの一つ Saga パターン と GCP での実装例を紹介します。マイクロサービス入門者の方に向けた記事として、できるだけわかりやすく書いてみたいと思います。

TL;DR

  • Saga パターンはマイクロサービスでトランザクショナルな処理を実現するアーキテクチャ パターン
  • GCP では、Cloud Run や Cloud Workflows などのサーバーレス サービスを利用して、マイクロサービスを効率よく構成可能
  • GitHub にすぐに試せるサンプルコードがあります

マイクロサービス アーキテクチャの課題

マイクロサービスの概念は以前からありますが、最近では Kubernetes やサービスメッシュの文脈でマイクロサービスについて言及されることも多いので、耳馴染みのある方も増えているのではないでしょうか。

しかしマイクロサービスはメリットも大きいのですが、いわゆる分散型アーキテクチャになるため、従来のアーキテクチャ(モノリス型)には見られなかったさまざまな課題がでてきます。これらの答えのひとつとして Kubernetes や、Istio のようなサービスメッシュが登場し、多くのファンを獲得してますが、これらは主にデプロイのサポートや運用軽減など非機能面の課題についての解決策といえます。

それに対して、マイクロサービス アーキテクチャの機能面の部分には注目が集まりにくいと感じています。Kubernetes などの機能だけでは解決が難しい部分です。

マイクロサービスのパターン

もちろんこのような課題についても、パターン化されたプラクティス が用意されています。その一つが Sagaパターン です。

こちらはマイクロサービスの構成イメージです。紫色で表されている各マイクロサービスはそれぞれ別々のデータストアを持っており、いわゆる Database per Service の構成になっています。代表的なマイクロサービスのアーキテクチャですね。

ここでお気づきでしょうか?
各サービスごとにデータストアを用意するのは、マイクロサービスとして設計、運用するための要件になってきます。ですが、この構成だからこそ発生するトランザクションについての課題があります。

例えば EC アプリケーションでは、注文して、決済して、在庫処理する、というのがよくある流れかと思います。これらの処理に対してひとつのデータストア(RDBMSなど)を使っていれば、トランザクションでアトミックな処理としてまとめることができます。注文後に在庫がない場合なども、注文自体を取り消すことが簡単です。

しかし上記のようなマイクロサービス アーキテクチャではどうでしょう?注文サービス、決済サービスなどは別のマイクロサービスとして分けて作る場合も多いかと思います。各サービスは別々のデータストアを使っていますし、お互いが何をやってるか知りません。通信の窓口は決められたAPI だけです。

たとえ各サービスが同じデータストアに接続し、別々のスキーマを使っていたとしても、他のサービスのスキーマを同じトランザクションのスコープに入れてしまうと、サービスの内部情報を共有することになります。それでは独立した開発やデプロイに干渉するなど、マイクロサービスのメリットを破壊してしまいます。

ここで登場するのが Saga パターンです。マイクロサービスの分散アーキテクチャを維持しつつも、RDBMS などのトランザクションを使わず複数のサービスと連携したトランザクショナルな処理を実現します。使うのは各マイクロサービス に用意された API です。エラーなどが起こって先に進められない場合、補償トランザクションと呼ばれるフローでデータの整合性を維持します。

Saga パターンのサンプルコード

Google Cloud の GitHub に Saga パターンを GCP で実装したサンプルアプリケーションが公開されています。用意されている Saga パターンとしては 2 種類あります。

  1. コレオグラフィベースの非同期 Saga パターン
  2. オーケストレーションベースの同期 Saga パターン

用語を説明しておきます。

  • コレオグラフィ
    各マイクロサービスが自律的に他サービスと協調しながら、処理を進めます。指示や管理をする調整役はいません。コレオグラフィという言葉はもともと振り付けという意味で、バレエダンサーのようにそれぞれがばらばらに動きながらも協調しつつ、ひとつのバレエ劇を構成するところからきています。
  • オーケストレーション
    指示や管理をする調整役がいる形です。各サービスの間に入り、指示をしたりそのレスポンスを解釈して他のサービスへの指示をします。オーケストラの指揮者を想像してるとわかりやすいですね。
  • 同期と非同期(ここでの意味)
    各マイクロサービス 間、及びマイクロサービスとクライアント/外部サービス間の処理に関する方式です。
    同期はリクエストの受付後、すぐに依頼された処理を実行、その結果を返します。クライアントにすぐに結果を届けたい場合に適しています。しかし結果を受け取るまで待たされる(ブロックされる)ため、時間がかかる処理には向きません。
    非同期はリクエストの受付後、処理を別ジョブで実行、結果は別途確認します。時間や負荷がかかる処理をジョブとしてバックエンドに任せることができ、かつジョブの実行中もクライアントは次のリクエストを送ることができる(ブロックしない)ため、スケーラビリティを上げることが可能です。

コレオグラフィ/オーケストレーションと、同期/非同期は任意に組み合わせが可能です。また同期と非同期が混在することもあり、あるマイクロサービス間は非同期で、別の箇所では同期、というような組み合わせもあります。

今回はこの GitHub のサンプルコードを使って、2. オーケストレーションベースの同期 Saga パターン をやってみます。
サンプルコードの README.md では、「Synchronous orchestration」として紹介されているパターンになります。

実装するユースケース
完成イメージはこちら

ここでは以下の3つのサービスが登場します。

  • Order 処理サービス(order-processor-service)
    顧客の注文インタフェースとなるサービス
  • Order サービス(order-service-sync)
    注文を処理するサービス
  • Customer サービス(customer-service-sync)
    顧客と Credit を管理するサービス

実行プラットフォームとしてはすべて Cloud Run を使っています。そしてOrder サービスと Customer サービスのオーケストレータとして Cloud Workflows がいて、Saga パターンを実現しています。点線の枠内がオーケストレーションの範囲です。

Cloud Workflows はサーバーレスでワークフロー機能を提供する新しいサービスです。
詳しくは Advent Calender 12日目 Workflowsで実現するサーバーレスなワークフロー の記事をご覧ください。

ユースケースのシナリオ

ユースケースとしてはシンプルなシナリオになっていて、とある商品の購入アプリケーションを実現します。とある商品ではわかりにくいので、季節柄、ケーキにしてしまいましょう🍰

トランザクショナルな連携が求められるのはこのケーキの注文機能を提供する Order サービス、決済機能を提供する Customer サービスの部分です。事前に顧客には使用できる Credit の上限を割当てます(例 10000 Credit)。
ここではケーキ1つの購入に対して、Credit 100 が必要です。例えばケーキ10個購入する場合は、消費 Credit 1000 という感じです。

  1. 顧客は Order 処理サービスに対して、カスタマー IDとケーキの購入数を指定して注文をします。
  2. オーダー処理サービスは渡された引数でワークフローを実行します。
  3. ワークフローはまず、Order サービスを呼び出します。Order サービスはリクエストを受け取って、Order の状態を保留状態(pending)にしてOrder ID をワークフローに返します。
  4. 次にワークフローは Customer サービス(/api/v1/customer/reserve)に処理を渡します。Customer サービスはデータストアから現在の Credit の値などを引き出し、使用後の Credit の量と上限を比較します。
  5. Customer サービスは購入に問題なければ Credit をその分消費してデータストアに記録し、Order サービスに通知、Order サービスは Order を Accepted(受諾) に変更します。上限を超えていて購入できなければ、Order サービスは Order を Rejected(拒否)に変更します。
  6. ワークフローは Order サービスからレスポンスを受け取り、Order 処理サービスを経由してユーザにレスポンスを返します。

これらは同期的なフローになっているため、顧客はすぐに結果を受け取ることができます。

実装

それでは GitHub の手順に従って、実装を試してみましょう。

GitHub の README.md に手順が載っていますが、1のコレオグラフィベースの非同期 Saga パターンと、2 のオーケストレーションベースの同期 Saga パターンが含まれていますので、2 のパターンのみを抜き出してやってみます。(なお各マイクロサービスの運用はそれぞれ別チームで実施することが多いのですが、今回はもくもくと一人で作っていきます。)

それでは早速はじめていきます。

  1. 各マイクロサービス の準備
    この赤枠で囲まれたサービスを一気に準備します。

これ以降は作業環境として、Cloud SDK がインストール、環境設定されたシェル(bash 想定)と、Google Cloud コンソールを使っていきます。ローカルに Cloud SDK の環境を準備していない方は Cloud コンソール上のCloud Shell をご利用ください。

1–1. サービスの有効化
今回の構成で利用する以下のサービスを有効化します。

  • Cloud Run
  • Cloud Workflows
  • Cloud Firestore Datastoreモード
  • Cloud Build(コンテナのビルド用)

GCP では利用したいサービスについて有効化を明示的に実施しますが、今回は下記コマンドで一気に有効化してしまいます。

gcloud services enable run.googleapis.com workflows.googleapis.com datastore.googleapis.com cloudbuild.googleapis.com

1–2. ソースの取得
お手元に GitHub のソースをご用意ください。ホームディレクトリ直下にclone します。

cd $HOME
git clone https://github.com/GoogleCloudPlatform/transactional-microservice-examples

1–3. 作業シェルの設定
シェル変数 PROJECT_ID をセットしておきます。また、gcloud コマンドにもプロジェクトを設定しておきます。

PROJECT_ID=利用するプロジェクトID
gcloud config project $PROJECT_ID

1–4. データストアの作成

最初にデータストアとなる、Cloud Firestore Datastore モードを用意します。今回はより汎用性が高い使い方ができる Datastore モードを選択しています。(ただし一度ネイティブモードで使っている場合は、Datastore モードに切り替えができないのでご注意ください。作業用に別プロジェクトを準備される方が簡単だと思います。)

Cloud Firestore に最初にアクセスすると下記画面が表示されるので、Datastore モードを選びましょう。

リージョンは nam5(アメリカ)を選択し、作成します。

作成できたら gcloud コマンドで index を作成します。index の作成に使う設定ファイルは、コレオグラフィベースの同期 Saga パターンのときに利用する、event-publisher サービスの直下にあります。

cd $HOME/transactional-microservice-examples/services/event-publisher
gcloud datastore indexes create index.yaml --quiet

なお、今回はすべてのサービスのデータストアに Cloud Firestore のDatastore モードを使います。Datastore にはスキーマ(テーブル)に似たKind(日本語表示では種類)という概念がありますが、

  • Order サービス用に Order
  • Customer サービスで Customer

上記のように、Kind を完全に分けた状態で利用します。実際のマイクロサービスの設計では、それぞれのサービスに適した別種類のデータストアを利用する場合も多いかと思います。

1–5. マイクロサービスのビルド、デプロイ

対象のディレクトリは services/ 配下の3つです。

  • order-sync/
  • customer-sync/
  • order-processor/

なお後ほど各サービスのアクセスログを見たいため、Dockerfile に手を加えて、gunicorn の引数を追加しています。

-CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
+CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app --access-logfile - --capture-output

必要な方はそれぞれのディレクトリの下の Dockerfile で、この変更をしてください。一気に全サービス分変えたい場合はこちら。

cd $HOME/transactional-microservice-examples/services
perl -pi -e ‘s/main:app$/main:app --access-logfile - --capture-output/’ */Dockerfile

それでは Cloud Build でコンテナイメージをビルドして、Googld Container Registory に登録、そのまま Cloud Run にデプロイしてしまいます。

  • Order サービスのデプロイ
cd $HOME/transactional-microservice-examples/services/order-sync
gcloud builds submit --tag gcr.io/$PROJECT_ID/order-service-sync
gcloud run deploy order-service-sync \
--image gcr.io/$PROJECT_ID/order-service-sync \
--platform=managed --region=us-central1 \
--no-allow-unauthenticated
  • Customer サービスのデプロイ
cd $HOME/transactional-microservice-examples/services/customer-sync
gcloud builds submit --tag gcr.io/$PROJECT_ID/customer-service-sync
gcloud run deploy customer-service-sync \
--image gcr.io/$PROJECT_ID/customer-service-sync \
--platform=managed --region=us-central1 \
--no-allow-unauthenticated
  • Order 処理サービスのデプロイ
cd $HOME/transactional-microservice-examples/services/order-processor
gcloud builds submit --tag gcr.io/$PROJECT_ID/order-processor-service
gcloud run deploy order-processor-service \
--image gcr.io/$PROJECT_ID/order-processor-service \
--platform=managed --region=us-central1 \
--no-allow-unauthenticated \
--set-env-vars "PROJECT_ID=$PROJECT_ID"

Cloud コンソールで確認すると、3 つのサービスがデプロイされています。

1–6. サービスアカウントの作成と設定

各マイクロサービスを動かしている Cloud Run を呼び出すためのサービスアカウントを作成して、権限を付与します。

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"

Order サービス呼び出しの権限付与

SERVICE_NAME="order-service-sync"
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

Customer サービス呼び出しの権限付与

SERVICE_NAME="customer-service-sync"
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

Order サービスと Customer サービスの Cloud Run のコンソールに移動して、[権限]タブで確認すると、下記のようになっているはずです

1–7. Cloud Workflows のワークフローのデプロイ

Customer サービスと、Order サービスに割り当てられている Cloud Run のURL を取得します。

SERVICE_NAME="customer-service-sync"
CUSTOMER_SERVICE_URL=$(gcloud run services list --platform managed \
--format="table[no-heading](URL)" --filter="SERVICE:${SERVICE_NAME}")
SERVICE_NAME="order-service-sync"
ORDER_SERVICE_URL=$(gcloud run services list --platform managed \
--format="table[no-heading](URL)" --filter="SERVICE:${SERVICE_NAME}")

Cloud Workflows のワークフローの設定ファイルを書き換えます。用意してあるテンプレートの YAML ファイル例がこちらです。steps に注目して軽く目を通すと、処理の流れがわかるかと思います。ユースケースのシナリオも合わせて確認するとわかりやすいです。

テンプレートを書き換えて設定ファイルを作成します。置き換える値は、Order サービスの URL と Customer サービスの URL です。

cd $HOME/transactional-microservice-examples/services/order-processor
cp order_workflow.yaml.template order_workflow.yaml
sed -i "s#ORDER-SERVICE-URL#${ORDER_SERVICE_URL}#" order_workflow.yaml
sed -i "s#CUSTOMER-SERVICE-URL#${CUSTOMER_SERVICE_URL}#" order_workflow.yaml

ワークフローをデプロイします。

gcloud beta workflows deploy order_workflow \
--source=order_workflow.yaml \
--service-account=$SERVICE_ACCOUNT_EMAIL

デプロイが完了すると、Cloud Workflows の管理画面に order_workflow というワークフローがデプロイされているのが確認できます。

2. マイクロサービス Saga パターンのテスト
こちらの作業もシェルを中心に行っていきます。

2–1. 顧客の登録
Customer サービスに直接リクエストを発行して、ユーザと Credit の上限を登録します。ここでは顧客自身がサインアップするイメージで登録します。

customer_id: customer02を、Credit 上限: 10000 で登録します。
Cloud Run は --no-allow-unauthenticated オプションをつけてデプロイしているので、Authorization ヘッダに Bearer トークンを指定しています。

curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"customer_id":"customer02", "limit":10000}' \
-s ${CUSTOMER_SERVICE_URL}/api/v1/customer/limit | jq .

レスポンス

{
"credit": 0,
"customer_id": "customer02",
"limit": 10000
}

2–2. 注文

まずOrder処理サービスのURLを取得して、変数に入れておきます。

SERVICE_NAME="order-processor-service"
ORDER_PROCESSOR_URL=$(gcloud run services list --platform managed \
--format="table[no-heading](URL)" --filter="SERVICE:${SERVICE_NAME}")

customer02 が、ケーキ(商品)を10 個注文します。

curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"customer_id":"customer02", "number":10}' \
-s ${ORDER_PROCESSOR_URL}/api/v1/order/process | jq .

レスポンス

{
"customer_id": "customer02",
"number": 10,
"order_id": "b0cf6c98-647c-4815-87f0-10edf8c86485",
"status": "accepted"
}

accepted で返ってきました。無事に成功して、ケーキが10個買えたようです。Saga パターンがあるからこそ、Customer サービスの結果と Order サービスの状態変更がシンプルに連携できています。

2–3. マイクロサービスのログを Cloud Logging で確認

マイクロサービスのログを確認してみます。
Cloud Logging に移動して、ログエクスプローラのクエリビルダーに下記を入力、クエリを実行します。

resource.type = "cloud_run_revision"
resource.labels.location = "us-central1"
severity>=DEFAULT

表示されたログを確認します。

順番通りにオーケストレーションされながら、各マイクロサービス にアクセスされていることがわかると思います。

  1. Orderサービスで注文を作成
    /api/v1/order/create
  2. Customerサービスで注文を受け付けることができるか確認
    /api/v1/customer/reserve
  3. Orderサービスで最終的な状態を更新
    /api/v1/order/update

ここで補足なのですが、Cloud Workflows 自体はリクエストを受け付けるとすぐにジョブ名を返却して、非同期でワークフローのジョブ処理を開始します。つまり実は非同期処理が入っています。そのため、Order 処理サービスのコードでは 5 秒おきにジョブの終了をチェックしています。タイミングによってはレスポンスの出力に数秒かかることがありますが、ログで見て分かる通り実際の処理は、1 秒程度で完了しています。
なお、最初に curl でアクセスした Order 処理サービスのアクセスログが最後に出ているのは、最終的にレスポンスを返した時間を記録するようになっているからだと思われます。

2–4. 注文(失敗)

現在の Credit を確認してみます。さっき10個ケーキを買って1000 Credit を使いました。

curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"customer_id":"customer02"}' \
-s ${CUSTOMER_SERVICE_URL}/api/v1/customer/get | jq .

レスポンス

{
"credit": 1000,
"customer_id": "customer02",
"limit": 10000
}

customer02 で、ケーキを95 個注文します。95 個のケーキを買うためには9500 の Credit の消費が必要ですが、上限の 10000 を超えてしまうため、失敗するはずです。

curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{"customer_id":"customer02", "number":95}' \
-s ${ORDER_PROCESSOR_URL}/api/v1/order/process | jq .

レスポンス

{
"customer_id": "customer02",
"number": 95,
"order_id": "e340189b-b735-45f0-a852-aa17251c717a",
"status": "rejected"
}

無事に失敗しましたね。
Customer サービスが accepted を false で返しているため、Orderサービスは rejected になっています。order_flow.yaml の 24行目付近の条件分岐通りに動いていますね。
ご興味がある方は、ログも確認してみてください。

3. プロダクション利用時の注意点

Cloud Workflows は 2020 年 12 月 現在 ベータ版となっています。
また同時に実行できるワークフローの最大数などの各種制限もありますので、下記ページもよく確認してください。
Quotas and limits | Workflows

まとめ

マイクロサービス間でトランザクションを実現する Saga パターンがシンプルに実現できることがご理解いただけたかと思います。小さいシナリオだったので、応用しやすいのではないでしょうか。

今回ご紹介できなかったコレオグラフィベースの非同期 Saga パターンは、よりスケーラブルな構成に対応できるだけでなく、トランザクショナルにメッセージを送信する outbox パターンなどの実装も含まれており、設計の参考になります。

また今回はコマンドベースで API を利用しましたが、同じリポジトリにはこれらのサービスの利用するための Flutter for Web ベースのフロントエンド も準備してあります。簡単に利用できるようになっているので、ぜひお試しください。

明日の記事は Swacchi さんの「Firebase Authentication & Google Identity Platform 入門」です。お楽しみに。

--

--