microservices
マイクロサービス
architecture

"Building Fault Tolerant Microservices" というプレゼンがとても良かった話

Microservices Advent Calendar 2017 7日目の記事です。

昨今、microservicesという言葉を聞かない日がないくらい、Netflixを始め、多くの企業がmicroservicesを導入したシステムを運用し、そのノウハウが蓄積されています。 (このAdvent Calendarが盛り上がってないのはちょっと寂しいですね)
ですが、正しくmicroservicesを導入することはとても難しいですし、正しく導入できたとしても、運用している内に複雑性はどんどん増していき、様々な困難に直面することになります。

microservicesって、必要となる知識が多岐に渡っていて、どこから手をつけたらいいの?って感じで、体系立てて知識を蓄えるのが難しいなーと思い、あれこれと本を読んだり、発表を見たりしていますが、"正しくmicroservicesを導入する" という部分に関しては、Sam Newmanの "Principles Of Microservices" という動画がとても参考になりました。
(まとめ) https://qiita.com/seikoudoku2000/items/002392894c7da3db6cc5

そして、今回は、microservicesなシステムを運用する中で最も大事と思われる"Fault Tolerant" (=一部で問題が起きてもそれが全体に波及しない)システムを作るという所に関して、"Building Fault Tolerant Microservices" というプレゼンがとてもよくて、ちょっと感動したので、この内容を紹介したいと思います。おー、それあるある!って感じの共感しやすい (ていうか、実際に食らったことのあるものがちらほらとw)例をベースに、時系列&体系的にとても分かりやすくまとまっていました。

動画 & 資料の場所

(動画のリンク) https://www.youtube.com/watch?v=pKO33eMwXRs
(資料のリンク) https://www.jfokus.se/jfokus16/preso/Building-Fault-Tolerant-Microservices.pdf

スライドの抜粋を貼ると結構なボリュームになってしまうので、以下の説明ではページ数のみを記載します。是非、スライドと付き合わせながら読んでみてください。

tl;dr

  • Use Timeout
  • Circuit Braker
  • Bulk heads
  • Thread pool handover

の順で導入していき、Service Callをしっかりとmontoringしよう!

Introduction

1666年、ロンドンのパン屋で始まった小さな火事は、街全土を焼き尽くす大火災に発展した。この時、小さな火事のうちに周りのビルを壊すか?という判断に対して、ロンドンの市長が"No" の決断を下したことが被害を大きくした。被害が小さいうちに、全体への波及を確実に止めるというのはとても大事。
https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%B3%E3%83%89%E3%83%B3%E5%A4%A7%E7%81%AB

サービス初期 - microservice化 (スライド7-15)

(ここで使われているAcme Books というサービスは実際には存在しないが、ここであげられる各例は、AVANZAで実際に起きたことに沿った内容になります。)

最初の頃はサービスへのアクセスが増えるにしたがって、webサーバをscale outしたり、DBをscale upしたりしてきましたが、どんどんと巨大化が進んだので、3つのサービスに分割した

1回目の障害 (スライド16-26)

そんなある日、"Special Offers"サービスが使っている"Purchace history" サービスがダウンしたことで、影響が連鎖し、サービスが全ダウンしてしまった。
postmortemでは、どのサービスでも障害を出さないように、"もっとコードレビューをすればいいんじゃないか?"、"テストをもっと充実させれば?" など様々な意見が上がってきた。
しかし、仮に各サービスが99.999%のSLAを実現しても、1000 サービス存在すると、全体としては99.9%のSLAになってしまう。耐障害性の強いシステムを作ることが大事だ! = Design for Failure

1つめのルール: USE TIMEOUT (スライド27-39)

各サービスの呼び出し部分のロジックを確認したところ、timeoutが設定されていなかった。(Javaのlibraryのdefaultが無制限に待つというものだった)
当然、これだと、どこかのサービスが応答しなくなると、すぐにThread poolを喰い潰してしまうので、全てのサービス呼び出し部分にtimeoutを追加した。
これで解決だ!

2回目の障害 (スライド40-47)

Special Offersサービスが急に過負荷な状態に陥り、サービス全体のレスポンスが遅くなり、サービス全体がダウンする事態に。サービス間の通信でのtimeoutは設定していたのだけれど、timeoutが多発するくらいにサービスのレスポンスが悪化すると、timeout待ちなスレッドが多すぎて、外から流入してくるリクエストを捌けなくなる。

また、過負荷になった原因を調べたら、HTMLのheaderに修正があり、色んなページでSpecial Offersを呼ぶようになっていて、一気にリクエスト数が増えていた。Timeoutだけではこのパターンの障害を防ぐことは出来ない。

2つめのルール: CIRCUIT BREAKERS (スライド48-58)

単位時間あたりの、timeout発生回数が閾値を超えた、想定外のエラー数が閾値を超えた場合などは、自動的にそのサービスへの接続はエラーにしてしまう。その間はsingle callでサービスの状態をチェックし、healthyになったと判定できてら、再度アクセスを元に戻す。
クライアント側で、circuit breaker発動中のエラーハンドリングを足すのも忘れずに。
これで解決だ!

3回目の障害 (スライド60-64)

長い間、安定して動いていたが、再び、サービス全体のレスポンスが遅くなり、サービス全体がダウンする事態に。
今回もSpecial Offerサービスの遅延が原因。しかし、今回は呼び出しtimeoutが発生するほどの遅延ではなかったので、Cirucuit Breakerは発動しなかった。timeoutしない程度に処理は処理は成功していたが、レスポンスがずっと遅い状態が続いたためにじわじわとThreadを食いつぶし、サイトへのincomingなrequestを捌けなくなってしまった。
一般的にサービス呼び出しのtimeout設定はサイト全体のresponse timeよりは長く設定するので、TimeoutとCircuit Breakerだけではこのパターンの障害を防ぐことは出来ない。

3つめのルール: BULKHEADS (スライド65-77)

各サービスごとに同時呼び出し数を制限。どこかのサービスの処理が遅くなったとしても、そのサービス呼び出しで、全体のThreadを食い潰すのを防ぐ。
各サービスへの割り当てThread数の総数は、全体のThread総数より、かなり少ないものでなければならない。流入してくるrequestの数とresponse timeから適切な値を計算しよう。
また、各サービスの同時呼び出し数の制限が入ることになるので、あるサービスにアクセスが殺到して過負荷に陥るのを防ぐことも出来る。

Circuit Breakerの時と同様に、クライアント側でのエラーハンドリングを足すのも忘れずに。
これで解決だ!

4回目の障害 (スライド78-83)

利用可能なthreadがずっと少なくて、throughputも出てない。全てのサービス呼び出しが失敗している。
さらに調べていくと、timeoutが効いていないじゃないか!?
client libraryのversion updateがあったが、新しいversionのclient libraryにバグがあり、timeoutの設定が効かなくなっていたことが判明。client library選定/確認の問題ではあるが、起こりうる以上は何らかの対応が必要。

4つめのルール:Thread pool handover (スライド84-95)

clientがリクエストを飛ばすthread poolと実際にサービスを呼び出すthread poolを分ける。
仮にclient libraryがおかしくなったとしても、サービス呼び出しのthread pool <-> サービスの間の通信にtimeoutがあるので全体に波及する問題には発展しない。Thread割り当て数の調整は必要なのでBulkheadも必然的に含まれる。

これまで同様、クライアント側でのエラーハンドリングを足すのも忘れずに。

ただし、このパターンはレイヤーが増えることで、僅かながらにパフォーマンス影響があるので、流量の多いところでは、このパターンは敢えて適用せずに、client libraryのupgradeの所をしっかりとチェックするという方式で運用している箇所もある。

これでついに全て解決したぜ〜! Yey!!

monitoringに関して (スライド96-101)

通常、モニタリング(サーバのCPU利用率、サービスとしてのthroughputやresponse time)するものに加えて、microservicesなシステムを運用する際はサービス呼び出し側の部分をしっかりモニタリングするべし!
- Timeout Rate
- Rejected call count
- circuit rate
- Failure/Success Rate
- Response times

エラーが起きるたびに、都度、反応するのではなく、閾値を決めて対応。
むしろ、それなりの規模のサービスで、エラーが全く起きていない状態の方が、何かが正常に動いていない可能性があるので、疑った方がいいかもしれない。
また、異常が発生している時に、状況をしっかりと理解せずに設定の変更(e.g. timeoutの引き伸ばし) はダメ。絶対。

Appendix

参考になる書籍やプロジェクト
Relase it! という書籍
https://www.amazon.co.jp/dp/4274067491
NetflixのHystrix
https://github.com/Netflix/Hystrix/wiki

QAセッション

Q: こういったパターンは各クライアント(=サービスを呼び出すサービス)毎に実装する必要があるのか? 例えば、違うプログラミング言語をクライアントが動いていたら、その言語毎に実装しなければならない? もし、それを解決するbrilliantなアイデアがあれば、教えてもらいたい。。
A: そんなbrilliantなアイデアはないよー。

感想

Fault Tolerantなシステムを作ろう、そのために、circuit breakerやbulkheadを導入しよう!みたいな記事やら書籍は目にしたことがありましたが、その場では分かった気になるけど、いざ、導入となると、はて、どこから手をつければ、、という状態でした。
このプレゼンテーションでは、仮定のサービスが進化していく様子とともに、それがない状態だとこうなるよーというのを、おーー、それあるある!と思える例をベースに、時系列&体系的にプレゼンテーションが組み立てられていて、とても分かり易く、このサービスが歩んだ道が踏んでいくべきステップであり、具体的に考えやすくなりました。
ということで、Microservices怖くないぜ!Yey!!と言える日に向けて、Fault Tolerantなシステム作りを頑張りたいと思います。
最後のQAの所のQuestionに対する1つの解が、envoyのようなsidecar的なプロキシレイヤーになるのかなと思うので、来週のアドベントカレンダーでは、その辺を調べて書きたいと思ってはいるけれど、はてさて、、、