まえがき
- マイクロサービスにおいて、複数のserviceが依存するtransactionを設計する場合、client側にエラーをどう見せるかで内部のエラーハンドリングやリカバリの処理が大きく変わってきます
- 筆者は2〜3個のserviceに依存するAPIを設計してきましたが、その度に個別のserviceで発生したエラーをどう扱うかが悩みどころでした
- このqiitaでは、実際の経験と「もしこういうのを作るとしたら」という妄想にもとづいて、マイクロserviceにおけるエラーの見せ方と、それぞれのアプローチのpros/consについて書いてみます。
具体例で考える
直列で複数のserviceを利用する場合
A. 何も考えずにエラーを全て伝達する
中継する全てのserviceが正常なレスポンス(例えば2xx系レスポンス)を返して来ない限りclientには2xxを返さないパターン。例えば、複数のserviceでの処理が決まった順番で、かつ全て正常に完結することが求められるtransactionを実現する場合は、このパターンが必要になるケースがある。
pros
- clientから見た結果が明快で分かりやすい
- client及び各serviceの実装がシンプルになる
cons
- clientにエラーレスポンスが返された時に、どのserviceでのエラーによるものかを調査するのが困難
- 例えば、上の図の
Service B
がエラーレスポンスを返す時、その原因は最低でも二通り考えられる
- 依存している
Service A
がエラーレスポンスを返してきた -
Service B
固有の処理に失敗してエラーレスポンスを返した
- 最低でもこの2つを切り分けるためには、中継しているserviceのログを一個ずつ調べる必要がある。さらに
Service C
がエラーレスポンスを返す時、原因の組み合わせはさらに増加していく。 - 対策としては、途中のsergviceのエラーを示すエラーコードやメッセージを含めて隣のservice伝達したり、外部から監視するためのserviceを用意し、clientからのリクエストがどこで失敗したかを特定できるようにしておくなどの何らかの対応が必要になる。
- 例えば、上の図の
- エラーが発生したときのリカバリはすべてclient側で責任を持つことになる。この場合、client側にリカバリ用の処理をすべて実装することにあるが、例えばスマホのnativeアプリがclientだと複数のバージョンが同時に運用されるケースがあるなるため、nativeアプリ側のリカバリ処理が最新のAPIに追従できなくなるリスクがある。
B. clientが呼び出したserviceから呼び出したserviceのエラーは無視する
中継するserviceの結果に関係なく、最初にリクエスト受けるserviceがclientから正常にリクエストを受け付けたら、依存しているserviceの結果に関係なく200を返すパターン。
このパターンは各種service間の処理が非同期でないと成り立たないケースが多いと思われるが、一応同期処理でも実装は可能だと思う。 1
pros
- エラーの情報を一箇所のserviceに集約できる
- リカバリ処理をservice側で行うことができる
- 実際の処理が実現されるまでに時間がかかっても問題ない場合、非同期であとでリカバリできる
cons
- service側のリカバリ処理の実装が複雑になる
- 例えばリカバリ担当の
Service C
が依存しているserviceが2個であれば、最大2つのserviceの面倒を見ればよい(transactionのrollback等が必要ならばそれを行うrequestを飛ばす等)。エラーが発生するパターンもそう多くはない。 - しかし単純に増えれば増えるほど対処しなければならないservice数とエラー発生パターンが線形的に増加する。
- こうなってくると、ある程度serviceをグルーピングして、group内のtransactionを管理する代表のserviceを決めておき、それぞれのグループ内でリカバリを行うなどの工夫が必要になってくると考えられる。
- 自分は実際に設計したことはないが、大規模なマイクロservice内でのtransaction処理はこのような配慮が必要なのではないかと考えられる。 2
- 例えばリカバリ担当の
並列で複数のserviceに依存する場合
直列に依存している場合に比べて格段に複雑になる。この場合、3つすべてのserviceの結果が正常な時だけclientに正常なレスポンスを返すか、それぞれの処理結果をすなおにclientに伝えるか、3つのうち半分が成功したら…、等色々と選択肢が発生する。
どのように処理すべきかは各serviceが行う処理内容に依存するが、ここでは下記の2パターンについて考えてみたい。
A. 1個でもエラーを返したらそれを伝達する
3つのserviceの処理が正常に完了しなければ完結しない処理を行う時、例えば複数serviceにまたがる分散transactionを行う場合はこの選択肢になる。
pros
- APIとしてのレスポンスが明確
- client側の実装がシンプルに
- エラーの情報は一箇所のserviceに集約できるため、障害調査がしやすい
cons
- リカバリ大変すぎ問題
- 依存しているserviceすべてをrollbackしなければならない場合、依存するserviceの数が増えれば当然線形的に複雑さが増加する
- これも依存しているserviceの数が多い場合、serviceをgroupingしてgroupの中でtransactionを管理するserviceがcommit, rollbackを担当するように分割統治していく必要がありそう。
- なお、このような分散transactionの管理を行う専用のレイヤーを導入するSaga Patternという設計パターンが存在する
B. 正直に全ての結果をclientに伝える
3つの処理のうち、どれがが失敗してもclient側で回避ができる場合や、失敗する可能性があることがわかってる処理を投機的に行うようなケースだとこのパターンが使える。
pros
- client側で柔軟に処理が実装できる
cons
- clientにエラー処理が依存する
- これは直列で処理する場合のBのパターンで発生するのと同じ問題。
- clientにすべてを委ねるように実装するとAPIの変更があったときにclientの仕様も変更する必要が発生するため、APIの変更に追従できるようにしていく必要がある。
まとめ
- マイクロserviceのエラーハンドリングは処理内容に応じて適切な方法を選ぶ必要がある
- 安易にclient側にエラーハンドリングを丸投げするとAPIはシンプルになるが、古いclientに引っ張られてAPI側のアップデートが難しくなるなどトレードオフが存在するので、API側で実装する処理やclientの特性に応じて、適切なエラーハンドリングの手法を選ぶ必要がある