はじめに
こんにちは、Wantedly の Infrastructure Team でエンジニアをしている南(south37)です。
Wantedly では、マイクロサービスアーキテクチャを採用しています。
マイクロサービスで開発を進める上で、重要な関心ごとの1つが「多数のマイクロサービスの協調動作をどう実現するか」です。そもそもマイクロサービスとは、多数の小さなサービスが協調動作をすることでシステムとしての振る舞いを実現するものです。そのため、「マイクロサービス同士の連携方法」は、システムにとって極めて重要な技術要素となります。
例えば書籍「マイクロサービスアーキテクチャ」では、マイクロサービスの連携方法として以下の2種類があると説明されています。
- リクエスト/レスポンス
- イベントベース
このうち、分かりやすいのは「リクエスト/レスポンス形式でのマイクロサービス間コミュニケーション」でしょう。これは、クライアントマイクロサービスがリクエストを発行し、通信相手のマイクロサービスからのレスポンスを待つというものです。書籍「マイクロサービスアーキテクチャ」では、Remote Procedure Call(= RPC)や REST over HTTP などが具体例として紹介されています。
Wantedly ではこの目的では gRPC を積極的に利用しています。gRPC はハイパフォーマンスな RPC framework で、マイクロサービス間で低 latency の通信を実現することができます。「Wantedly での gRPC 活用法」については、以下のようにブログ執筆や登壇を行っていますので、そちらも参照してみてください。
gRPC Internal - gRPC の設計と内部実装から見えてくる世界 | Wantedly Engineer Blog
Real World Performance of gRPC - gRPC 利用による劇的なパフォーマンス改善 | Wantedly Engineer Blog
gRPC Development Environment - Wantedly の gRPC Server/Client 開発環境 | Wantedly Engineer Blog
もう1つの方法が、「イベントベースでのマイクロサービス間コミュニケーション」です。これは、クライアントマイクロサービスは「ある事態が起きたこと(= イベント)を通知」し、他のマイクロサービスは「興味のあるイベントを Subscribe して、イベントを検知して処理を行う」という振る舞いを指しています。
イベントベースのコミュニケーションは、リクエスト/レスポンスとは異なり、他のマイクロサービスに何かを直接指示はしません。そして、本質的に非同期であり疎結合です。イベントを発行するマイクロサービス(= Publisher)はそのイベントがどう利用されるかを知る必要は無いですし、イベントをチェックするマイクロサービス(= Subscriber)として何が存在するかを知る必要もありません。Publisher と Subscriber はお互いに相手の事を知る必要はなく、疎結合な状態を保つことができます。
イベントベースでの通信を行うアーキテクチャは Event-Driven Architecture と呼ばれます。Wantedly のマイクロサービスでも、必要に応じてイベントベースでの通信を取り入れています。
このブログでは、「Event-Driven Architecture のメリット」および「Wantedly での Event-Driven Architecture の活用方法」についてまとめたいと思います。
Event-Driven Architecture のメリットとは?
Wantedly ではイベントベースでの通信を取り入れていて、必要に応じて Event-Driven Architecture を採用していると説明しました。それでは、そもそも「Event-Driven Architecture を採用する理由」とは何なのでしょうか?
ここでは、AWS が公開する Document の1つである What is an Event-Driven Architecture? をベースに説明します。この Document では、Event-Driven Architecture の利点として以下の4つが挙げられています。
- Scale and fail independently
- Develop with agility
- Audit with ease
- Cut costs
この中でも、自分が特に強い利点だと感じているのが「1. Scale and fail independently」と「2. Develop with agility」の2つです。ここでは、この2つに絞って説明をいたします。
Event-Driven Architecture のメリット1: Scale and fail independently
まず1つめが「Scale and fail independently」です。これは、Event-Driven Architecture を採用することで、「マイクロサービス同士の疎結合な状態を保ち、障害の影響を局所化できる」という性質を指しています。
イベントベースでの通信を行う場合、一般には「Event router」と呼ばれるミドルウェアを利用します(注: Event router という用語は AWS の Document 内での用語で、Wikipedia の Event-driven architecture ページ では同等のものが Event channel と呼ばれていますし、Microsoft の Event-driven architecture style の解説記事 では Pub/sub と呼ばれています。個人的には、Pub/Sub や Pub/Sub Messaging という用語をよく見かける印象です)。Event router は、「発行された Event を複数のマイクロサービスに配信する」という機能を持っています。
イベントを発行するマイクロサービス(= Publisher)とイベントをチェックするマイクロサービス(= Subscriber)はそれぞれ Event router のみに依存していて、お互いに直接コミュニケーションは取りません。そのため、例えば Subscriber がダウンしていたり過負荷で処理が遅くなっていても、Publisher の処理には一切影響しません。これはとても重要な性質で、この性質のお陰で「Publisher の SLO(= Service-level objective、サービスの信頼性目標)を Subscriber の SLO と独立して決めること」が可能になります。
具体例を考えてみましょう。プロダクトにとってとても重要な Entity(e.g. ユーザーおよびそのプロフィールなど)を扱うマイクロサービス A が存在していて、その A に依存する形で複数のマイクロサービス B, C, D が存在するとします。B, C, D が Search 用のミドルウェア(e.g. Elasticsearch)や Cache 用のミドルウェア(e.g. Redis)を内部で保持している場合、重要な Entity の更新があった場合にはそれぞれのミドルウェアのデータを更新する必要があります。
この時、Event-Driven Architecture を採用せず、gRPC などの「リクエスト/レスポンスのコミュニケーション」で更新を行おうとするとどうなるでしょうか?この場合、「重要な Entity の更新を行っている処理」の全てに、「B, C, D へ更新リクエストを送る」という処理の実装が必要になります。このアーキテクチャを模式的に表したのが以下の図です。
図1. 「リクエスト/レスポンスのコミュニケーション」を利用したアーキテクチャの模式図
このアーキテクチャは以下のような問題を抱えています。
1. マイクロサービス A が「マイクロサービス B, C, D の障害や過負荷」の影響を受けてしまう
- B, C, D のうちどれか1つでもマイクロサービスが落ちていた場合、全体の処理が失敗してしまう。また、失敗させずに retry などを行う場合、「どのマイクロサービスでどういった retry を行うべきか」という知識をマイクロサービス A が持つ必要が出てしまう。エラーレポーティングなどもマイクロサービス A で行う必要がある。
- B, C, D のどれか1つでも処理が遅くなった場合に、マイクロサービス A の処理の latency が増えてしまう。
- 結果的に、「マイクロサービス B, C, D の信頼性が、マイクロサービスA の信頼性に影響を与える」という構図になってしまう
2. マイクロサービス A が「マイクロサービス B, C, D へリクエストを送るための知識」を持つ必要がある
- それぞれのマイクロサービスへの request parameter の組み立ての知識や endpoint の知識を持つ必要がある。また、適切な timeout 値の設定など「通信相手の想定される振る舞いの知識」も持つ必要がある。
- 新しく「Entity の変更を検知したいマイクロサービス」を作りたい場合、マイクロサービス A にリクエスト先を追加する変更を行う必要がある
上記のような問題は、結果的に「システム全体の信頼性低下」や「開発速度の低下」に繋がります。例えば、「マイクロサービス A がそのほかのマイクロサービスの障害や過負荷の影響を受ける」構造である場合、マイクロサービスが増えるほどにマイクロサービス A の信頼性は下がってしまいます。あるいは、マイクロサービス A の信頼性を下げないために、不必要に高い SLO を設定してしまって開発速度が低下してしまうかもしれません。また、「マイクロサービス A の変更」が必須の状態だと、変更対象が増える分だけ開発コストも上がってしまいます。
それでは、「リクエスト/レスポンスのコミュニケーション」を行う代わりに、Event-Driven Architecture を採用するとどうなるのでしょうか?この場合、マイクロサービス A の責務は「重要な Entity の更新を行った際に、あらかじめ定義した形式の Event を Publish すること」だけになります。「Event を取得して適切に処理をする」という責務はマイクロサービス B, C, D が持つことになります。このアーキテクチャを模式的に表すしたのが以下の図です。
図2. Event-Driven Architecture を採用した場合の模式図
このアーキテクチャは「リクエスト/レスポンスのコミュニケーション」に比べて、以下のような利点を持っています。
1. マイクロイクロサービス A は「マイクロサービス B, C, D の障害や過負荷」の影響を一切受けない
- マイクロサービス A の責務は「適切な形式の Event を Publish すること」だけであり、Publish に成功すればその時点で処理は終了する。Event がどう活用されようと、その後の処理には一切影響されない。
- マイクロサービス B, C, D の障害や過負荷は、それぞれのマイクロサービスが自分でハンドリングする。
- 注: Kafka や Google Cloud Pub/Sub のような「Subscriber Queue が存在するアーキテクチャ」の場合、Event は失われずに Queue に残る。そのため、たとえ障害があっても復旧後に溜まった Event の処理を行うことが可能。
2. マイクロサービス A は「イベントがどう利用されるか」を一切知る必要がない
- 「重要な Entity の変更を検知するマイクロサービス(= Subscriber)」を追加したい場合は、ただ対象の Event の Subscribe を行えば良い。マイクロサービス A に一切の変更を加える必要がない。
上記の特徴のお陰で、結果的に「システム全体の信頼性向上」や「開発速度の向上」を達成する事ができます。これは、Event-Driven Architecture のとても大きな利点の1つと言えるでしょう。
さて、What is an Event-Driven Architecture? の「1. Scale and fail independently」の説明に戻ります。このドキュメント中ではもう1つの要素として、「Event router が buffer として機能する」という部分にも言及しています。このお陰で、Spike が来てもシステムが止まることはなく柔軟に処理を継続する事ができます。
Event-Driven Architecture のメリット2: Scale and fail independently
次に What is an Event-Driven Architecture? の「2. Develop with agility」についてですが、実はこれは既に説明した内容で、「Publisher と Subscriber を独立して開発可能」ということを指しています(注: Document 中では、Publisher と Subscriber の間の Heavy coordination をなくす事が出来る、という表現になっています)。あらかじめ Event の format を定義しておけば、それが「公開インターフェース」となるため、Publisher は「公開インターフェースを満たす事」だけを念頭において開発出来ますし、Subscriber は Publisher の実装詳細を知ることなく開発が可能です。こういった特徴のお陰で、Event-Driven Architecture では生産性高く開発を進める事が可能です。
ここまでで、Event-Driven Architecture の利点について説明しました。適材適所で利用する事で、「システム全体の信頼性向上」や「開発速度の向上」といった恩恵を受けられる事がわかったかと思います。
次は、「Wantedly での Event-Driven Architecture の活用法」について説明したいと思います。
Wantedly における Event-Driven Architecture 活用法
Wantedly では、部分的に Event-Driven Architecture を取り入れています。具体的な例はいくつかあるのですが、ここでは「プロダクトにとってとても重要な Entity(e.g. ユーザーデータ)を扱うマイクロサービスが、その更新を Event として Publish して、いくつかのマイクロサービスがその Event を Subscribe して利用する」というケースについて説明します。
お気づきの方もいるかもしれないですが、実はこれは上記の Event-Driven Architecture のメリットとは?というセクションで説明したモデルケースと一致するものになっています。逆に言えば、そこで説明した恩恵が受けられると判断したからこそ、Event-Driven Architecture を採用しています。
Wantedly では、定期的にマイクロサービスアーキテクチャの改善を行っています。2019 年にCloudNative Days Tokyo 2019 で発表させていただいた資料 の中では、ユーザーサービスという「ユーザーデータを扱うマイクロサービス」をご紹介しました。このユーザーサービスこそが「ユーザーデータの更新」を Event として Publish する Publisher となっています。ユーザーサービスは様々なマイクロサービスから利用される重要なマイクロサービスであり、その情報を利用したい Subscriber も多数存在します。その部分に、以下のような Event-Driven Architecture が採用されています。
図3. ユーザーサービスにおける Event-Driven Architecture の模式図
「多数のマイクロサービスから依存されるマイクロサービス」というのは高い信頼性が求められますし、そこが開発のボトルネックになってしまうと開発組織全体の開発生産性が大きく損なわれてしまいます。そこで、可能な限り疎結合な状態を保って開発生産性を高く保ちつつ、高い信頼性も同時に実現するために、Event-Driven Architecture を採用しています。
まとめ
Event-Driven Architecture について、そのメリットと Wantedly での活用例についてご紹介しました。Event-Driven Architecture は適切な箇所で利用すれば、「システム全体の信頼性向上」と「開発生産性の向上」を両方実現できる優れたものとなっています。ぜひ、フラットな目線で検討してみてください。
ただし、注意しなければならないのは、Event-Driven Architecture は決して銀の弾丸ではないという事です。「リクエスト/レスポンスのコミュニケーション」と比較すると、ツールやエコシステムの成熟度合いの違い、モニタリングの難しさ、イベントを利用した際の特有の問題の存在など様々な困難も存在します。そのため、常に「Event-Driven Architecture が適切な箇所なのか?」を考える事が重要だと思います。
なお、Event-Driven Architecture の実現にあたって、気になるトピックの1つが「Event router(あるいは Pub/Sub)ミドルウェアとして何を利用しているか」という部分です。Wantedly では、この用途では Google Cloud Platform(GCP)の Cloud Pub/Sub というサービスを利用しています。Cloud Pub/Sub の詳細な使い方などは、また後日ブログにまとめたいと思います。
このブログが、システムアーキテクチャを考える際の一助となれば幸いです!