CQRS+ESって何?美味しいの?と思った時に見ていただきたい記事
こんにちは!sugitaniと申します。
Black Cat CarnivalというSNSアプリを開発しています。現在ベータテスト中です。
本稿はBlack Cat Carnivalを例にとり、CQRS+ESを採用すると何がうれしいのか?をできるだけ簡単に解説することを試みます。
CQRS+ESって何?
簡単にまとめますと
- CQRS: 書き込み部分と読み込み部分をしっかりと分ける、具体的にはデータベース分けちゃうくらいまで分けると良いこといっぱいあるよ!
- ES: CQRSやると書き込み側で起こった変更を読み込み側に伝えないといけない。たぶんイベントを伝える実装になるとおもうけど、そのイベントの台帳を基底とすると便利だよ!
となります。
前提: Black Cat Carnivalのシステム
せっかくなのでかわいいプロモーションムービーをご覧ください!
フォローがなくスワイプで探すスタイルを基本とし、一人が複数のルーム(タイムラインのような物)を持て、匿名でコミュニケーションでき、チャットシステムに近いリアルタイムなコミュニケーションができる、といった代物です。
実装としてもほぼチャットシステムです。
ほぼチャットシステムなのね、というを念頭においていただくと読みやすくなると思います。
CQRS+ESのなにが良い?何が便利?
以下はBlack Cat Carnivalのデータの流れを図にした物です。二つに分けて解説します
1. イベント発行
WriteAPIサーバはクライアントからのリクエストを受けて以下の手続きを行います
- DynamoDBからイベントログや、計算結果のスナップショット(Aggregateといいます)を読み込みます。
- ユーザーのリクエストと現在の状態を付き合わせて、問題なかったらイベントをDynamoDBに書き込みます。この操作はトランザクションを使って安全に行われます。
- RedisのQueueにイベントを乗せ、後続に発送します
①の領域において、この構造をとることのメリットは以下の通りです
- リード側とつながっておらず、負荷や障害が伝搬しない
- イベントの読み書きは単純でDynamoDBと相性が良くスケールが効く
スケールアップ特化、とも言えます。
イベント発行の補足(読み飛ばし可)
- 今回はイベント伝搬インフラとしてRedis / 正確にはk8sと組み合わせるときの便利機能が沢山あるDragonflyを利用していますが、AWSを使うのであればAmazon Kinesisを使った方がよいでしょう。今回はどうせRedisがある & ローカルで動かすのが便利、ということでRedisを採用しました
- WriteAPIは Scala3 + ZIO-HTTPを採用しました。単純RESTです。
- Event保存は j5ik2o氏が開発している event-store-adapterを利用すると簡単です。いろんな言語での実装があります。ただし今回は後述するレジューム機構のため、LSIを追加する改変を行っています。
- イベントDBへの書き込みはProtocolBufferあたりを採用すると速度と容量ともに良い具合になるのですが、スキーマ定義が面倒なのと、今回はイベント台帳への読み込みはそれほど発生しないシステムなのでJsonをZstandardで圧縮して突っ込む、という暴挙をしています(あとで苦労するぞー?)
2. イベント配送とレジューム
イベントはRedisのQueueを使って以下の2経路に配信されます
- リード側永続データを更新する系(Read Model Updaterと呼ばれます)
- ルームに関するイベントを配信する系
イベントを受信するのは一つでなくてよい、というのはCQRS+ESのうま味でしょう。
例えば、ルームに対しての投稿は以下の流れをたどることになります
- ルームのオーナーが投稿APIを呼び、検証され投稿イベントが作成・保存・配信される
- リード更新機構が永続層に保存する
- ルーム管理機構が利用者にSSE(Server Sent Event)を使って配信する。かつメモリ上のルーム状態や過去ログを更新する
これにより、仮にリード更新系が詰まってもルーム配信に影響はなく、ルーム配信系がクラッシュしてもログの保存には影響がないことになります。
またイベントには連番のイベント番号が振られており、リード更新系は再起動したときやイベント番号が飛んだときはイベントDBから読み直すことで取りこぼしや不整合を防ぎます。
またリード側の主記憶はDragonfly(Redis)という暴挙を行っています。通常であれば正気ではおこなえない所業ですが、CQRS+ESであれば正となるデータはイベントDBにあり、処理のレジュームも可能なので、RedisのSnapshotを適切にとればなんとかなると判断しました
またDragonflyはレプリケーションで利用しており、メモリに収まるデータ量を超えるか、書き込み速度が問題になるまではいくらでもスケールできます。
まとめると②の領域において、この構造をとることのメリットは以下の通りです
- リード側は一つだけなくてもよい
- リード側のデータ永続インフラはかなりのやんちゃができる
イベント配送とレジュームの補足(読み飛ばし可)
- 一言で”ユーザー情報更新機構" などとまとめていますが、実際にはDynamoDBのPartitionKey毎にStreamがあるのでかなりの数が常駐しています。
- リード更新系はRedisのトランザクション(watch)を活用し、複数のサーバで処理がかぶっても大丈夫にしています。具体的には衝突が発生したら、負けた方が同一PartitinKeyに対しての操作を一定時間放棄します(雑なCSMA/CDみたいなもの)
- リード更新系もルーム配信系もZIO2のStreamを活用しています
- ルームから利用者に対してチャットや各種制御にSSEを使う方式はSUGARというライブ配信システムでも同じ手法を使っており、Fastlyを挟めば一つのルームに20万人が殺到しても裁けることが確認できています
- リード側で永続化したデータはGraphQLを使って取得することができます。ScalaでGraphQLを扱うときはCalibanというライブラリを使うと大変楽で、例えばTypeScript+Apollo Serverと比較するとスキーマ定義を行わなくても自動で生成できて楽 / DataLoaderを持ち出さなくてもzio-queryを使って快適にデータ読み込み最適化ができて素晴らしいです。
- イベント発行時にリード側の情報が欲しくなることが高頻度でありますが、リード側を直接読みに行くとカオスが発生するので、リード側から各種データをTokenとして受け取りライト側に渡す、という仕組みにしています。
終わりに
以上がBlack Cat Carnivalを例にとったCQRS+ESのうま味の紹介となります。
どのシステムにもCQRS+ESが良いのか?と言われると断じてNOだと思います。機構を整えるのは大変ですし、リード側もライト側も実装上の制約はでてくるので不自由を感じる場面は多いでしょう。(C#であればSekibanというフレームワークがあります)。大体の場合はオーバーキルになると思います。
ただ、性能を極限まで絞り出してインフラ代を節約したいとか、負荷で困る場面がでそう、と思うのであればCQRS+ESが良いのではないか?とは思いますし、かつて作ったシステムはほとんどがそういう制約だったので、もう一度作るならCQRS+ESにするとおもいます。
ところで Black Cat Carnivalの調子はどう?
ほとんどできあがっています。
最近まではお知り合いの方を招いて内部テストをやっており、12/3からTestFlightを使った公開ベータテストに移行しました。
・・・・・・
いやー・・・・・・・・・
・・・・
厳しいですね、できれば・・・・・・助けていただきたいです。
ご参加いただける方を探すのが難しいです。是非ベータテストにご参加頂きたい。
12/05時点で50人近くの方々が参加してくださっており、作りたかった世界(人とふれあうにはにゃーんとつぶやくだけでも十分な世界)は小さく成立しているようにも見えます。楽しんでいただけているようにも見えますが、もう少し賑わうともっと面白くなる設計、かつ、そこが検証したいポイントなので、そこを目指したいところです。
CGMは利用者を増やすことが難しいことはわかっていて、SmartySmileというCGMサイトを作って全く利用者が得られなくて即終了したこともかつてはあります。
今回も"小さい資本ではどうにもならないのは必然"とは思っていましたが、出来たものは個人的には面白く内部テストでも楽しかったので、広告打てばもしかしたら?とは思いましたが、やはり厳しそうではあります。
もしよろしければ https://bcc.cc よりベータテストにご参加いただき、ご意見をいただけると幸いです。現時点ではiOSのみのベータテストとなりますが、Android版もオープンテストの審査が通れば公開できます。
読んでいただき、ありがとうございました
Discussion