履歴を中心に据えたイベント・ソーシングのメリットと仕組み
イベント・ソーシングはデータ保存の仕組みの中で、履歴をその中心に据えるアプローチです。保存された履歴による過去の調査やトラブルシューティングが可能になりますが、Akkaのアクターと組み合わせることでそれ以外にもさまざまなメリットがあります。
連載の中で紹介してきたAkkaのメリットをそのまま享受できることはもちろん、データベースのトランザクションで排他制御を行うより、アクターを使ったほうが扱いやすくパフォーマンスも出しやすい非同期処理が実装できます。また、データベースとアプリケーションの疎結合を実現しやすいので、データベースの都合に振り回されないアプリケーション設計がしやすくなります。
その他にもメリットはあるのですが、その前にイベント・ソーシングの仕組みを解説したほうがわかりやすくなるので、ここからはその仕組みと背景をお話していきます。

イベント・ソーシングでは、データ保存にデータベースだけではなく、メッセージキューなどさまざまなミドルウェアを利用できるので、データ保存に用いる層をデータベース層ではなく永続化層と呼ぶことができます。最新の状態はアクターに内包され、永続化層は履歴のみを保存するので、アクターにメッセージを投げないと最新の状態は取得できません。永続化層に最新の状態を問い合わせできないというのは、はじめてイベント・ソーシングを知る人にとっては驚きかもしれませんが、永続化層を履歴の保存に特化させることで、最新の状態を問い合わせるのにアクターと永続化層どちらにすべきか迷うことがなくなります。
Akkaとイベント・ソーシングを組み合わせる場合、最初のステップはアクターがメッセージを受け取ることです。イベント・ソーシングの中ではアクターが受け取るメッセージをコマンドと呼びます。アクターがコマンドを受け取ると、処理を開始します。

コマンドは多くの場合Web APIなど外部からの入力を起点としており、外からのアプリケーションに対する操作を表します。eコマースであれば買い物かごに入れる操作や注文確定、あるいは配送開始の記録などがコマンドに、文書管理であれば文書の保存などがコマンドとなるでしょう。
コマンドを受け取ったアクターは、その内部状態と照らし合わせて不正なコマンドでないかチェックします。もし不正が無ければ、コマンドをイベントと呼ばれる保存のためのデータ形式に変換し、永続化層に挿入します。アクターは永続化が完了してはじめて自身の内部状態を更新するので、永続化に失敗したイベントによってアクターの内部状態が更新されることはありません。
以上の流れにより、アクターは次から次にコマンドを受け取り、データ保存層には更新履歴を表すイベント列が保存されます。イベント・ソーシングでは、アプリケーションは永続化層のイベント列を更新も削除もしません。更新、削除は禁止されているので、イベント列は常に信頼できる履歴となります。これは過去の調査やトラブルシューティング、あるいは監査のような厳格さが要求される場合においても非常に有用です。
さらに、イベント・ソーシングでは先に説明した仕組みにより、イベント列を再処理すれば必ず現在のアクターの状態が復元できるので、「履歴はあるがアプリケーションの状態は復元できない」という事態は発生しません。
これが有効に働くのは、ソースコードにバグを発見したときです。ソースコードにバグはつきものですが、バグを含むソースコードでイベントを処理した場合、当然その時点でのアクターの状態も正しくない可能性があります。イベント・ソーシングではバグを修正した後に、正しいソースコードでイベント列を再処理すれば、正しいアクターの内部状態を構成できます。
アクターは何らかの障害によってダウンする可能性がありますが、その場合でもイベント列を再処理すればアクターの内部状態を復元できます。アクターの復元は別のマシン上でも可能なので、マシン自体がダウンする障害にも対応できます。
マシンをまたいだ耐障害性はアクターにとって重要です。アクターは以前の記事で説明したように、親子関係による監視と復元が可能なので、耐障害性に優れた仕組みです。しかし、イベント・ソーシングを用いない場合、マシン自体がダウンしたときにはメモリごと内部状態が消えてしまい、耐障害性が失われてしまいます。一方、伝統的な3層アーキテクチャでは、アプリケーション状態を全てデータベースに保存することで、マシンのダウンに対して耐障害性をもっていました。これは伝統的な3層アプリケーションの非常に優れた点ですが、イベント・ソーシングを用いればアクターでも同等の耐障害性を獲得できます。
最後に、イベント・ソーシングはストリーム処理パイプラインと統合しやすいメリットがあります。ストリーム処理はアプリケーション内をリアルタイムかつ大量のデータが絶え間なく流れる現在の大規模アプリケーションを構築するのに無くてはならないものです。永続化層にイベントが次々に挿入されていく特徴はまさにストリームとして表現しやすいものであるので、統合しやすいです。
イベント・ソーシングを補完する設計パターン:CQRS
ここまでイベント・ソーシングの紹介をしてきましたが、データベースを用いたアプリケーション設計に詳しい方は、問い合わせに対して複数件のデータ取得を行う場合どうするのか疑問に思った方もいるでしょう。
イベント・ソーシングのみを採用したアプリケーションでは、永続化層はイベント履歴のみを保存するので、データの最新状態を読み込むにはアクターにメッセージを投げて返信を受け取る必要があります。しかし、これでは抽出や集計の際に困ってしまいます。データベースと違い、アクターは複数のアクターから同時にデータを抽出、集計するのに向いていないからです。

この問題への対処法は永続化層へのコマンド(書き込み)側とクエリ側を分け、コマンド側はイベント・ソーシングを、クエリ側は別の設計を用いることです。この設計パターンはCQRS( Command Query Responsibility Segregation)と呼ばれます。

CQRSを採用する場合、クエリ側はアプリケーションで利用するクエリに最適化されたミドルウェアを採用します。多くの場合はリレーショナル・データベースを利用してSQLでクエリを行いますが、アプリケーションの要件によって自由に選べます。
コマンド側とクエリ側で異なるミドルウェアを採用でき、その場合、それぞれに合わせた性能特性のミドルウェアを選ぶことでシステム全体のパフォーマンスを向上できます。コマンド側は書き込み特性に優れ、クエリ側はクエリ特性に優れたミドルウェアを選びましょう。
CQRSが書き込み側と読み込み側を疎結合にすることで生まれるメリットは、以下のMicrosoftの記事がよくまとめているので引用します。
コマンド クエリ責務分離 (CQRS) パターン - Microsoft Azure :
(https://docs.microsoft.com/ja-jp/azure/architecture/patterns/cqrsより)
- 読み取りと書き込みのワークロードが不均衡になりやすいため、パフォーマンスやスケールの要件が大きく異なってくる可能性があります。
- 読み取りと書き込みのデータ表現が一致しないことがよくあります。具体的には、操作の一部としては必要ないものの、正しく更新しなければならない追加の列やプロパティなどです。
CQRSは大掛かりな設計パターンです。実装や運用の難易度が高いので、CRUDで十分な単純なシステムには向いていません。特にコマンド側とクエリ側で異なるミドルウェアを採用する場合、その運用コストは格段に大きくなります。365日動くシステムにはトラブルがつきものであり、ミドルウェアの特徴をしっかり理解していないとトラブルシューティングはできません。あなたが作るシステムにCQRSが必要かは慎重に見極めてください。
Akkaではツールが揃っている
ここまで紹介してきたイベント・ソーシングですが、これは設計パターンであり実装まで提供しているのは、私が知る限りではAkkaが唯一のライブラリです。 自分でこの仕組みを実装するのは、いくらメリットが大きい設計パターンとはいえハードルが高いものです。
アプリケーションの要件から、イベント・ソーシングやCQRSが必要となったら、ぜひAkkaの活用を検討してください。学習コストや運用コストは高いものの、本当にそれらの設計パターンが必要なシステムなら、Akkaは選択肢の第一候補に入るツールとなるはずです。
