あいかわらずブログからの転載です。
https://munchkins-diary.hatenablog.com/entry/2019/11/14/151109
雑談を抜いたプログラミングに関係する場所だけそのままコピペしてきてます。
さて、今回は短めの記事です。
アドベントカレンダー作りました
先に宣伝させてください。
分散型やクラウドネイティブに関するアドベントカレンダーを作りました。
クラウドネイティブアプリケーションの開発や設計 Advent Calendar 2019
https://qiita.com/advent-calendar/2019/cloud-native-dev-arch
数日前に作成したのですが、参加者が鳴かず飛ばずで、かなり落ち込んでいます。
もしよかったらどなたか参加していただけると嬉しいです(;_;)
本題
分散型アプリの開発をしていて、別の開発者とPOSTの仕様で少し討論になったので、メモがわりに残しておきます。
このDiscussionのおかげで私はidempotentと言う単語を完全に記憶しました。
冪等性(べきとうせい)に関する簡単な説明
初学者向けにまず 冪等性(Idempotency) に関する説明です。
冪等性という言葉に関しては、Wikipediaの言葉を引用すると
ある操作を1回行っても複数回行っても結果が同じであることをいう概念である
とあります。
簡単に身近な例で例えると、
- 汚れたお皿Aに洗うと言う行為を1回やる → 綺麗なお皿Aが残る
- 汚れたお皿Aに洗うと言う行為を100回やる → 綺麗なお皿Aが残る
という感じで、洗うと言う行為はお皿に対して冪等な操作であると言えます。
一方、ポケットのビスケットを叩いて増やす歌を考えると
- ポケットを叩く → ビスケットが2つ
- もひとつ叩く → ビスケットが3つ
となり、叩くごとにポケットの中のビスケットは増えていくため、叩くと言う行為はポケットに対して冪等でない操作と言えます。
システム開発で例えると、
- データベースのQUERY -> 何回叩いてもDBのレコード群の状態を変えない -> 冪等である
ですが、
- INSERTをIncrement IDで流す -> 叩いた数だけレコードが作成される -> 冪等ではない
と言う感じになります。
この説明は全く厳密でないため、もっと詳しく知りたい方は @KyojiOsada さんの記事がとてもくわしく書かれており、とても参考になったので、ぜひそちらを読んでみてください。
参考記事: 冪等と安全に関する誤解
REST APIのメソッドごとの割り当て
詳しくは以前書いたこちらの記事に詳しく書きましたが、REST APIに置いて、リソースに対する操作は基本的に、HTTPのメソッドごとに処理を割り当てます。
そのうち、GET, PUT, DELETE
などのメソッドは、冪等性が保証されなくてはいけません。
(参考:REST API Tutorial)
一方で、POSTに関しては明確に
POST is NOT idempotent. (POSTは冪等ではない)
と言う風に記述されてます。
もうすこし上述のサイトのPostに関する部分を引用すると、
Generally – not necessarily – POST APIs are used to create a new resource on server. So when you invoke the same POST request N times, you will have N new resources on the server. So, POST is not idempotent.
要約すると、必須ではないが、一般的にPOSTは新しいリソースを作成するのに使用されるため、N回呼ばれればN個のリソースを作成されることが多く、POSTは冪等ではない、と言う風に書かれています。
分散型アーキテクチャとWebAPIのエラーハンドリング
MSAなどサービス同士の疎結合を維持しながらWeb API経由でコミュニケーションをとるアーキテクチャを取る場合、クライアント側は以下のエラーの可能性を気にしながら実装を行う必要があります。
エラータイプ | 代表的なステータス | 対応例 |
---|---|---|
即時の復旧が見込まれる一時的なServerエラー | 503, 504, 509など | 一定時間sleepさせたあとリトライする。 MQなどに流して復旧後にリトライ |
復旧に長期間かかる(可能性のある)Serverエラー | 500, 501, 502, 507など | 処理を中断してロールバック MQなどに流して復旧後にリトライ ユーザにwarningを提示し復旧後に再度処理を流してもらう |
リトライ可能なクライアントエラー | 401, 407, 408, 429など | 再認証したのちリトライ 一定時間sleepさせたあとリトライ タイムアウトを伸ばしてリトライ |
リトライ不可能なクライアントエラー | 400, 403,405, 406など | 入力をマスクしたのちlogに出力し開発者に通知 ユーザにwarningを提示し復旧後に再度処理を流してもらう |
状況によっては無視できるクライアントエラー | 404, 409など | 既存オブジェクトを確認して同一ならスキップ(409) |
※対応例やレスポンスのステータスコードはあくまで例示であり、通信先のサービスの仕様やビジネス要件に依存します。期待されるエラーやリトライ方法は鵜呑みにせず、自分のサービスや通信先の仕様や要件に合わせて柔軟に設計・実装してください。
また、WebApi経由の処理群はトランザクション管理が難しいため、処理群の中のAPIコールが1つでも失敗した場合、一連の処理を流し直したり、作成・編集された可能性のあるレコードを全てロールバックしたりと言う処理を行う必要があります。
(ここに関してはpub-subなどを利用した回避方法もあるのですが、それはアドベントカレンダーの記事で紹介します。多分。)
POSTが冪等性じゃないと困る理由
以上のように、Web APIコールで処理を行う設計の場合、多くのケースでAPIコールで失敗した場合ロールバックやリトライの処理を挟む必要があります。
単純にロールバックして処理を終える場合はまだいいのですが、リトライを行う際にPOSTが冪等性でない場合、APIコールごとにリソースが作成される
ため、一旦すでに作成された可能性がある要素を削除して再度作り直すという処理が必要になり、リトライのコストが高くなります。
Web APIというのは使用者にとって使いやすく設計することが最も重要であり、リトライにたいするコストが高いAPIはいいAPIとは言えません。
POSTを冪等にするための実装
POSTを冪等にするための実装方法は、GOOGLEで検索するといくつか出てきますが、僕はPOSTを設計する際、よくResourceのIDを事前に指定して作成するようにしています。
例えば、
/api/docs/${docType}/${docId}
と言うRestfullなpathを設計した場合、一般的にはPOSTは一つ上の階層の/api/docs/${docType}/
にたいして処理を割り当てることが多いですが、ここで/api/docs/${docType}/${docId}
に対してもPOSTを割り当てられる用にします。
このような設計にした場合、
- IDが重複しないこと
- 重複した場合適切なHTTP Statusを返す事
-重複した場合、できるだけClient側でGetし直さずにエラーハンドリングができるようにすること
などを設計の段階で織り込んで置く必要があります。
IDのコンフリクト回避には、UUIDのversion4
や、作成したいリソースのhashから作成したUUID
などを使い、作成方法をAPI Docsなどに明示します。 ←ここ大事
作成したいリソースのHashから作成する場合、一意性を強固にするため、作成時間のTimestampと作成者のIDを必ず含めるようにしてhashを作成し、Pathに割り当てた上でPOSTしてもらいます。
また、エラーハンドリングを用意にするため、作成が成功した場合200
を、已に作成されていた場合は409
を返すようにします。
IDがConflictする可能性はほぼゼロと言っていいレベルで低いですが、409が返す時、一緒に作成者のIDのSHA256HashおよびTimestampを返す用に設計すれば、409が返ってきた際にもAPIの利用者は安心して作成をスキップすることができます。
また、僕がメンバーと議論してる時に大いに参考にさせていただいた、Saurav SinghさんのHow to achieve idempotency in POST method?では、headerにidempotentKeyを設定し、そのidempotentKeyをなんらかのStorageに保存するという方法が紹介されています。
個人的にはやや冗長に感じるためあまりモチベーションはわかないのですが、こういう方法もあるんだなといい勉強になりました。
まとめ
POSTは一般的に冪等性が担保しにくいメソッド、もしくは担保しなくていいメソッドという認識が一般的かもしれません。
しかし、POSTの冪等性をAPI開発者側が担保してやることで、リトライやロールバックが用意になり、利用者からはより使いやすいAPIになると僕は考えています。
通信先のサービスの一部のNodeが落ちていることや、リクエスト過多によるスケーリング中でタイムアウトしてしまうことなど、分散型では日常茶飯事です。
フォールトトレラントなAPIを提供するためにも、明確にPOSTの冪等性を担保することは有意義だと考えています。
もしこの記事がこれからAPIを設計しようとしている誰かの役に立てば幸いです。