Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AWSでCQRS Event Sourcing するにはどうすればいいのか

AWSでCQRS Event Sourcing するにはどうすればいいのか

世の中にはクラウドを採用していても、スケーラビリティのないサービスを開発・運用しているエンジニアは少なくないと思います。過去の私もその一人でした。CQRS/Event Sourcingはその解の一つです。私自身も2016年にCQRS/ESに出会って以来、AWS上でその根本的な課題に取り組んで来ました。その経験を生かしてAWSでの実現方法について解説します。

かとじゅん
PRO

August 12, 2021
Tweet

More Decks by かとじゅん

Other Decks in Programming

Transcript

  1. そろそろマネージド、
    クラウドネイティブで行こう!
    2021/08/12
    Chatwork株式会社
    テックリード
    加藤潤一
    AWSでCQRS/Event Sourcing
    するにはどうすればいいのか
    1

    View Slide

  2. © Chatwork
    ● @j5ik2o
    ● 属性
    ○ Chatwork テックリード
    ■ 次期アーキテクチャのプランニングや設計・
    開発
    ○ 職歴
    ■ SIer → ドワンゴ → グリー → Chatwork
    ○ DDD, CQRS/Event Sourcing
    ○ Scala, Rust
    ● 最近はまっていること
    ○ Akka,Actixなどを参考に、Rustでアクターモデル
    を実装すること(習作目的)。既存実装は50弱ある
    (えっ…)
    自己紹介
    2

    View Slide

  3. © Chatwork
    セッション概要とアジェンダ
    ● セッション概要
    ○ 世の中にはクラウドを採用していても、スケーラビリティのないサービスを開発
    ・運用しているエンジニアは少なくないと思います。過去の私もその一人でし
    た。CQRS/Event Sourcingはその解の一つです。私自身も2016年にCQRS/ESに
    出会って以来、AWS上でその根本的な課題に取り組んで来ました。その経験を生
    かしてAWSでの実現方法について解説します
    ● アジェンダ
    ○ CQRS/Event Sourcingの概念理解編
    ○ CQRS/Event Sourcingのアーキテクチャ・実装編
    ■ 実装レベルでどうするか
    ■ ツールは何を使えばよいか
    ■ AWSでどうするとよいか
    ■ Chatworkでの検討中のアーキテクチャ
    ○ まとめ
    3

    View Slide

  4. © Chatwork
    概念理解編
    4

    View Slide

  5. © Chatwork
    CQRSとは
    5

    View Slide

  6. © Chatwork
    CQRSとは
    ● Command and Query Responsibility
    Segregation = コマンド・クエリ責務分
    離 のこと。分離というより隔離という解
    釈が正しい
    ● コマンド(書き込み)とクエリ(読み込み)を
    完全に隔離することを意味する。単にド
    メインモデルをコマンド用・クエリ用に
    分割することではない
    ● CQRSはDDDを前提とします(ドメインか
    ら本質的ではないクエリ責務を排除する
    ための設計パターン)。詳しくは CQRS
    Documents by Greg Young を参照のこと
    6
    Write DB Read DB
    Interface Adaptor
    Command Processor
    Domain
    Interface Adaptor
    Interface Adaptor
    Read Model Updater
    Query Processor
    Command Side Query Side
    Read Model Updater
    Client

    View Slide

  7. © Chatwork
    コマンドとクエリでは要件が異なる
    データ構造だけではなく他の要件も異なるので、コマンドとクエリをそれぞれを隔離する
    7
    コマンド クエリ
    一貫性/可用性 トランザクション整合性を使い強い一
    貫性を重視する
    結果整合を使い可用性を重視する
    データ構造 トランザクション処理をおこない正規
    化されたデータを保存することが好
    まれる(集約単位など)
    非正規化したデータ形式を取得するこ
    とが好まれる(クライアント都合のレス
    ポンスなど)
    スケーラビリティ 全体のリクエスト比率とごく少数のト
    ランザクション処理しかしない。必ず
    しもスケーラビリティは重要ではない
    全体のかなりのリクエスト比率を占め
    る処理をおこなうため、クエリ側はス
    ケーラビリティが重要

    View Slide

  8. © Chatwork
    それぞれの特性にあった
    ストレージを使うべきだが…
    8

    View Slide

  9. © Chatwork
    RDBへの書き込みがスケールしない問題
    ● Writeのシャーディング。自前は無理スジ
    ○ Write Sharding: Writer, Read Repclica
    のセットを分割して書き分ける
    ○ Vitess(ヴィテス)
    ● いずれにしてもアプリケーション(もしくはミド
    ルウェア)で書き込みを分割する。柔軟なクエリ
    ができなくなるという代償を払う必要がある
    ○ 書き込むデータからヒントを得て、どの
    DBに書き込むかを決める
    ○ 分割されたデータどうしでは結合するク
    エリはできない
    ○ 2台→4台→8台と手動でWriterを増やす
    さいデータの移動が必要になる
    9
    9
    Writer
    アプリケーション
    A-1
    B-1
    A-2
    B-2
    ReadReplica
    ReadReplica
    Writer
    アプリケーション
    A-1
    A-2
    ReadReplica
    ReadReplica
    B-1 A-1
    B-2 A-2
    Writer
    B-1
    B-2
    A-1
    B-1
    A-2
    B-2
    A-1
    B-1
    A-2
    B-2
    そもそもRDBが向かない要件をRDBで解決しよう
    として複雑化する…

    View Slide

  10. © Chatwork
    NoSQLでRDBのクエリのような使い方をしてしまう問題
    ● NoSQLはハッシュキーでスケールアウトできる
    ○ ハッシュキーで自動的にシャーディング(パー
    ティショニング)される
    ○ ただしキーでしかエンティティを解決できない
    ● エンティティの属性でもクエリしたい…
    ○ GSIを多用する
    ■ 個数の上限がある
    ● 上限があるなら、転置インデックスを作ろう
    ○ エンティティの更新以外にインデックスデータ
    も更新する必要がある
    ○ エンティティの取得の前にインデックスの解決
    が必要になる
    10
    DynamoDB
    アプリケーション
    A-1
    B-1
    A-2
    B-2
    DynamoDB
    { A-1, 技術部, KATO } B-1
    { A-2, 総務部, SATO } B-2
    GSIで部署名で検索できるように

    EMP
    { A-1, 技術部, KATO }
    { A-2, 総務部, SATO }
    EMP_DEPT_IDX
    { 技術部, [ A-1, A-3 ] }
    { 総務部, [ B-2, B-4 ] }
    逆引きする際は、EMP_DEPT_IDXでIDを解
    決してからEMPを引くことになる…
    転置インデックス
    NoSQLで複雑なクエリ要件を取り込もうとしてシステムが複
    雑化する…使い方を考え直したほうがいい

    View Slide

  11. © Chatwork
    データ構造はそもそも非対称
    11

    View Slide

  12. © Chatwork
    「注文コマンド要求」と「注文クエリ結果」の違い
    ● コマンド要求はシステムに送られるメッセージ
    ○ システム向けのデータは正規化される
    ● クエリ結果はシステムから返されるメッセージ
    ○ 人間向けのデータは非正規化される
    12
    注文ID
    注文日時
    商品ID
    注文数
    購入者ID
    注文ID
    注文日時
    商品ID
    商品名
    注文数
    購入者ID
    購入者名
    商品
    アカウント
    注文コマンド要求
    注文情報のクエリ結果
    system

    View Slide

  13. © Chatwork
    スケーラビリティもそもそも非対称
    13

    View Slide

  14. © Chatwork
    仮にC/Qを同居させてスケールアウトさせた場合の問題
    ● ほとんどのユースケースではC:Q =
    2:8
    ● QのコンテナにCを同居させた場合
    は、Cが過剰にスケールさせてしまう
    ○ リクエストが来なければリソー
    ス消費しない工夫ができそう
    ● CのコンテナにQを同居させた場合
    は、Qのスケーラビリティが不足する
    ○ 物理的に足りないのは問題なの
    で結局多いほうに合わせること
    になる
    14
    C C
    Q Q Q Q Q Q Q Q Q Q
    C C
    Q Q Q Q Q Q Q Q Q Q
    C C C C C C C C
    Qに合わせるとCが過剰になる
    Cに合わせるとQが不足する

    View Slide

  15. © Chatwork
    実装上の問題
    15

    View Slide

  16. © Chatwork
    実装上の問題(1/2)
    16
    ● クエリ要件を満たすことでリポジトリが複雑になる。クエリするだけでドメインロ
    ジックを呼び出さない。他にもページングやソートも扱うケースがある…。
    ● レスポンス用DTOをリポジトリで組み立てるため、非効率なN+1クエリが発生する
    val employees = employeeRepository.findByDeptIdsWithEmpNamePatterns(deptIds, empNamePatterns)
    // このあとに、ドメインロジックはない。 DTOに詰め直してクライアントに返すだけ。
    // ドメインロジックを起動するためではなく、データを閲覧するためだけに使っていることがある
    val reservationDtos = reservationRepository.findByIds(ids) // SQL発行
    .map{ reservation =>
    val hotel = hotelRepository.findById(reservation.hotelId) // SQL発行
    val customer = cusotmerRepository.findById(reservation.customerId) // SQL発行
    new ReservationDto(reservation, hotel.name, customer.name) // アプリケーション空間で結合及びデータを捨てる
    }
    ドメインはドメインの、クエリはクエリの都合で最適化が求められる

    View Slide

  17. © Chatwork
    実装上の問題(2/2)
    17
    ● コマンドを意識しないデータ指向では、エンドポイント、アプリケーションサービス、ドメ
    インがCRUDの用語に汚染されてしまう、という仮説がある
    ○ 商品の注文がcreatePurchaseItem?
    ○ 注文のキャンセルがupdatePurchaseItem?
    ● ドメインの動詞を重視するコマンド指向では、orderItem, cancelOrderなどユビキタス言語
    にフォーカスできるようになる。コマンドの表現によって意図が明白なインターフェイスを
    作ることができる
    ○ ただこの考え方は、CRUDであっても注意深く設計すれば可能…。
    ○ 実装というより分析の段階でコマンドを使うことのメリットが強い
    CQRSは非機能の観点が注目されがちだが、本来の目的はコマンド指向のドメインモデリングにある

    View Slide

  18. © Chatwork
    CQRSのPros/Cons
    18
    ● 利点
    ○ コマンドとクエリが分離しているため、耐障害性が高くなる(耐障害性に寄与す
    る)。別々にデプロイできる
    ○ コマンドとクエリを必要に応じて個別に最適化できる(弾力性に寄与する)。別々に
    スケールさせることもできる
    ● 欠点
    ○ 非CQRSと比べて手間が掛かる。目的ごとにサブシステムを分けるので構成要素が
    多くなる

    View Slide

  19. © Chatwork
    Event Sourcingとは
    19

    View Slide

  20. © Chatwork
    Event Sourcingとは
    ● 唯一信頼できる情報源(Single Source Of Truth)は、状態(ステート)ではなく(ドメイン)イベントという考え方
    ● CRUDでは従来からの最新状態を常に上書きするが、イベントは事実を記録するだけ
    20
    Event Sourcing
    CRUD(State Sourcing)
    Account { ID=1,
    NAME=KATO }
    Account { ID=1,
    NAME=SATO }
    AccountCreated{ ID=1, NAME=KATO }
    AccountRenamed{ ID=1, NAME=SATO }
    最新のエンティティを上書きする そのときのイベントを追記する

    View Slide

  21. © Chatwork
    ドメインイベントは
    ● イベントは過去に起きた出来事を意味する
    ● ドメインイベントは、ドメイン上のイベント
    を意味する
    ● 一般的には過去形の動詞として表現される
    ○ CargoShipped
    ○ CustomerRelocated
    ● イベントからコマンドが想起可能
    ○ ShipCargo
    ○ RelocateCustomer
    ● イベントとコマンドは似ているが別概念
    ○ コマンドは拒否されることがある
    ○ イベントはすでに起こったことを示す
    21
    ショッピングカートのイベント

    View Slide

  22. © Chatwork
    通知にイベントを使う
    ● CQRSはコマンド側からクエリ側に変更を伝える必
    要がある
    ○ コマンド側のドメインイベントをクエリ側に
    伝える
    ● 上記の現実的な実現手段として以下がある
    ○ ポーリング
    ○ Pub/Sub
    ● (賢くない)ポーリングは極力避ける
    ○ 賢くないポーリングだと、ただ負荷をかける
    だけになる
    ○ 見た目はPub/Subであっても内部実装はポー
    リングだったりすることがある。無駄なI/O
    がなければよい
    ● 通知は上流から下流にイベントを流すことが都合が
    よい
    22
    C
    Q
    C
    Q
    変更がないと
    きもポーリング
    で負荷をかけ
    てしまう
    変更があるときだけ
    イベントを通知する
    ポーリングはスケールし
    ない
    EventをPub/Subする

    View Slide

  23. © Chatwork
    Event SourcingのPros/Cons
    ● 利点
    ○ イベントは更新されず追記のみなので、スケーラビリティが確保しやすい
    ○ 特定の時点のリードモデルをイベントから導出することができる
    ■ ドメインイベントがあれば、リードモデルの設計をいつでもやり直せる
    ○ データマイグレーションコストではゼロではないが
    ■ 監査ログや行動履歴の分析に利用することができる
    ● 欠点
    ○ 大量のイベントから状態をリプレイする際に時間がかかる
    ■ 最新状態を保存したスナップショットを使うとリプレイ時間を短縮でき

    ○ 原則的にすべてのイベントをストレージに保存する必要がある
    ■ スナップショット保存時に、古いイベントを消すことも可能
    23

    View Slide

  24. © Chatwork
    アーキテクチャ・実装編
    24

    View Slide

  25. © Chatwork
    CQRS/ESを前提にしたアプリケーションアーキテクチャ
    25
    ドメイン
    イベント
    集約
    リード
    モデル
    コマンド
    プロセッサ
    クエリ
    プロセッサ
    コマンド
    リクエスト
    コマンド
    レスポンス
    クエリ
    リクエスト
    クエリ
    レスポンス
    リードモデル
    アップデータ
    クライアント
    ドメインオブ
    ジェクト群
    ドメインの語彙で命
    令する
    クライアントの画面
    や帳票に合わせた
    クエリ結果を返す
    PKey=集約ID, SKey=シーケンス番号,
    本体=ドメインイベント
    ドメインイベントを基にリー
    ドモデルを作る
    リードモデル構築時間はレスポンスタイム
    に反映されない

    View Slide

  26. © Chatwork
    コマンドプロセッサの実装イメージ(1)
    ● CRUDとはまったく様相が異な
    るコードになります
    ○ DBには追記しかしない
    ● 大まかな流れ
    ○ イベントからドメインオブ
    ジェクトを再生
    ○ ドメインロジックを実行
    ○ イベントを永続化
    26
    ・データ競合を防ぐためのロックができないのでは

    ・イベントが長大な場合、集約の再生に時間かかるのでは

    View Slide

  27. © Chatwork
    スナップショット機構を追加する
    27
    イベント
    集約
    リード
    モデル
    コマンド
    プロセッサ
    クエリ
    プロセッサ
    コマンド
    リクエスト
    コマンド
    レスポンス
    クエリ
    リクエスト
    クエリ
    レスポンス
    リードモデル
    アップデータ
    クライアント
    ドメインオブ
    ジェクト群
    ドメインの語彙で命
    令する
    クライアントの画面
    や帳票に合わせた
    クエリ結果を返す
    スナップ
    ショット

    View Slide

  28. © Chatwork
    コマンドプロセッサの実装イメージ(2)
    ● スナップショットには集約の
    最新状態が含まれる。スナッ
    プショット以降に発生したイ
    ベントの部分集合だけを取得
    することで、再生時間を
    ショートカットできる
    ● スナップショットの楽観的
    ロックを使うことでデータ競
    合を回避できる。
    ○ スナップショットとイベ
    ントは同一トランザク
    ションが望ましい
    28
    リクエスト毎に、リプレイやスナップショット保存のオーバヘッドがか
    かる…

    View Slide

  29. © Chatwork
    具体的に実装するには…
    29

    View Slide

  30. © Chatwork
    ● akka/akka
    ○ JVM, .NET版
    ○ 2011年から
    ● AsynkronIT/protoactor
    ○ Go, C#, JVM対応
    ○ 2016年から。まだリリースはされていない?
    ● commanded/commanded
    ○ Elixir
    ○ 2016年
    ● VLINGO XOOM
    ○ JVM, .NET対応
    ○ 2020年から
    ○ 実践ドメイン駆動設計の著者ヴァーノンさんの会社で開発している
    CQRS/ESのためのツール
    30

    View Slide

  31. © Chatwork
    FYI: AWSに対応したakka-persistenceプラグイン
    ● ジャナールをDynamoDBに、スナップショットをS3に配置するためのプラグインがある
    ● 他のストレージに対応したプラグインも数多くある
    31

    View Slide

  32. © Chatwork
    特別な仕組みなし&DynamoDBでやるなら…
    ● CartSnapshot, CartEventsを同じトランザクションで書き込む。CartEventsのNewImageをStreamからコ
    ンシュームし後段につなげる。もしくは、CartSnapshotだけにして、OldImage, NewImageの差分計算にy
    よってCartEventを生成する方式。後段につなげるパターンは同じ。
    ● 後段はKCLで使う。リードモデルは必要に応じてNoSQL, RDBMSを選択する
    ● 詳しくは DynamoDBを使ったCQRS/Event Sourcingシステムの構築方法(言語・F/W非依存) を参照
    32

    View Slide

  33. © Chatwork
    Akkaでは一行で済みます!
    ● Akkaでは集約をクラスタリングされたノード上に起動するアクターとして実装します
    ● ひとつひとつに集約のIDが振られていて一意に識別されます。どのノードからでも集約ア
    クターへコマンドを送信し返事をもらうことができます
    ● コマンドが受理されるとイベントが永続化されます。永続化されたイベントはアクターの
    再生に利用されます。スナップショットの保存タイミングも選択することができます
    ● 最初のリクエストでアクターが起動しワークロードがなくなるとアクターは停止します
    33

    View Slide

  34. FYI:Chatwork新アーキテクチャの概要
    state
    Shard
    Shard
    ShardR
    egion
    RoomAggregateActor
    Journal DB
    (DynamoDB)
    SnapshotStore
    (S3)
    Message
    Bus
    RMU Read DB
    Read
    API
    Read
    API
    Read
    API
    Controller
    UseCase
    Write API
    Server
    Write API
    Server
    Write API
    Server
    コマンドサイド
    akka-clsuter
    他のMSへ
    logic
    Client
    ID = 1
    クエリサイド
    サーバーサイド・チーム
    クライアントサイド・チーム
    ● 非同期・ノンブロッキング
    ● スーパービジョン
    ● 位置透過性
    ● ステートフル
    ● DBとの完全な同期によって読み込み不要
    ● ワークロードのパーティショニング
    ● 正規化されたデータ構造を扱う
    ● ネットワーク分断時は一貫性を重視
    コマンドサイドではドメインロジックを実行してドメイン状
    態を変える機能のみを提供する
    クエリサイドはドメインイベント
    をもとにクライアントにとって
    都合のよいリードモデルを構
    築する
    ● 非同期・ノンブロッキング
    ● ステートレス
    ● 非正規型データを扱う
    ● ネットワーク分断時は可能性を重視
    ● ラムダアーキテクチャでも十分可能
    MessagePosted
    MessageDTO
    PostMessage
    MessageDTO
    ID = 2
    ID = 3

    View Slide

  35. © Chatwork
    まとめ
    ● 可用性やスケーラビリティが問われるシステムでは、CQRS/Event
    Sourcingはよい選択肢になる。実現難易度はそこそこあるが一昔前
    より環境が整ってきているので、マイクロサービスアーキテクチャ
    を考えるうえで検討の余地があると思います
    ● FYI: DDD, CQRS/Event Sourcingに興味があるけどどこから手を出
    せばいいかわからない人、質問箱に投稿してくれれば答えます
    35

    View Slide

  36. © Chatwork
    本日はありがとうございました
    36

    View Slide