複数のserviceの結果に依存しているAPIのエラーの見せ方が悩ましい

# まえがき
- マイクロサービスにおいて、複数のserviceが依存するtransactionを設計する場合、client側にエラーをどう見せるかで内部のエラーハンドリングやリカバリの処理が大きく変わってきます
- 筆者は２〜３個のserviceに依存するAPIを設計してきましたが、その度に個別のserviceで発生したエラーをどう扱うかが悩みどころでした
- このqiitaでは、実際の経験と「もしこういうのを作るとしたら」という妄想にもとづいて、マイクロserviceにおけるエラーの見せ方と、それぞれのアプローチのpros/consについて書いてみます。

# 具体例で考える
## 直列で複数のserviceを利用する場合

### A. 何も考えずにエラーを全て伝達する

![chokuretsu1.png](https://qiita-image-store.s3.amazonaws.com/0/7174/ec2ebc6a-8fee-5e38-ab59-0bc3b73e5476.png)

中継する全てのserviceが正常なレスポンス(例えば2xx系レスポンス)を返して来ない限りclientには2xxを返さないパターン。例えば、複数のserviceでの処理が決まった順番で、かつ全て正常に完結することが求められるtransactionを実現する場合は、このパターンが必要になるケースがある。

#### pros
- clientから見た結果が明快で分かりやすい
- client及び各serviceの実装がシンプルになる

#### cons
- clientにエラーレスポンスが返された時に、どのserviceでのエラーによるものかを調査するのが困難
    - 例えば、上の図の`Service B`がエラーレスポンスを返す時、その原因は最低でも二通り考えられる
    1. 依存している`Service A`がエラーレスポンスを返してきた
    1. `Service B`固有の処理に失敗してエラーレスポンスを返した
    - 最低でもこの２つを切り分けるためには、中継しているserviceのログを一個ずつ調べる必要がある。さらに`Service C`がエラーレスポンスを返す時、原因の組み合わせはさらに増加していく。
    - 対策としては、途中のsergviceのエラーを示すエラーコードやメッセージを含めて隣のservice伝達したり、外部から監視するためのserviceを用意し、clientからのリクエストがどこで失敗したかを特定できるようにしておくなどの何らかの対応が必要になる。
- エラーが発生したときのリカバリはすべてclient側で責任を持つことになる。この場合、client側にリカバリ用の処理をすべて実装することにあるが、例えばスマホのnativeアプリがclientだと複数のバージョンが同時に運用されるケースがあるなるため、nativeアプリ側のリカバリ処理が最新のAPIに追従できなくなるリスクがある。

### B. clientが呼び出したserviceから呼び出したserviceのエラーは無視する

![chokuretsu2.png](https://qiita-image-store.s3.amazonaws.com/0/7174/6218e444-8cdb-9472-f61e-69c3462243ba.png)

中継する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に依存する場合

![tehepero.png](https://qiita-image-store.s3.amazonaws.com/0/7174/980ac4e7-a6d3-a983-8953-acac4d0161e6.png)

直列に依存している場合に比べて格段に複雑になる。この場合、３つすべてのserviceの結果が正常な時だけclientに正常なレスポンスを返すか、それぞれの処理結果をすなおにclientに伝えるか、3つのうち半分が成功したら…、等色々と選択肢が発生する。

どのように処理すべきかは各serviceが行う処理内容に依存するが、ここでは下記の2パターンについて考えてみたい。

### A. 1個でもエラーを返したらそれを伝達する
３つのserviceの処理が正常に完了しなければ完結しない処理を行う時、例えば複数serviceにまたがる分散transactionを行う場合はこの選択肢になる。

#### pros
- APIとしてのレスポンスが明確
- client側の実装がシンプルに
- エラーの情報は一箇所のserviceに集約できるため、障害調査がしやすい

#### cons
- リカバリ大変すぎ問題
    - 依存しているserviceすべてをrollbackしなければならない場合、依存するserviceの数が増えれば当然線形的に複雑さが増加する
    - これも依存しているserviceの数が多い場合、serviceをgroupingしてgroupの中でtransactionを管理するserviceがcommit, rollbackを担当するように分割統治していく必要がありそう。
    - なお、このような分散transactionの管理を行う専用のレイヤーを導入する[Saga Pattern](https://speakerdeck.com/caitiem20/applying-the-saga-pattern)という設計パターンが存在する

### B. 正直に全ての結果をclientに伝える
３つの処理のうち、どれがが失敗してもclient側で回避ができる場合や、失敗する可能性があることがわかってる処理を投機的に行うようなケースだとこのパターンが使える。

#### pros
- client側で柔軟に処理が実装できる

#### cons
- clientにエラー処理が依存する
    - これは直列で処理する場合のBのパターンで発生するのと同じ問題。
    - clientにすべてを委ねるように実装するとAPIの変更があったときにclientの仕様も変更する必要が発生するため、APIの変更に追従できるようにしていく必要がある。

# まとめ
- マイクロserviceのエラーハンドリングは処理内容に応じて適切な方法を選ぶ必要がある
- 安易にclient側にエラーハンドリングを丸投げするとAPIはシンプルになるが、古いclientに引っ張られてAPI側のアップデートが難しくなるなどトレードオフが存在するので、API側で実装する処理やclientの特性に応じて、適切なエラーハンドリングの手法を選ぶ必要がある


[^1]: ちなみに筆者はこういうパターンで実装する時は非同期でしか実装したことがない
[^2]: Netflixとかの規模になるとこういう処理がガンガン出てきそう。いつかそういう巨大なマイクロservice群を相手にした仕事がしてみたいものである。