イベント駆動設計について、基礎から(feat.apache kafka)
この記事では、イベント駆動の概要を、その前提となる概念を説明したうえで解説しています。
前提となる概念の説明
サーガパターン
複数のサーバーに跨る処理を、トランザクションとして実装する設計パターンを指します。複数のサーバーに跨がっているので、確実なトランザクションを実装することは恐らく、原理的に不可能だと思います。それを何とか頑張って実現しようという設計です。
そして、サーガパターンを何とか頑張って実現しようとする方法が、この後の項目で語る
- コレオグラフィ
- オーケストレーション
この二つになります。
オーケストレーション
一言でいうと、指示役(オーケストレーター)を置いて各サーバーにリクエストを出し、どこかでエラーが出たら指示役がトランザクションのための取り消し処理などを行う方法です。
ここで重要なのは、各サーバーはトランザクションになる、一連の処理について何も知らないことです。ただ、自身の持つ機能に対してのみ責任を持つので、関心の分離が実現しやすいです。
その一方で、オーケストレーターの責務が大きくなりやすく、複雑になってしまう可能性があります。
コレオグラフィ
オーケストレーションとは対照的に、指示役は置かず、各サーバーが次のサーバーに指示を出す設計です。(バケツリレーなどに例えられることが多いです。)
この「指示」というのは、必ずしも直接リクエストを送ることを指しません。(この記事で紹介するkafka[1]を利用する場合のコレオグラフィはそのような実装になります。)
ただし、基本的にコレオグラフィはクソゴミアーキテクチャと言わざるを得ません。デメリットは以下に示されたものが挙げられます。
- サーバーどうしの繋がり、連携が分かりずらい
- どこかのサーバーでエラーが起きた時が面倒
- エラー処理をしようと思うと、各サーバーは他のサーバーについて知りすぎている密結合になる
- サーバーどうしがやり取りする際の型定義の管理が面倒くさい。(これを解決するためのツールはあるので、一応解消可能)
なので、基本はオーケストレーションが推奨なのですが、kafkaを使うとなれば話は別です。上記のデメリットのうち以下二つが解消され、むしろそれは長所に転じます -
どこかのサーバーでエラーが起きた時が面倒-> kafkaならエラー処理が簡単 -
密結合-> オーケストレーションほどではないが、疎結合
特にエラー処理の容易さや、エラーが生じてもリトライ機能する機能があるので、これがkafka+コレオグラフィの大きなご利益の一つと言えます。
イベント駆動アーキテクチャとkafka
kafka
メッセージブローカーと呼ばれるソフトウェアの一種です。詳しい仕組みは、イベント駆動の概要を把握してからのほうが分かりやすいので、今はいったん「掲示板」をイメージしてほしいです。
- 誰でも書き込める
- 誰でも見れる
- 自ら発信はしない(実際はそんなことないが、イメージなのでいったん飲み込んでほしいのです)
イベント駆動のイメージ
ここでいうイベントは一般名詞ではなく特別な意味を持つ言葉です。イベントとは、kafkaに書き込まれるメッセージのことです。イベントの種類(イベントタイプ)と必要なデータが書かれていると思ってください。そして、この設計においては、各サーバー(コンシューマー)およびリクエストの発行者(イベントプロデューサー or プロデューサー)それぞれがこのイベントを書き込みます。そして、イベントタイプには各サーバーが何をやったかあるいは何が起きたかを記述します。具体例を見てみましょう。
UberEatsを例にとってみましょうか。
利用者が注文したら、アプリは「注文入りましたイベント」を書き込みます。
それを監視していた配達員アプリは注文を表示し、それを配達員が承諾したら「配達員見つかりましたイベント」を書き込みます。
その一方、商品を作る店舗も「注文入りましたイベント」を発見し、「注文承諾イベント」を書き込みます。~~~
このように、プロデューサーやコンシューマーたちは起きたことまたはやったことを書き込み、kafkaに自分が連携して処理を行うべきイベントが書き込まれた場合は処理を行い、その結果をまたkafkaに書き込む、このようなイベントの連鎖により一連の処理がなされる、これがイベント駆動アーキテクチャの概要です。
イベント駆動のご利益
以下のような耐障害性が挙げられます
リトライが堅牢かつ柔軟に実現
リトライ機能が容易に実現可能です。処理に必要な情報がkafkaで保存(永続化)されているので、容易にリトライ可能です。何回か連続で失敗した場合は、リトライを中止し、書き込まれたイベントをほかの場所[2]に退避させることが可能です。
打消し処理も自律的かつ分散的に実装可能
オーケストレーションでは、どこまで処理が進んでいるかによって打消し処理を分岐させる必要があり、オーケストレーターに大きな負荷がかかり、単一障害点(SPOF)になってしまいます。
一方、イベント駆動なら処理に失敗したコンシューマーが処理失敗イベントを書き込み、それを検知したほかのコンシューマーは実行済みの処理の打消し処理(補償トランザクション)を実行します。サービス感を独立させつつデータの整合性を保つことができます。
プロデューサーもそのイベントを検知して、失敗したことを知ることができます。
失敗がログに残る
処理が失敗した場合も、ログが残ります。また、リトライが行われたにも関わらず失敗する場合、障害やロジックの誤りなどの原因が考えられます。それらを修正したうえで、再度ログに残っている処理を再度流し込むことが可能です。
イベント駆動のデメリット
結構制約も増えます。銀の弾丸は存在しないのです。対策可能なものもありますが、手間が増えること自体デメリットでしょう。というか、対策可能というか対策必須といったニュアンスで受け取ってもらったほうがいいと思います。
対策不可
即時反映されない
即時反映(ACID特性)は無くなります。どうしようもないです。リクエスト送信後、数msのラグが必ず発生するので、フロントで対処が必要になります。
対策としては、
成功したことにして表示しちゃう(楽観的UI)や完了通知がくるまで待つ(無理やり同期処理にする)、ポーリング(完了したかをGETリクエストで定期的に確認)、処理が終わったらメッセージを送らせる(SSEというらしい)に完了通知をなどの対処が必要です。
完了まで待つ対応は、イベント駆動の特徴である非同期を消すことになります。
対策可能
処理フローが追跡困難
処理がスパゲッティ化しやすく、そうでなくとも監視役のオーケストレーターがいない分追跡が困難です。
エラーが発生しても、ログが分散してしまいます。対策としては、分散トレーシングがあります。一連の処理で発生する各イベントに同じトレースIDを付与することで、IDをもとに分散したログを集めることが容易になります。
冪等性の保証の困難
kafkaの性質上、複数回同じイベントを読んでしまいます。これはどうしようもないです。
何も対策しないと、「2回注文される」「在庫が2回引かれる」というようなバグが発生するでしょう。
全てのイベントにはIDが付与されるのですが、そのIDを管理する必要があります。そのために処理済みのイベントを保存するテーブルを用意する手間もあります。
型管理の困難
疎結合といいながら、各コンシューマーは型を厳密にすり合わせる必要があります。それぞれの発したイベントのデータをそのまま処理に使う訳ですからね。
対策としては型を管理するスキーマレジストリを導入することです。
逆に、スキーマレジストリのおかげでコンシューマはプロパティの不足のようなお話にならないリクエストのバリデーションから解放されるとも言えます。
ログが残るので重要情報は乗せれない
パスワードなどの重要な情報のやりとりにkafkaは向きません。
イベントは基本永続化されるため、パスワードや秘匿性の高い機密情報は同期的に行うのが無難でしょう。
参考にさせていただいたもの
- https://qiita.com/kotauchisunsun/items/14e21ef893de90780051
- https://zenn.dev/tatta/books/4e993c596e7dc9/viewer/4700fb
- https://www.creationline.com/tech-blog/microservices/event-driven/63887
- https://www.creationline.com/tech-blog/microservices/event-driven/63912
Discussion