トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

Cloud Buildからpsqldefを使用してCloud SQL for PostgreSQLにマイグレーションする

この記事はトレタ Advent Calendar 2022の15日目の記事です。

はじめに

こんにちは、サーバーサイドエンジニアの @shiroemons です。

前回の記事に書いた通り、認定資格を取得してからGoogle Cloudを頻繁に活用するようになりました。

tech.toreta.in

現在のプロジェクトでは、Cloud Buildを用いてdocker buildやCloud Runへのデプロイを行っています。

また、データベース(Cloud SQL)へのマイグレーションもCloud Buildから行っています。

今回は、Cloud Buildからpsqldefというマイグレーションツールを使用して、Cloud SQL for PostgreSQLにマイグレーションする方法を紹介します。

ただし、Cloud SQL for PostgreSQLなどの設定は完了している前提のため、省略しています。

psqldefとは

SQLで羃等にDBスキーマ管理ができるツール「sqldef」のPostgreSQL用のツールです。

github.com

PostgreSQL用の他に、MySQL用のmysqldefやSQLite3用のsqlite3defなども存在します。

sqldefについての細かい説明は、ここでは省略します。

Cloud Buildとは

Cloud Buildは、Google Cloud 上でビルドを実行するサービスです。

Cloud Buildでは、ビルド構成ファイルと呼ばれる設定ファイルにビルドやデプロイの方法(指示)を記述します。記述した指示に基づいてタスクを実行し、ビルドやデプロイを行います。

Cloud SQLへのマイグレーションで使用した構成ファイルの紹介と説明をします。

Cloud Buildの構成ファイル

Cloud SQLへのマイグレーションで使用したCloud Buildの構成ファイルはこちらになります。

  • cloudbuild-migration.yaml
steps:
  - name: gcr.io/cloud-builders/wget
    args:
      - 'https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64'
      - '-O'
      - ./cloud_sql_proxy
    id: cloud_sql_proxy download
  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - |
        chmod +x ./cloud_sql_proxy && ./cloud_sql_proxy --version
    id: cloud_sql_proxy version
    entrypoint: bash
  - name: gcr.io/cloud-builders/wget
    args:
      - >-
        https://github.com/k0kubun/sqldef/releases/latest/download/psqldef_linux_amd64.tar.gz
    id: psqldef download
  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - |
        tar -zxvf psqldef_linux_amd64.tar.gz && ./psqldef --version
    id: psqldef version
    entrypoint: bash
  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - >
        ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT & sleep $_SLEEP_SEC;
        ./psqldef --dry-run --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME
    id: psqldef dry-run
    entrypoint: bash
    secretEnv:
      - DATABASE_PASS
  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - >
        ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT & sleep $_SLEEP_SEC;
        ./psqldef --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME
    id: psqldef execute
    entrypoint: bash
    secretEnv:
      - DATABASE_PASS
substitutions:
  _SCHEMA_FILE: ./schema.sql
  _DATABASE_HOST: 127.0.0.1
  _DATABASE_PORT: '5432'
  _DATABASE_NAME: db_name
  _DATABASE_USER: db_user
  _DATABASE_PASSWORD_KEY: database_password
  _INSTANCE_REGION: asia-northeast1
  _INSTANCE_ID: database
  _INSTANCE_CONNECTION_NAME: '${PROJECT_ID}:${_INSTANCE_REGION}:${_INSTANCE_ID}'
  _SLEEP_SEC: '5'
availableSecrets:
  secretManager:
    - versionName: >-
        projects/$PROJECT_ID/secrets/${_DATABASE_PASSWORD_KEY}/versions/latest
      env: DATABASE_PASS

(代入変数部分を変更して動作することを確認しています。)

各ビルドステップの説明

  • よく使用するフィールドについて簡単に説明します。
    • name: クラウドビルダーの指定(Docker..etc)
    • args: ビルダーに渡す引数のリスト
    • id: ビルドステップに対して一意の識別子
    • entrypoint: エントリポイントを指定 (bash etc)
    • secretEnv: Cloud KMS暗号鍵を使用して暗号化された環境変数のリスト
  • 詳しい説明は、 ビルド構成ファイルの構造 を参照ください。

ステップ1: Cloud SQL Proxyのダウンロード

  - name: gcr.io/cloud-builders/wget
    args:
      - 'https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64'
      - '-O'
      - ./cloud_sql_proxy
    id: cloud_sql_proxy download
  • Cloud SQLに接続する際、Cloud SQL Proxyを使用して接続します。
    • Cloud SQL Proxy を使用するには、Cloud SQL Admin API を有効にする必要があります。
    • こちらから有効にできます。
  • Cloud SQL Proxyをwgetでダウンロードします。

ステップ2: Cloud SQL Proxyの実行権限付与とCloud SQL Proxyのバージョン確認

  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - |
        chmod +x ./cloud_sql_proxy && ./cloud_sql_proxy --version
    id: cloud_sql_proxy version
    entrypoint: bash
  • ダウンロードしたCloud SQL Proxyに実行権限を付与します。
  • どのバージョンを使用したかわかるようにバージョンを確認します。
  • ファイルの存在確認の意図もあります。

ステップ3: psqldef の最新版をダウンロード

  - name: gcr.io/cloud-builders/wget
    args:
      - >-
        https://github.com/k0kubun/sqldef/releases/latest/download/psqldef_linux_amd64.tar.gz
    id: psqldef download
  • マイグレーションに必要なpsqldef(最新版)をwgetでダウンロードします。

ステップ4: psqldef の解凍とpsqldef のバージョン確認

  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - |
        tar -zxvf psqldef_linux_amd64.tar.gz && ./psqldef --version
    id: psqldef version
    entrypoint: bash
  • ダウンロードしたpsqldefは圧縮されているため解凍します。
  • どのバージョンを使用したかわかるようにバージョンを確認します。
  • ファイルの存在確認の意図もあります。

ステップ5: psqldef の dry-run 実行

  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - >
        ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT & sleep $_SLEEP_SEC;
        ./psqldef --dry-run --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME
    id: psqldef dry-run
    entrypoint: bash
    secretEnv:
      - DATABASE_PASS
  • Cloud SQLに接続する場合、Cloud SQL Proxyを先に起動しておく必要があります。
  • Cloud SQL Proxyの起動に一定時間かかるため sleep を入れています。
  • 設定に必要な値は、すべて代入変数で定義しておきます。
    • 代入変数を用いることでビルド構成ファイル自体の変更をしなくてもよくなります。
  • psqldefを本実行する前に、dry-runで実行してエラーがないことを確認しておきます。
  • DBのパスワードは、Secret Managerに保存して使用しています。

ステップ6: psqldef の 実行

  - name: gcr.io/cloud-builders/gcloud
    args:
      - '-c'
      - >
        ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT & sleep $_SLEEP_SEC;
        ./psqldef --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME
    id: psqldef execute
    entrypoint: bash
    secretEnv:
      - DATABASE_PASS

内容は、dry-runとほぼ同じです。psqldefのdry-runオプションを外した内容です。

代入変数(substitutions)の説明

substitutions:
  _SCHEMA_FILE: ./schema.sql
  _DATABASE_HOST: 127.0.0.1
  _DATABASE_PORT: '5432'
  _DATABASE_NAME: db_name
  _DATABASE_USER: db_user
  _DATABASE_PASSWORD_KEY: database_password
  _INSTANCE_REGION: asia-northeast1
  _INSTANCE_ID: database
  _INSTANCE_CONNECTION_NAME: '${PROJECT_ID}:${_INSTANCE_REGION}:${_INSTANCE_ID}'
  _SLEEP_SEC: '5'
代入変数名 説明
_SCHEMA_FILE psqldef でマイグレーションするスキーマファイル
_DATABASE_HOST DBのホスト
_DATABASE_PORT DBのポート
_DATABASE_NAME DB名
_DATABASE_USER DBのユーザー名
_DATABASE_PASSWORD_KEY DBのパスワードを Secret Manager に保存した際のシークレット名
_INSTANCE_REGION Cloud SQLのインスタンスのリージョン
_INSTANCE_ID Cloud SQLのインスタンスID
_INSTANCE_CONNECTION_NAME Cloud SQLの接続名(環境変数や代入変数で値を作成する)
_SLEEP_SEC Cloud SQL Proxyの起動を待つスリープ(秒数を指定する)

数値の指定の場合、クォートが必要です。そのため、DBのポートやスリープの秒数にシングルクォートを付けています。

availableSecretsの説明

availableSecrets:
  secretManager:
    - versionName: >-
        projects/$PROJECT_ID/secrets/${_DATABASE_PASSWORD_KEY}/versions/latest
      env: DATABASE_PASS
  • availableSecretsは、Cloud BuildでSecret Managerのシークレットを使用する時のフィールドです。

DBのパスワードを、Secret Managerに保存しており、そこから最新の設定内容を取得するようにしています。

Cloud Build 実行に必要なロール

  • 今回のCloud Build実行には以下の2つのロールが必要です。
    • Cloud SQL クライアント
    • Secret Manager のシークレット アクセサー

実行ログ

構成ファイルの代入変数の値を適宜変更し、実行すると以下のように成功します。

実行ログ

振り返り

Cloud Buildでsqldefを用いてマイグレーションする方法をネットで探しても出てこず苦労しました。

Cloud SQL ProxyとpsqldefをwaitForを使用して分けて実行させてみたりと試行錯誤の連続でした。

Cloud BuildでCloud SQL Proxyを用いてCloud SQLへの接続がどうしてもうまく行かず、Google Cloudのサポートも利用しました。

Cloud SQL Proxyの起動には、少し時間を要するためスリープを入れることで接続できない問題は解決しました。

さいごに

この記事が、Cloud Buildからpsqldefもしくはmysqldefを使用してマイグレーションを検討している方の助けになれば幸いです。

次回の トレタ Advent Calendar 2022 の22日目は、Cloud Buildの結果をSlackに通知する方法を紹介したいと思います。

トレタではエンジニアの募集を全方位で行なっております。

コロナ禍を乗り越えた飲食店の新しい姿を探求する仲間をお待ちしております。

corp.toreta.in

G.I.G プログラムの参加とProfessional Cloud Developerの受験記録

この記事はトレタ Advent Calendar 2022の8日目の記事です。

はじめに

こんにちは、サーバーサイドエンジニアの @shiroemons です。

今回は Google Cloud主催の G.I.G プログラム という招待制の特別トレーニングプログラムに参加し、Professional Cloud Developer認定試験を受験しました。

G.I.G プログラムの内容から受験の体験について記そうと思います。

G.I.G プログラムについて

G.I.G プログラムとは?

G.I.G. は Google Cloud Innovators Gym の略称です。 Google Cloud様が主催する、各業界をリードするエンジニアに向けた、Google Cloud Platform の特別招待制トレーニングプログラムです。

対象認定資格

G.I.G プログラムで対象となるのは、以下の3つの認定資格になります。

私は、 Professional Cloud Developer を選択しました。

Professional Cloud Developerは、Google Cloud (旧称 GCP) におけるアプリケーション開発者向けの認定資格です。

プログラム内容

  • 全3回のGoogle Cloud のエンジニアによるセッション
  • Coursera を使用した学習コースの無料提供
  • Google Spaces を利用したGoogle Cloud エンジニアによる学習サポート
  • Google Cloud 認定資格取得のサポート

プログラムの修了条件

  • 特別セッションの受講
  • Coursera 規定コース5つ以上の受講完了
  • Google Cloud 認定資格の合格報告

セッション

セッションは、全てGoogle Meetでのオンライン開催でした。

  • 特別セッション
  • 補習セッション

Coursera

Courseraとは、オンライン学習プラットフォームです。

Google Cloud が提供する Coursera の全コースを無料で受講できました。

取得する資格に応じて5つ以上のコースを受講する必要がありました。

各コースは、解説動画を視聴し、Qwiklabs1 と連携したハンズオンを行い、理解度チェックテストを受けるのサイクルでした。 各コースは、ほぼ日本語対応がされていましたが、解説動画の音声は英語で日本語は字幕でした。字幕は別途、テキスト化されて助かりました。

試験対策

G.I.G プログラムのカリキュラムをこなすだけで合格できるものでもないため、自己学習は必須です。

認定試験について

試験の実施方法

試験は、以下のどちらかを選択することができます。

  • 遠隔地からオンライン監視試験
  • テストセンターで行うオンサイト監視試験

私は、試験会場に行くのが面倒と思い、オンライン監視試験を選択し、試験の予約をしました。

試験当日(2022年7月6日)

試験日が近づくにつれ、試験環境が自宅では適さないことがわかり、試験当日はオフィス出社し会議室を借りて受験しました。 (懇親会で聞いた話ですが、自宅のお風呂場で受験した方もいたそうです。)

オンライン監視試験は受験専用のアプリケーション(ブラウザー)を手元のマシンにインストールして行います。 不正防止のため、モニター出力無効化、専用アプリ以外は表示できないなど制限される仕様となっていました。

試験予定時間の10分前くらいに開始ボタンが表示されました。

試験開始前

試験開始10分前に開始ボタンが表示

開始ボタンをクリックすると専用アプリが立ち上がりました。 しばらくすると、画面が更新されチャットベースでの英語でのコミュニケーションが始まりました。

普段ならDeepL翻訳など使って理解できるけど、不正防止のため専用アプリが立ち上がっているため翻訳サイトが使えなかったためあたふたしました...

途中からGoogle翻訳されたような日本語になりました。

以下の確認がありました。

  • 身分証明書の提示
  • 試験環境の確認、Webカメラで部屋を映しながら確認
  • 質問事項の質疑応答
    • 部屋のドアはどこにつながっているのか
    • 携帯はどこにあるのか

など指示があるのでそれに従います。

無事に確認が取れたようで試験開始となりました。

試験時間は 120 分、問題数は 60 問

1問1問丁寧に問題を確認し進めていきました。すぐにわからない問題は1度スキップして最後まで進みました。

最後まで解いた後に、スキップした問題を解きつつ、すでに回答した問題の見直しを行いました。

試験結果

時間になり試験が終わると...

「合格」!!!!

7月14日に無事デジタル認定証が届きました🎉

認定試験後

G.I.G. プログラム事務局に認定資格の合格報告が必要だったため、デジタル認定証が届くまでドキドキでしたが無事に合格報告できました。

その後、G.I.G. プログラム 第4期オンライン修了式と懇親会に参加しました。

無事に G.I.G プログラムを修了することができました。

振り返り・まとめ

  • 特別セッション
    • Google Cloudの社員の方が行ってくれたため、基本的なことから最新情報まで教えていただき、非常に貴重な体験でした。
    • ハンズオンでも実際のGoogle Cloudをさわって動くところが学べたので良かったです。
  • Coursera
    • Qwiklabsで実際のGoogle Cloudの環境にふれられたのが良かったです。
    • もともと英語のコンテンツを日本語化したものでしたので、解説動画は英語音声だったので、なかなか頭に入らず苦労しました...
  • 試験対策マニュアル
    • 的確すぎた。ありがとう。
  • 書籍
    • あの1冊でGoogle Cloudの基本的な部分は網羅されていたので学びが大きかった。
  • 試験
    • オンライン試験は大変だったので、次受けるときはオフサイト試験にします。

無事に Professional Cloud Developer に合格できてよかった。

無事に G.I.G. プログラム 修了できてよかった。

www.credential.net

さいごに

Professional Cloud Developer認定資格を取得したあとは、Google Cloud をさわることが増え、Cloud Build・Cloud Run・Cloud SQLを活用して開発を進めています。

スムーズに開発が進めることができているのも、G.I.G プログラムに参加したことが大きく影響しています。招待していただき、本当にありがとうございました!

トレタ Advent Calendar 2022の15日目は、Cloud Buildについてなにか書ければなと思っています。

なお、トレタではエンジニアの募集を全方位で行なっております。

コロナ禍を乗り越えた飲食店の新しい姿を探求する仲間をお待ちしております。

corp.toreta.in


  1. Qwiklabsとは、Google Cloud のラボ環境を一時的に払い出してくれて、ハンズオンができるサービスです。

飲食店モバイルオーダー・トレタO/Xの開発反省会

こんにちは、トレタのモバイルオーダー事業でプロダクトマネージャーを行なっている北川です。

こちらはトレタアドベントカレンダー2日目の記事です。この記事では2022年の振り返りとして、今年の前半に取り組んだモバイルオーダー・トレタO/Xでの決済トラブルについてどう対応していったのかを記そうと思います。

toreta.in

決済でのトラブルというのは基本的に起こしてはならないものであり、それを記事にすることは憚られましたが、自分が苦闘しているなかで同様に決済への試みをしている記事を大いに参考にさせていただいたので、自分も誰かの助けになればと思いこの記事を公開します。

あらためて、導入店の飲食店様と来店してご利用いただいていたお客様には、多大なご迷惑をおかけしたことをこの場を借りてお詫び申し上げます。

トレタO/Xの決済の概要

まずはトレタO/Xでの決済について説明します。 トレタO/Xでは店員を介さずにユーザーが自分のスマホから飲食代金を決済することが可能です。 ユーザーは飲食後にトレタO/Xでお会計画面を開き、支払い方法として、オンライン決済オフライン決済かを選択することができます。

オフライン決済は従来の決済のように、店員を呼び出しレジでの会計となります。 オンライン決済であれば、クレジットカード情報を入力して決済を完了すればそのまま退店することができます。

会計待ちなどの煩わしさが減り、店員と来店者の両者の手間が省かれる体験となっています。

多重決済の問題

運用が少しずつ軌道にのりサーバーでの処理リクエスト数が日に日に増えていく中で、お会計が重複して行われてしまう多重決済の問題が発生しました。

調査したところ、多重決済が発生するケースには2パターンがありました。

  1. 決済が途中で失敗したケース
  2. ひとつのお会計に対して複数の決済が同時に行われたケース

まず1つ目の決済が途中で失敗したケースについてですが、これはシステムの構成が関わっています。

トレタO/Xのバックエンドはマイクロサービスの構成となっており、決済においては「注文サービス」と「決済サービス」を跨いだ処理となります。また、それらの処理をオーケストレーションするAPIのGatewayサーバーがあります。

決済の処理としては、

  1. オーダーアプリからGatewayサーバーへ決済のリクエストを送る
  2. Gatewayサーバーは、注文サービスへ会計情報を問い合わせる
  3. Gatewayサーバーは、取得した会計情報の金額で決済サービスへ決済処理をリクエストする
  4. 決済サービスは、決済プラットフォームのStripeを利用しており、Stripeへ決済をリクエストする
  5. 決済が完了したらGatewayサーバーは、注文サービスに該当の注文に対しての支払いレコードを登録し、会計を支払い済みのステータスに更新する

さて、今回発生したトラブルでは決済サービスからレスポンスが返ったあとに注文サービスへの書き込みでエラーが発生していました。

注文や決済などが重なりリクエストが集中する時間帯においてサーバーへの負荷が上がり、一部のリクエストでタイムアウトが発生したためです。

決済サービスへの書き込みに失敗するとユーザー側にはエラーの表示がされますが、決済処理をバックオフができないため、決済サービスでの決済は完了されてたままに注文サービスでの会計は未会計のステータスのまま、というデータの不整合が起きた状態となります。

そして会計が未会計のステータスなので、ユーザーから支払いを再試行することが可能であり二重の支払いが行われてしまいます。

リトライと冪等性

これを解決するには、エラーとなった箇所でのリトライをすればよいと考えられます。

ただし、サーバーからタイムアウトのレスポンスが返ってきた場合にはその処理の結果が成功/失敗かがわからないため、単純にリトライしてしまうと1つの決済に対してリトライ回数分の支払いレコードが登録されてしまうことになります。

そのため注文サービスの支払いレコード登録APIには冪等性の担保が必要となります。

冪等性対応の仕様としては、The Idempotency-Key HTTP Header Fielddraft-ietf-httpapi-idempotency-key-header-02を基に一部簡略化して実装しました。

  • HTTPリクエストのリクエストヘッダーにIdempotency-Keyを設定する
  • 過去に同一のIdempotency-Keyの値でのリクエストがあれば、処理は行わずにステータスコードを409(Conflicted)でレスポンスを返す

また、StripeのAPIにも同様に冪等キーを付与する仕組みがあるので決済サーバー側もこちらを用いて冪等対応をします。

では冪等キーには何を使用するのが妥当でしょうか。

注文をとりまとめた会計レコードがもつ会計IDがあるので、一見すると会計IDを使うと会計に対して一回以上支払いができないように制限でれるように考えられます。

しかし、支払いは会計に対して本当に一度だけでしょうか。

オンライン決済やオフライン決済が複合して行われることを想定すると、会計に対して複数回の支払いを行うケースはいくつか考えられます。

  • クーポン(金券)で一部を払い、残りをクレジットカードで払う
  • クレジットカードで一部を払い、残りを現金で払う
  • グループ内で割り勘し、それぞれがクレジットカードで払う※

※ 現在は機能として未実装

会計に対して支払いは一回とは限らず、複数回行えるとした方がよさそうです。 そのため冪等キーは支払いに対して毎回ユニークなキー(UUIDなど)を発行することになりました。

同時支払いの問題

次に、2つめの問題である同時支払いについて考えていきます。

同時支払いはいわゆるECサイトなどでの決済では起きづらいですが、複数人の会計がひとつにまとめられている飲食店での決済においては発生する頻度が高まります。

同時支払いが起こるシチュエーションは主に2パターンあります。

複数のユーザーが同時に払うケース

飲食店における会計はグループ内で共有しているため、グループ内の誰でも会計を行うことが可能です。 そのため一人が支払いを開始して完了する前に別の人が支払いを開始してしまうと、両者が決済してしまうことにます。

なお、前述の通り1会計につき1支払いとは限らないので支払いを1回だけに絞ることはできません。

オンライン会計とオフライン会計が同時に行われてしまうケース

複数人の同時支払いと同様に、オンライン会計とレジでの現金払いが同時に行われてしまうケースも可能性としてあります。

オフライン決済だと現金以外にクーポン利用もあり、クーポンであれば過払いも許容されます。(例. 1,000円のお会計に3,000円のクーポンで支払う)

注文の排他制御

この問題を解決するには、支払いに対しての排他制御が考えられます。

誰かが支払い行為を開始した時点で支払い枠にロックをかけ、他の支払いは通さないようにさせます。他の人がロックを取得している状態で新しくロックの取得をしようとすると失敗するため、支払いは常に1つずつ処理されることになります。

排他制御を加えた決済の処理としては、

  1. オーダーアプリからGatewayサーバーへ決済のリクエストを送る
  2. Gatewayサーバーは、注文サービスへ会計情報を問い合わせ、ロックを取得する
  3. Gatewayサーバーは、取得した会計情報の金額で決済サービスへ決済処理をリクエストする
  4. 決済サービスは、Stripeへ決済をリクエストする
  5. 決済が完了したらGatewayサーバーは、注文サービスに該当の注文に対しての支払いレコードを登録し、会計を支払い済みのステータスに更新する

対応方針のまとめ

2つの多重決済の問題に対して、以下の対応方針で解決できそうなことがわかりました。

  • 支払いの排他制御を行う
  • 各処理で失敗したら冪等性を担保しつつリトライを行う

補償トランザクション

ではリトライを何度も行っても解決しない場合はどうすればよいでしょうか。

決済サービスへの決済処理が完了するまであればバックオフが可能です。 その場合、取得したロックを解除してユーザーからの再試行を行える状態にします。

では決済処理完了後に、注文サービスへの支払い完了登録が行えない場合はどうでしょうか。

バックオフはできませんし、エラー終了すると支払いのロックは残ったままでデッドロックとなってしまいます。 同様に、バックオフ中にロックの解除に失敗し続けた場合もデッドロックのままとなってしまいます。

現状ではこの状況に陥った場合には、お客様からは支払いを頂かずに後日トレタ側で飲食店へ補填する対応としています。 そもそも注文サービスが何度リトライをしても失敗してしまうケースとしては、サーバーがダウンしている可能性が高いので、その他の処理も続行が難しいと考えられるためです。

最善の策ではないので、発生頻度などを注視しながらあるべき対応を引き続き検討している状況です。

実装手段

これらの実装にあたって、今回はGCPのWorkflowsの利用を取り入れました。

Workflowsはタスクを登録することで外部API呼び出しなどの定義されたステップを順に実行してくれるというサービスです。 AWSであれば似たようなサービスとしてStepFunctionsがあります。

リトライの仕組みを持ち合わせているため、リトライ回数やインターバルなど柔軟なリトライ処理を容易に組むことができます。

今回はこのWorkflowsを使い、外部API呼び出しとそのレスポンスに応じたリトライ、バックオフ処理を実装しました。また、タスク登録時にタスクのIDを得られるので、冪等キーにはこれを利用しました。

ちなみにWorkflowsでの使いづらい点を挙げておくと、基本的に定義はすべてyaml形式であるため、プログラマブルな細かい制御は難しく、テストもしづらい点です。

さいごに

以上が今回行ったトレタO/Xにおける多重決済に対しての対応のアプローチでした。

分散トランザクションの難しさに改めてて対峙し、自分の未熟さを痛感しましたがここで得た教訓とノウハウを今後の開発に活かし、より安全なシステムの開発に向けて精進していこうと思います。

なお、トレタではエンジニアの募集を全方位で行なっております。

コロナ禍を乗り越えた飲食店の新しい姿を探求する仲間をお待ちしております。

corp.toreta.in

© Toreta, Inc.

Powered by Hatena Blog