AWS データベースブログの記事「Amazon DynamoDBによるCQRSイベントストアの構築」を勝手に読み解くスライドです。
© ChatworkCQRS Meetup 【Chatwork × ZOZO】2022年11月25日 Chatwork株式会社AWS データベースブログの記事「Amazon DynamoDBによるCQRSイベントストアの構築」を勝手に読み解く
View Slide
会社概要会社名Chatwork株式会社代表取締役CEO山本 正喜従業員数284名(2022年6月末日時点)所在地東京、大阪、ベトナム、台湾設立2004年11月11日2
コーポレートミッション働くをもっと楽しく、創造的に人生の大半を過ごすことになる「働く」という時間において、ただ生活の糧を得るためだけではなく、1人でも多くの人がより楽しく、自由な創造性を存分に発揮できる社会を実現する3
Chatwork は日本最大級のビジネスチャットサービス3月リリース30万社突破!20万社突破!導入社数34.3万社を突破!(2021年12月末日時点)10万社突破!4※Nielsen NetView 及びNielsen Mobile NetView 2020年6月度調べ月次利用者(MAU:Monthly Active User)調査※調査対象 44サービスはChatwork株式会社にて選定
2016年末 CQRS/ESシステムをリリース5● Akka+Kafka+HBaseで実現● Chatworkではダブルコミットはしない事例紹介されている本
自己紹介6● Chatworkのテックリード。10歳で初めてプログラミングに触れる。SIとしてさまざまな現場での業務を経験した後、2011年より某D社、2013年より大手ソーシャルゲーム企業で、それぞれScalaやドメイン駆動設計を採用したシステム開発に従事。2014年7月よりChatworkに参画。現在はChatwork次期アーキテクチャのプランニングや設計、開発に携わる。@j5ik2o
AGENDA以下のAWS Blogを解説します。「Amazon DynamoDBによるCQRSイベントストアの構築」アジェンダ
イベントソーシングとCQRSの概要1
CQRSのアーキテクチャ9● WRITE(コマンド)モデルはコマンドを処理する集約で構成される● READ(クエリ)モデルはクエリを受け取り、そのクエリをマテリアライズドビューに対して適用するもので、読み取り要求をサポートする
コマンドと集約とイベント10● ドキュメントでは、Widgetのリネームが例示されているChangeWidgetName(コマンド)WidgetNameChanged(イベント)Widget(集約)
DynamoDBによるイベントストアの設計2
DynamoDB12● RDBなどを使ってもイベントストアは構築できるが、DynamoDBを使うと以下の主な利点がある○ サーバレスで、独立性が高くスキーマレスな性質を持つイベントをスケーラブルに書き込みができる○ 変更データキャプチャ(CDC)はDynamoDB Streams, KDS for DynamoDB
DynamoDBのイベントストアで使うエンティティタイプ13● Aggregate○ ドメインオブジェクトをカプセル化する責務● Event○ 何かが起こったことを示すイベント。原則的に不変● Snapshot○ 特定の時系列ポイントまでに起こったイベントから導出された状態を示すスナップショット。スナップショットを使うとランタイムで全てのイベントの履歴の保持やロードが不要になる。
Eventテーブル14● PartitionKey(PK)はWidget集約の識別子(ID)● Sort Key(SK)はイベントの時系列番号=シーケンス番号(集約単位で一意である必要がある)● WidgetCreated → WidgetNameChanged → WidgetDescriptionChanged● イベントのペイロードは JSON にシリアライズされた状態で保存。ProtocolBufferなどを利用する
Aggregateテーブル15● 集約の現在の状態を示すテーブル● PKは集約のID● last_eventsは未処理のイベント集合● 楽観的ロックのためにversionを使う
Snapshotテーブル16● 過去の集約状態を示すテーブル● PKは集約のID● SKはシーケンス番号● event_numberはスナップショット作成時処理されたイベント数○ イベント数なの?○ シーケンス番号のほうが正しいと思われる
集約読み込みのアルゴリズム3
● 集約ルートの項目を取得● 現在の集約の状態を準備● 最新のスナップショットを読み込む● スナップショットに取り込んでいない残りのイベントを読み込む集約読み込み時(リプレイ)のアルゴリズム18
● GetItemを使い、集約IDをもとに、Aggregateテーブルから関連する項目を取得する● 見つからない場合はクライアントにエラーを返す①集約ルート項目を取得する19
● 未処理イベント(last_events)があればPutItemする②現在の集約の状態の準備20
● ScanIndexForward:false + Limit 1で最新のSnaptshotテーブルを読み込む③最新のスナップショットを読み込む21
● PK = 123 and SK >event_number でEventテーブルをクエリする④差分のイベントを読み込む22
集約の保存アルゴリズム4
● 保存されるのは最新のステートではなく未処理のイベント(last_events)● versionで楽観的ロック①集約ルート項目を保存する24
● 集約の現在の状態、具体的な項目を保存する● Snapshotテーブル自体への保存はオプション②スナップショット項目の保存(任意)25
集約の読み込みと保存の例5
● CreateWidgetのコマンドを受理・処理したら、AggregateテーブルにWidgetCreatedを追加するCreateWidgetコマンド27
ChangeWidgetNameコマンド28AggregateテーブルEventテーブル● Aggregateテーブルのlast_events(WidgetCreated)をEventテーブルを移す● WidgetNameChangedをlast_eventsに追加する
● Aggregateテーブルのlast_events(WidgetNameChanged)をEventテーブルを移す● WidgetDescriptionChangedをlast_eventsに追加するChangeWidgetDescriptionコマンド29AggregateテーブルEventテーブル
DynamoDB Streams によるイベントストアの変更データキャプチャ6
DynamoDB Streams によるイベントストアの変更データキャプチャ31● Eventテーブルの変更をDynamoDB Streamsを通じて変更を取得できる○ 変更をキャプチャできるのは1度だけ○ キャプチャ前のデータは24時間後に消えるDynamoDBApplication(DynamoDBStreams Client)Event をストリーム経由で読み込む
私なりのレビュー7
● スケーラビリティを犠牲にするトランザクションをいかに避けるかという観点が盛り込まれている○ 集約が複数のイベントを発生させた場合でも単一の書き込みにできる○ 集約状態+上限数を設けた未処理イベントの組み合わせでWCUをキャップできる前提) 書き込みのスケーラビリティを犠牲にしない設計戦略33
懸念) 集約を更新しても、集約を読まないとイベントがCDCできない?34● 集約を更新しても、イベントはすぐにイベントテーブルに保存されない。CDCできない?● 集約を読み出さないと、イベントが流れない?集約が読まれるタイミングで EventのPutItemが起きる…イベントはAggregateテーブルに一時的に保存されるイベントはAggregateテーブルからCDCする
懸念事項)リプレイ中のPutItemがコンフリクトするのでは?35● PutItemする複数スレッドで更新が重なりそう。複数回、同一イベントが発生しないのか?● 読み込み動作中に書き込み動作を行うことはCQS違反ではあるEventをCDCしないなら、Eventの書き込みは後勝ち?になるだけ
懸念事項) リプレイ中のPutItemがコンフリクトするのでは?(図解)36サーバー1getAggreateState(id=1)サーバー2getAggreateState(id=1)AggregateEventEvent A { PK: 123, SK: 1, created: 1, name: WidgetCreated, payload: { name: WidgetCreated }Event A’ { PK: 123, SK: 1, created: 2, name: WidgetCreated, payload: { name: WidgetCreated }Event A’ Event A後勝ち?イベントをCDCすると問題がある?イベントテーブルから CDCしないのでこの問題は無害
● あなたの要件次第。このドキュメントもその前提で読む● 1度に格納するイベント数を100件までにすれば、集約+イベントという非正規化テーブルも考えられるすべてに合理的な設計戦略はない37
FYI) トランザクションを使う方法もシンプル38● Aggregateテーブルのlast_eventsを廃止する● Aggregate + Events でTransactWriteItems(PutItem) + versionで楽観的ロック。ただしスケーラビリティが犠牲になるサーバー1updateAggreateState(id=1)サーバー2updateAggreateState(id=1)AggregateEventTransactWriteItems(PutItem) + version optimistic lock
FYI) 1コマンドによってNイベントが発生するか?39● ないとはいえないが、懐疑的です● カート追加コマンドで値引きイベントが生じるかどうか。責務の観点からはそういうことは起きない可能性が高い。意図しない副作用ではないかAddCartItem(コマンド)CartItemAdded(イベント)Cart(集約)CartItemDiscounted(イベント)コマンドにあっているイベントなのか意図しない副作用ではないか?
FYI) 1コマンドによってNイベントが発生するか?40● 値引きコマンドの意図を明確したインターフェイスに変更するにはポリシーを使う● 非イベント駆動ならユースケース内で順番にコマンドを実行するフローを制御するAddCartItem(コマンド)CartItemAdded(イベント)Cart(集約)CartItemDiscounted(イベント)DiscountCartItem(コマンド)DiscountPolicy(ポリシー)
結論8
● 特定の言語/フレームワークに依存しないCQRS/ESの実装モデルが公開されたことは、CQRS/ESの認知・普及に一定効果があると思われる● トランザクション使用の有無で設計戦略が大きく変わりそう○ スケーラビリティを重視するなら今回の設計パターンは有用○ そうでないならトランザクションのパターンも検討するまとめ42
Chatwork 株式会社では、日本全体を DX する ことの実現に向けて、全方位でエンジニアを募集しています!We are Hiring !!!43働くをもっと楽しく、創造的に
44チャットサービスづくり ≒ ハイトラフィックとの戦い"落とさないこと" を意識してインフラを構成DAU 100 万人 が 1 日平均 8 時間利用!470万ユーザー突破!1 Web アプリという枠組みではもはやなく、仕事を支える 一種の社会基盤 というフェーズへ(サービスダウンすると災害情報に載ることも …)
45社会基盤を見据えたアプリケーション設計45stateShardShardShardRegionRoomAggregateActorJournal DB(DynamoDB)SnapshotStore(S3)MessageBusRMU Read DBReadAPIReadAPIReadAPIControllerUseCaseWrite APIServerWrite APIServerWrite APIServerakka-clsuter他の MS へlogicClientID = 1サーバーサイド・チームクライアントサイド・チームリアクティブシステム と CQRS / ES を反映したアーキテクチャを採用(クレジット決済基盤 / 銀行送金基盤 / 証券取引基盤などで実績あり)● 非同期・ノンブロッキング● スーパービジョン● 位置透過性● ステートフル● DBとの完全な同期によって読み込み不要● ワークロードのパーティショニング● 正規化されたデータ構造を扱う● ネットワーク分断時は一貫性を重視コマンドサイドではドメインロジックを実行してドメイン状態を変える機能のみを提供するクエリサイドはドメインイベントをもとにクライアントにとって都合のよいリードモデルを構築する● 非同期・ノンブロッキング● ステートレス● 非正規型データを扱う● ネットワーク分断時は可能性を重視● ラムダアーキテクチャでも十分可能MessagePostedMessageDTOPostMessageMessageDTOID = 2ID = 3
参考:代表的な技術スタック46