エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

イベント駆動アーキテクチャの勘所

こちらはエムスリーAdvent Calendar 2022の24日目の記事です。

こんにちは、デジスマチームの田口(id:ken-tunc)です。 私達のチームではQRコードによる受付や自動後払いなど、新しい診療体験を提供するデジスマ診療というサービスを開発しています。

開発メンバー6人と小規模のチームですが、毎週のように新機能をリリースしています。 また、ユーザー数も非線形的な成長を遂げており、システムのトランザクションは日に日に増加しています。

IR資料「2023年3月期第2四半期決算発表資料」より

このようなスピード感のある開発を実現できている要因はいくつかありますが、この記事ではそのうちの1つであろうデジスマチームで採用しているアーキテクチャについてまとめていきます。

デジスマ診療のアーキテクチャ

デジスマ診療のアーキテクチャの概要については下記資料に載っているので、是非一度ご覧ください。

デジスマチーム紹介資料

speakerdeck.com

デジスマ診療ではマイクロサービスアーキテクチャ、Amazon SNS/SQSを利用したイベント駆動アーキテクチャを採用しています。

「受付」や「問診」など各業務ドメインを扱うマイクロサービスAPIがEKS上で動いていて、データを更新したタイミングでイベントをSNSに送信します。 イベント起因で何か処理を挟みたい場合、lambdaや他のAPIからSQSを経由してイベントを受信し、処理を実施します。

非同期メッセージングのメリットはいくつかありますが、個人的にはマイクロサービスから他コンポーネントへの依存関係を少なくできることに特に恩恵を受けていると思います。

例えば「予約が新規に作成されたタイミングで医療施設に通知を送信する」という機能を新しく実装する場合、「予約作成イベント」を受信するキューとlambdaなどのジョブを用意すれば良く、受付APIには全く手を加える必要がありません。 このように、複雑になりがちなマイクロサービスから依存するものを少なくすることが、システムの拡張性を高めていると感じています。

他にもスケーラビリティに優れていたり、メッセージキュー毎に監視やリトライを柔軟に設定できたりというメリットもあります。

イベント駆動アーキテクチャの設計で気を付けていること

デジスマチームで設計・運用を試行錯誤していくなかで、こういう設計にすると良いのではないか、という知見が多少溜まってきました。 先述したメリットを享受するために、現在システムの設計で気をつけていることについてまとめます。

イベント毎に一意なIDを付ける

Amazon SNSで標準トピックを利用している場合、ベストエフォート型の重複防止となっているため、イベントが重複する可能性があります。 つまり、実際にはデータを一度しか更新していないにも関わらず、イベント自体は複数存在することが有り得ます。*1

この状態をしっかり検知できるよう、イベント毎に実際の操作に対して一意に定まるようにIDをペイロードに設定しています。 例えば、予約を新規に作成した場合は ReservationCreated::{予約ID} 、予約を更新した場合は ReservationUpdated::{予約ID}::{更新timestamp} のようなフォーマットのIDを設定します(予約更新の方にtimestampが含まれているのは、「ある予約を更新する」という操作は複数回行われることがあり、各操作を区別するために付けています)。

イベントを受信する処理は冪等になるように

先述したように、Amazon SNSで標準トピックを利用している場合はイベントが重複する場合があります。 イベントを重複して受信しても問題ないよう、イベントを処理するロジックは冪等、つまり2回以上処理しても同じ結果になるように実装しています。

特にメール送信やpush通知など、実行する度に新しくリソースが作成されてしまうような処理はイベント毎に設定したID単位で冪等になるように設計しています。*2

また、非同期メッセージングにAmazon SQSを利用することでリトライが容易になり、処理を冪等にしておくことで失敗した場合はとりあえずリトライ、といったことが可能になります。

受信するメッセージの順序を考慮する

Amazon SNSで標準トピックを利用している場合重複防止がベストエフォートであることを述べましたが、順序付けに関しても同じくベストエフォートになっています。 極端な例ですが、予約を新規作成した後にキャンセルした場合、新規作成とキャンセルのイベントが逆順に配信される可能性があります。

イベントの順序によって不整合な状態が生じてしまうような処理の場合は注意が必要です。 この場合、例えばイベントのペイロードには各種データのIDだけ格納するようにしておき、ジョブでは毎回IDをキーに各APIからリアルタイムに状態を取得する、といった実装が必要になります。

確実にイベントを送信する

ここまで書いたように、イベントを受信する側はとにかく来たイベントに対して処理をすれば良い、というような設計になっています。 一方で、イベントが送信されなかった場合は、必要な処理が行われなくなってしまいます。

デジスマ診療では、このような「データが更新されているのにイベントが送信されていない」という状態を避けるため、イベントを送信してからトランザクションをコミットするという手段を取っています。 つまり、イベントの送信に失敗した場合はDBトランザクションをロールバックすることでイベントが送信されたことを保証しています(ただし、この方法はAmazon SNSに障害があるなどイベントの送信が失敗するような場合、システムが機能しなくなります。現在このようなリスクを回避するためのリアーキテクチャも検討しています)。

最後に

今回はイベント駆動アーキテクチャを設計・運用していくなかで得た知見についてまとめました。 上手く設計すれば、業務ロジックがどんどん複雑かつスケーラビリティを要求されるような場面でもスピードを落とさずに開発できるような土台を作ることができると思います。 また、イベントが伝播していく様を観察するのもとても楽しいです。

We are hiring!!

エムスリーではエンジニアを絶賛募集中です。チーム紹介資料を読んで興味を持たれた方も、そうでない方も是非ご応募ください!

jobs.m3.com

*1:FIFOトピックを利用する手もありますが、性能のや料金面の関係でなるべく標準トピックを利用したいのです。

*2:今回は詳細については割愛しますが、AWSのドキュメントでも紹介されています。