読者です 読者をやめる 読者になる 読者になる

複数サービス間の整合性の取り組みについて

こんにちは。技術部 開発基盤グループの大石です。

本日は開発基盤グループが社内の各サービスに提供している共通基盤サービスの1つである共通決済基盤を例にサービス間の整合性を維持するための取り組みを紹介したいと思います。(共通決済基盤については以前紹介した クックパッドの課金を支える技術 を参照ください)

決済における整合性を考える

サービス間連携は決済に限らず発生するものですが、共通決済基盤の場合、組織外にあるサービスと通信する必要があり、コントロールができない外的要因に影響を受けやすい点と、決済という確実性が求められる処理を含んでいるということの間で整合性について考える必要があります。

まずは、共通決済基盤上で行われるサービス間通信の種類とそれぞれで通信を行っている際にエラーが起きた場合にどのようにハンドリングすれば整合性を維持できるかを考えてみます。

サービス間通信の種類と流れ

共通決済基盤で行われるサービス間通信には2種類あります。

  1. 共通決済基盤と決済ゲートウェイとの通信
  2. 共通決済基盤と自社で運用する各サービスとの通信

(※決済処理は自分たちのシステム内では完結せず、クレジットカード決済であれば決済代行会社や、キャリア決済であればモバイルキャリアの決済システムとの接続が必要となります。本稿ではそれらをまとめて決済ゲートウェイと呼称します。)

これらの通信の流れの具体的な例として、継続決済の契約完了から有料サービスをユーザーが利用できるまでの流れとして下記に示します。

f:id:eisuke-oishi:20160531110230p:plain

  1. 決済ゲートウェイが決済手続き完了を共通決済基盤へ通知する
  2. 共通決済基盤は通知を受け取り、決済情報をデータベースへ保存する
  3. 共通決済基盤は決済結果を連携先の自社サービスにコールバックする
  4. 共通決済基盤は決済ゲートウェイへ応答を返し、決済が確定する

という流れになります。

エラーが起きたとき

上記の図のどこかで障害が発生しエラーが起きた場合、どのようにハンドリングするかを考えてみます。

一番わかり易くシンプルな方法としては、データベースにおけるトランザクションの概念のように、一連の処理の途中のどこかで失敗した時点で共通決済基盤はすべてロールバックできるようにすることです。

先程の流れで考えてみると、(1)あるいは(2)が失敗した場合はそのまま決済ゲートウェイに失敗の応答を返します。 (3)が失敗した場合は、(2)のデータベースをロールバックし、決済ゲートウェイに失敗の応答を返します。

この方法は一見問題ないように見えますが、いくつかの問題が発生します。

すべてロールバックするときの問題点

共通決済基盤は、

  • 決済ゲートウェイと共通決済基盤間
  • 共通決済基盤と自社サービス間

という2つのシステム境界を持っています。

例にあげた手続きの流れ全体に対して、トランザクションを確保しようとするアプローチの問題は、そのシステム境界間での通信においてエラーが起きたときにロールバックを完璧にできない場合が発生してしまうことです。

例えば、共通決済基盤と自社サービス間において、連携先の自社のサービスへ共通決済基盤がコールバックを送信する部分での通信時にタイムアウトが起きたとき、共通決済基盤上ではエラーとして処理を行い、決済ゲートウェイへは決済が失敗したと応答したのにもかかわらず、実は自社サービス側ではリクエストが成功しており、決済が成立した状態になることがありました。 またその頻度は、通信先の自社サービス内において、他のサービスのAPIなどの通信が発生している場合、共通決済基盤に間接的に繋がるいずれかのサービスへの通信が失敗した時点で全てロールバックすることになるため、サービス分割が進むにつれて上昇しやすくなっていくという問題もありました。

もちろん、自社サービス側でそういったことが起きないように適切にエラーハンドリングをしたり、2フェーズコミットのような方法を要求することもできますが、そういった方針は外部の設計に依存するため、共通決済基盤側で独立してうまく対処する方法をとる必要があります。

可能な限り成立させる

上記のような問題があったため、共通決済基盤でとった解決策は、障害が発生するまでの状態をできる限り保存し、エラーが起きたとしても可能な限り成立させるようにすることでした。

具体的には先程述べたの2つのシステム境界、

  • 決済ゲートウェイと共通決済基盤間
  • 共通決済基盤と自社サービス間

それぞれを1つの処理単位として考え、決済ゲートウェイと共通決済基盤間の連携が成功すればそこですべて成功とみなす方法です。

図にある流れ、

  1. 決済ゲートウェイが決済手続き完了を共通決済基盤へ通知する
  2. 共通決済基盤は通知を受け取り、決済情報をデータベースへ保存する
  3. 共通決済基盤は決済結果を連携先の自社サービスにコールバックする
  4. 共通決済基盤は決済ゲートウェイへ応答を返し、決済が確定する

で具体的にみてみます。

まず、(1)、(2)を1つの単位として、それが失敗した場合は決済ゲートウェイに失敗の通知をして終了します。 成功した場合は、(3)の処理を行い、この処理の成否に関わらず(4)では必ず成功の通知を決済ゲートウェイに対して送信します。 また、(3)が失敗した場合、ジョブキューなどを利用してリトライを試みたりすることで、自動的に整合性のある状態にするように努めます。

このアプローチによるメリットは、障害の影響をなるべく小さくし共通決済基盤の独立性を高めることで、外部環境への複雑な依存が少なくなるという点です。

デメリットとしては、一時的に不整合が発生することです。もし不整合が発生した場合、共通決済基盤はできるだけ短い時間で不整合を修正するように振る舞う必要があります。

しかしそのデメリットよりも、2つのシステム境界それぞれに対して独立して問題分析や対応を行うことができる点は複雑性を下げる大きなメリットとなると判断しました。

整合性を保つことができているのか

ここまで紹介したものは、システム境界間での整合性を保つ方法でした。 私達が採用した「可能な限り成立させる」方法は整合性に対して、完璧を目指すのではなくエラーが起きる前提で、エラーの影響を最小化する方法をとっているとも言えます。

そこで必要になるのが、整合性が保つことができているのかをチェックする機構です。

この機構によってリトライも失敗し不整合が解決できないままのケースや、決済ゲートウェイと共通決済基盤間の通信でタイムアウトなどが発生した場合は、そもそも共通決済基盤ではなにも起きていないのに、決済だけが成立してしまっている場合など、共通決済基盤が責任をもつすべてにおいての整合性を担保するようにします。

具体的には、クックパッドの課金を支える技術 に紹介した、決済ゲートウェイと共通決済基盤、共通決済基盤と自社サービスのそれぞれで決済の情報すべてを定期的に突合することで整合性が保たれているかの確認を行っています。

また最近では、比較的大きめの不具合や障害が起きない限り、ここまで到達するケースはほとんど無く、もし到達した場合は Issueを作成し、どのような原因で整合性を回復できなかったのかを記録し、その改善を共通決済基盤へフィードバックするということでより精度を高めるようにも機能しています。

最後に

すべてロールバックすることのメリットは、失敗したときの状態がわかりやすいという点、最終的に成功しているのか、失敗しているのかの2つに結果が収束する原子性ではありますが、うまくロールバックすることができないことが多く、私達はこのアプローチを改善する必要がありました。

そこで、強い整合性よりも結果整合性という考え方を優先して「可能な限り成立させる」方針で整合性を保つようにしています。

ただし、強い整合性を否定しているわけではなく、どちらか一方にはっきり固定しなければいけないということではありせん。決済ゲートウェイと共通決済基盤間において、安全にロールバックが可能な決済方法の場合は部分的に強い整合性に近い方法を採用している箇所も存在しています。

また、「可能な限り成立させる」方針によって、

  • 独立性を高めつつ、なにか1つの方法に縛られることなく柔軟に対応できる点
  • 完璧を目指すのではなくエラーが起きる前提で、通常のハンドリングから漏れてどうしても整合性を維持できなかったものが発生した場合は、改善のフィードバックをすぐに行うようにすることで外的要因の変化に対して柔軟に対応できる点

のような別のメリットもありました。

これらの対応によって不整合の発生する割合は以前よりも低下したので、このアプローチは今のところ上手く行っているのではないかと思います。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/