なぜEvent Sourcingなのか

加藤潤一氏(以下、加藤):「なぜEvent Sourcingなのか」という話で、Event Sourcingの場合はどうなっているかというと、CRUDのステートソーシングは、最新のエンティティを上書きする考え方です。Event Sourcingは、そのとき発生した変更を追記していく考え方になります。

「状態はどういうふうに作るの?」という話ですが、イベントから状態を導出する考え方です。関数にイベントをapplyしていくと、そのときの状態に対してイベントをapplyするかたちになるので、どんどん状態を作って更新していける。すべてのイベントがapplyされると最新状態になるような考え方です。

そのイベントの必要性はコマンドとクエリが分離されていますが、結果的に統合しないといけないので、イベントが使わるような話です。

なぜイベントを使うのか

“イベント”と言っているものは、もともとはDDDのドメインイベントという考え方からきていて、イベントそのものは過去に起きたできごと、ドメイン上のイベントを意味します。 一般的には動詞の過去形で表現され、イベントからコマンドを想起可能です。例えば「貨物の出荷みたいなものだと、過去形を現在形に直せばだいたい想起可能ですよね」みたいな話があります。

コマンドとイベントは似ています。コマンドもアクターみたいなものに投げられて、そこでコマンドが拒否されることがあります。アクターの中で、このコマンドは受理できるとなった場合に、拒否されずに受理されると、状態が変わってイベントが生成されます。そのイベントはEvent Sourcingだとディスクとかに保存されます。そのため、コマンドは拒否されるかもしれませんが、イベントはすでに起こったことを示します。

なぜイベントを使うかは簡単で、コマンド側からクエリ側に変更を伝搬させないとインテグレーションできないからです。一番わかりやすい方法としては「ポーリングしたらええやん」という話がありますが、変更がないときもポーリングで負荷をかけてしまうので、ポーリングは絶対スケールしません。

同時接続数が増えて、ポーリング自体も増えると、なにもタスクがないのにコマンド側に負荷をかけている。相手側のポーリング先に負荷をかけてしまうので、スケールしないと言われています。そのためどうするかというと、イベントをPub/Subする。「コマンド側からクエリ側を呼び出すので、クエリ側はなにもしないてもいいよ」「待っててね」という話です。

こういった考え方は、連携する方法としては「Event Sourcingを使うといいよ」みたいものがありますが、ほかにもCDCとOutboxを使うパターンがあります。今日は紹介しませんが、興味があったら検索してみてください。結局はEvent Sourcingとほとんど同じになっちゃいます。

僕のブログにも書きましたが、Event Sourcing以外に現実的な選択肢はない感じです。そのため、CQRSやるということは、ほぼEvent Sourcingをやることになります。

イベントの利点と欠点

利点と欠点です。、先ほども話しましたが、イベントは更新されず追記のみなので、ロックが不要です。ロックはどこかでトランザクション管理みたいなものをやらないと、不正なイベントが追加されたりしたらまずいと思います。例えば、create したあとにupdateがこないといけないのに、updateが先にきてcreateがきちゃうようなイベントの並びは許容できないと思います。Akkaの場合は、アクターがそれを管理します。

そういった問題はありますが、それをクリアしたあとのイベントの保存、永続化という部分でいうと、ロックが不要で追記のみになるので、スケーラビリティを確保しやすいです。

特定の時点のReadModelをイベントから導出できます。先ほど言ったような考え方で、いつでもReadModelを、その時点のReadModelを取り出せます。

ドメインイベントがあればReadModelの設計はいつでもやり直せます。イベントを絶対消してしまうことはできませんが、ReadModelの設計をミスしてしまったのであれば、もちろん再構築コストはかかりますが、いつでもイベントからReadModelの新しい設計に基づいて作り直すことはできます。

こういったイベントが監査ログや行動履歴の分析に利用しやすい副次的な効果もあります。ただし、大量のイベントから状態をリプレイする際に時間がかかってしまうことはあるので、Akkaではアクターの最新状態をスナップショットとして保存する機能があります。

スナップショットに加え、それ以降の差分イベントでリプレイ時間を短縮できる機能が備わっています。原則的にすべてのイベントをストレージに保存する必要があるところが、抵抗を感じるところです。

一応、スナップショット保存時に古いイベントを消すこともできます。本当にそれを消しても大丈夫かは、ビジネス的な観点で考える必要はありますが、技術的には古いイベントを消すことは可能です。

リアクティブシステムとCQRSを反映した新アーキテクチャ

やっと新アーキテクチャの話です。リアクティブシステムとCQRSを反映したアーキテクチャを考えています。これは検証環境で開発中のもののアーキテクチャ図です。一番左側にAkka-Clusterがあり、Write APIという枠があると思いますが、これがそのコマンドサイドです。

コマンドサイドとクエリサイド、ちょうどその真ん中より右のRMU(Read Model Updater)で境界が切れていて。左側のWrite API、Message BusまでがWrite APIのチームで、右側のRMUとRead APIとClientがクライアントサイドのチームというかたち。今の実際のチームはクライアントサイドのチームがRead APIも開発する構成になっています。

コマンドサイドはドメインロジックを実行してドメイン状態を変える機能のみを提供します。例えば、でメッセージ投稿やタイトルの変更はChatworkのルームなどのWrite API側で受けます。真ん中にRoomAggregateActorがいますが、PostMessageみたいものを受け取ると、Write API側からこのRoomAggregateActorにPostMessageというメッセージが届きます。

このアクターは軽量プロセスなので、起動している状態になっています。そのメッセージが届いたら、起動している状態に対してメッセージがきて、ロジックが反応します。ステートはオンメモリでもっているので、PostMessageというコマンドが受理されたら、MessagePostedというイベントがJournal DBに保存されます。

今のところDynamoDBでやっていますが、DynamoDBの場合だと、DynamoDB Streamsなどを使って、Message BusというStreamを流す中間のメッセージのバスみたいなものがあって。Kafkaを使っていますが、KafkaからRMUにメッセージをconsumeして、ReadModelを構築します。メッセージのDTOなどをメッセージDBから取り出して、Read APIがクライアントからの要求に応じて結果を返すようなことをやっています。

アクターが起動している状態の利点

このアクターが起動している状態のなにがうれしいのか。1つ特徴的なものが、Journal DBはイベントがどんどん蓄積されていきますが、データベースの状態とRoomAggregateActorの状態は完全に同期されている状態なので、Postであれはあまり関係ありませんが、UpdateMessageなどだと、既存のエンティティを取得して、それに対して状態を変えて、新しい状態をDBに保存し直すみたいなことがよく起きます。

最初にReadが発生して、そのエンティティの状態を変えてもう1回アップデートし直すようなことをよくやると思いますが、Akkaの場合はデータベースと状態が完全に同期されているので、最初の読み込みがなくなります。

そのため、コマンドを受信したらビジネスロジックをそのまま実行して、変わった状態はEvent Sourcingなので保存しない。アクターがオンメモリでもっているのが正しい状態なので、データベースはイベントを保存するだけです。

真の状態というのはアクターがもっていて、データベースはただのバックアップという扱いです。メモリでドメインの状態がすぐ手元にあるからこそ、ビジネスロジックが書きやすかったり、設計しやすかったりする特徴があります。そういった利点を活かして、ドメインモデルを作って実装していけるのは1つ魅力的なところです。今考えているものは、ざっくりこういう考え方に基づいています。

リアクティブシステムは組織戦略と一緒に考える必要がある

先ほども話がありましたが、本日紹介した技術はアメリカとかだと「キャズムを超えてる」みたいな、「なんだと?」みたいな話があって。弊社で使っている技術を囲ってみました。「この技術を使ったからどうだ」という話でもないんですが、トレンド的に「なんか日本とアメリカでは、だいぶギャップがあるな」みたいな話はあります。

日本語の書籍が出ないと、みんな手を動かさないようなところがあるのかもしれませんが、書籍が5年・10年遅れたりはよくあることなので、日本でも事例の1つになっていければいいかなと思っています。

リアクティブシステムを目指すには、それなりに複雑な仕組みが必要になってきます。もちろん高い技術力も求められます。全体が複雑になりすぎないように気をつけることもしないといけないので、濃淡つけていけるように、というところで四苦八苦しながらやっているところですね。

あと、技術の話ばかりをしましたが、技術的な話だけではなくて。いきなりマイクロサービスでわかってないのに最初から分けて始めるのではなく、はじめは分けやすいように1つから始めて、事業やビジネスやユーザーのことをわかっていきながら分けていくみたいことが好ましいと思っていて。

そういった部分で、組織的な問題もあると思っています。こういったことは組織戦略の問題と合わせて考えていかないといけないので、まだまだやることはけっこういっぱいあるなという印象です。

事業的にも、技術的にも成長の余地がけっこうあるので、プロダクトをともに支えてくれるエンジニアを募集中です。仕事はいろいろあるので、なにかあれば気軽に相談ください。

今日はありがとうございましたということで、ご清聴ありがとうございました。

質疑応答

司会者:ありがとうございました。Twitterからもいくつか感想などをもらっているので、私からちょっとだけピックアップしたいと思っています。

司会者:「メッセージパッシング、非同期、失敗を受け入れる……。Bulkheadingの考え方という最近のソフトウェアの思想が、どんどんErlangの思想に近くなってますね」というコメントが、私としてもすごく印象的だなと思っていて。

私は個人的にこの手のプログラミングを温故知新で勉強したいなと思って、最近『すごいErlangゆかいに学ぼう! 』とかを買っているので、このあたりを読んでいるとね。

加藤:そうなんですよね。Erlangの仕様とか見るとけっこうびっくりしますが、そういった昔から可用性を考えて言語とかを設計すると、こういうふうになるのかなって。そういう意味では、本当に学ぶべきところはあると思います。

司会者:そうですね。コロナ禍でなかなか家の外に出られないと思いますが、こういう本などをぜひ読んで時間を過ごしてもらえるといいのかな、なんて思っています。