この記事は Stripe Advent Calendar 2018 25 日目の記事です。
🎄🎄🎄 Merry Christmas to all Stripe users! 🎄🎄🎄
🎅みなさん、クリスマスを楽しんでいますか?🤶
残念ながら、東京ではホワイト・クリスマスとはいかなそうですが、天候が大きく崩れることもなく、まあまあのクリスマス日和といえそうです🙂
さて、わたしは、今年の聖夜を Elixir の Stripe SDK (非公式) にリトライ処理と冪等性の実装をして過ごしました。その過程で API ドキュメントの調査や、公式 SDK の実装を読み解いたりしたので、Stripe API のリトライ処理と冪等性保証について書いてみたいと思います。
なお、この記事で書いている内容は、公式の SDK であればデフォルトで有効になっていると思いますので、SDK を使う分には敢えて読む必要はありません。
リトライ処理の必要性
Stripe API は HTTP の REST API として提供されています。そして、すべての API 呼び出しは失敗する可能性があります。失敗する理由もさまざまです。まず考えられるのは、
- 指定したパラメーターが不足、あるいは間違っている
- 存在しないリソースを取得しようとした
- リソースにアクセスするための権限がなかった
など、API の呼び出し方やリソースの状態によって失敗するケースです。これらは、API の呼び出し方やリソースの状態がを変わらないかぎり、何度呼び出しても成功することはありません。
しかし、一時的な障害により失敗するケースは違います。
- ネットワークの遅延により、リクエストが完了せず、タイムアウトしてしまったとき
- 突発的な負荷増大により、API サーバの処理能力が追いつかず、コネクションを確立できない場合
これらのケースでは、すこし待ってから同じリクエストを再送すれば、成功する可能性があります。そのため、これらのケースに遭遇したときは、エンドユーザーにエラーを表示する前に、何度かリトライをした方が信頼性が向上します。
蛇足になりますが、Ruby SDK だと、上記のケースに加えて、HTTP レスポンス・ステータスが 409 Conflict
のときもリトライをするようになっています。リソースの作成途中に、同様のリクエストが来た場合の対処ですが、詳しくは該当 Issue での議論をごらんください。
リトライの間隔
さきほど「すこし待ってから同じリクエストを再送すれば」と書きましたが、では、どれくらいの頻度で再送すべきでしょうか? 単純に等間隔で繰り返してしまうと、API サーバーに不必要な負荷を与えてしまうかもしれません。
たとえば、API サーバーが一時的な障害により処理が重くなっている、としましょう。このとき、エラーを受信したクライアントがすべてデフォルト設定の等間隔でリクエストを再送した、としたらどうなるでしょうか? 同時にリクエスト再送が繰り返され、API サーバーには負荷がかかりつづけてしまいます。
このようなケースを考えて、一般的には、
- 指数関数的に増えるリトライ間隔 (Exponential Backoff) と、
- ランダム性を与える Jitter
を組み合わせるのが良い、とされています。
前者の Exponential Backoff は初期リトライ間隔 base
と最大リトライ間隔 cap
を決め、何回目のリトライかを表す attempt
を使って、以下のように計算します。
MIN(cap, base * 2 ** attempt)
また、こちらの記事で詳しく分析されているように、Exponential Backoff だけでは同時リクエストの問題にはあまり効果がありません。時間の間隔が長くなっただけで、同時リクエストは残ったままだからです。
そこで、Exponential Backoff にランダム性を導入 (Jitter) します。たとえば、0 <= N <= 1
のランダムな値を掛け合わせることで、個別クライアントのリトライ間隔に幅を持たせ、リクエストが集中することを防ぐわけです。
なお、Ruby SDK では Exponential Backoff および、0.5 <= N < 1
の Jitter が実装されています。
冪等性
**冪等性(べきとうせい)**と書くと小難しく聞こえますが、要するに「何度同じ操作をしたとしても同じ結果を得られる」ということです。これを実現できているシステムは、そうでないシステムと比較して運用コストが低くなります。
たとえば、新規ユーザーを作成する API があったとしましょう。もし、この API が冪等でない場合、高負荷時に以下のようなことが起こりえます。
- API サーバーがリクエストを受けつける
- 処理に時間がかかり、クライアント側はタイムアウト
- クライアントがリトライ
- API サーバーが 1 で受けつけたリクエストの処理を完了
- API サーバーが 3 で受けつけたリクエストの処理を完了
結果的に、同じユーザーがふたつ作成されてしまいます。この場合、運用する側としては、こうした結果を諦める受け入れるか、手作業で削除してまわるなどの対応が必要になってきます。このへんのリトライや冪等性の話については、リトライと冪等性のデザインパターンが参考になります。
Stripe API の冪等性
Stripe API では、ある API 呼び出しを冪等にするための手段が用意されています。
詳しい仕様は公式ドキュメントの Idempotent Requests にありますが、
- クライアントはリクエストに
Idempotency-Key: <key>
というヘッダーを含める - リトライするときは同じ
Idempotency-Key: <key>
をヘッダーを含める
だけです。<key>
にはユニークな値 (UUID v4 など) を使用すること以外は何を使ってもかまいません。これだけで、API リクエストは冪等になり、同じリソースが誤って二重に作られてしまうような事態を避けられます。
この機能は本当にすばらしい! 🎉
最後に
いかがだったでしょうか? 公式の SDK を使っている人にとってはあまり気にする必要のない話題でしたが、Elixir のように成長途中の言語を使っていて SDK に機能が足りない、なんて不幸な人の助けになれば幸いです。