CQRS+ESをAkka Persistenceを使って実装してみる。

80
-1

Published on

CQRS+ESをAkka Persistenceを使って実装してみる。

Published in: Software
0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
80
On SlideShare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
0
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide
  • EC のシステムをAkka Persistenceを使って開発していた。
    今日は、そのときの、経験を元に CQRS+ESのシステムの作り方やコツやハマりどころを紹介したいと思います。
  • AkkaやDDDはに興味をもつ人たちが確実に増えてきている。
  • ヴァンバーノン氏の2つの書籍
  • Lightbendから最近公開されたLagom、
    マイクロサービスを構築するためのフレームワーク  CQRS+ESがベースになっている

    Lagomがなにをやっているか分かる人は、今日の話は退屈かもしれないのです。

    実際に、ECのシステムを開発している時に感じたのは、
    CQRS+ESを使えば、ドメインモデリングに集中することができ、
    他の部分は作業として片付けることができるし、フレームワーク化しちゃえばもっと楽だな
    って思っていたら実際に作られてましたw

    ドメインモデリングに集中できる時代が近づいてきたという感じです。
    Lightbendに確実にロックインされるのは悔しいですが、選択肢としてはありだと思います。




  • Lagomなどの、マイクロサービス化の流れや、
    リアクティブという考え方の広まりも受けて
    AkkaとDDDを組み合わせて使う気運が高まってきたと感じます。
  • (言語やテクノロジに依存しないパターンやアプローチである。)
  • ドメイン層に登場するモデル(ドメインモデル、単にモデル、WriteModelともいったり)
    データアクセス層に登場するモデル (DTOといったり、ReadModelといったり)
  • ユーザーは他のユーザーをフォローする。 ユーザーはブロックしているユーザーにフォローされてはならない。

    UIに引きずられがちなドメインモデリングの例
    フォローする、ブロックするという課題を解決することにフォーカスしていない。
  • まず、フォローするという振る舞いに注目すると、フォロワーのリストは不要。
    「ブロックリスト」から「ブロックされているユーザーのリスト」へ変更。
    そして、フォローするという振る舞いの中で、
    自分がだれによってブロックされているかをチェックして、ブロックされていなければフォロイーのリストへ追加。

    ここで脱線して、フォローするという振る舞いの中で、フォローするユーザーの「ブロックしている」リストを問い合わせてチェックもできるが、シンプルではなくなる・動くモデルではあるが、「解決する」モデルではなくなる。

    んー、でも、フォロワーのリストも、フォロイーのリストも、ブッロクされてる・しているリストも欲しいんですけど。
  • ドメインモデルをコマンドサイドとクエリサイドに分ける。

    コマンドサイドにはフォロワーのリストが登場しない。(フォローするという振る舞いに影響しない)
    代わりにクエリサイドに登場している。

    クエリサイドはUIやプレゼンテーション層の要求に合わせて、好きなようにクエリを発行して、ReadModelを構築する。

    これは単純な例なのでこのような間違いはしないかもしれないですが、UIに引きずられて、そのままモデリングして、
    自分のフォロイーのリストに相手を追加して、なおかつ、相手のフォロワーのリストに自分を追加するような無駄に複雑なモデリングをしていませんか?
    この例だと、フォロワーのリストはフォローするという振る舞いに影響しないはずです。

    これは、.NETのエンタプライズアプリケーションアーキテクチャからの引用なのですが、

    CQRSを使わなかった場合のドメインモデルの複雑さは N×N になって
    CQRSを使った場合は コマンドサイドとクエリサイドで分かれて N + N になって、
    複雑さの爆発をかなり抑制することができると思います。
  • 複雑なドメインを、そのまま複雑なドメインモデルに落として満足しがち。
    我々は、複雑さと戦っている。そのままだと意味が無い。
    複雑なドメインをシンプルなモデルに
    シンプルにする手段として、まずはコンテキスト分割、そのあとCQRSやESを検討

    振る舞いやドメインを捉えきれていないのにCQRSから始めると失敗するかもしれません。
    (ユーザーに購買履歴がぶら下がっているモデルが、ログイン認証につかうパスワードをもってたりしませんか?)

  • (カートに発生するイベントの説明)

    例えば、カートにクーポンコードが紐づくように業務が拡張されたら、
    「クーポンコードが追加された」イベントを作るだけで、簡単に拡張できます。
    DBのテーブルスキーマの変更も不要で、ORMもいらなくなります。
  • - コマンドとイベントの流れの説明 Projeciton
    - データベース
    - データベースの選択
  • Cluster Shardingなどで  分散させて、ドメインイベントを大量に書き込むため、書き込みをスケールできるものがよい。 また、一つや2つDBのノードが落ちても書き込めるように可用性が高いものがよい。
    CAP定理でいうところのAが強いAPのあたり。A(Avialavility)P(Patrition Toralance) のあたり、

    クエリサイドは 要件に合わせてなんでも良い。
    クエリに強いものが良い
    組み合わせも自由

    要件を満たせるならコマンドサイドと同じDBでも良い。
    例えばCassandra は CQLというクエリ言語も一応使えるのでそれを使っても良い。実際、Lagomは コマンドサイドもクエリサイドもCassandraになっている。
  • コマンドサイドのDBが正のデータを保持し、クエリサイドはそれのViewと捉えることができます。
    このようなパターンをMaterialized View Patternといい、 CQRSはこのパターンの応用。

    クエリサイドのDBはぶっ飛んでしまってもOK, コマンドサイドのDBが残っていればゼロから作れる。

    また、読み込みをスケールさせたい場合のMongoDBのようなリードレプリカの構築と同じことができて、
    ただし、通常のリードレプリカと違って、種となるDBとリードレプリカ用のDBは異なるものが選択できる。 例えば、MongoならマスタもレプリカもMongoですが、CQRS+ESを使えば、マスタはCassandraでリードレプリカはMySQL+ Neo4j みたいなこともできる。
    さらには、DBが提供するレプリケーションの機能も不要になります。

    また、CQRSを使えばコマンドサイドとクエリサイドを独立してスケールさせることができます。
  • Pollingの仕組みを使って、新しく追加したサービスにドメインイベントを流し込む

    サービスバスも不要になるという利点があります。
  • コマンドを実行した直後に、
    クエリサイドからクエリを発行して、コマンドで更新した結果を取得しようとしても、
    上手く取得できないと思います。PollingやクエリサイドのDB反映もコマンドと同期されて実行されるわけではないので、
    コマンドで成功ACKが返ってきても、クエリサイドのDBには反映が終わっていない可能性が高いです。
    時間がたてば、解消されるのですが、このような状況を結果整合性といいます。

    UIで反映されているように上手くごまかすのが無難な解決方法です。
    コマンドサイドを無理やりクエリのように使うこともできますが、パフォーマンスの問題や、CQRSが意味を成さなくなる問題があります。

    結果整合性でも問題ないと判断できる場合もあります。
    例えば、ある会員登録のプロセスで、一意な会員IDとは別に、メールアドレスを入力しないといけないが、そのメールアドレスは他の人と被ってっはいけないという状況で、
    Queryサイドに反映済みであれば良いのですが、例えば二人の会員が別々の場所で、同じメールアドレスを入力するとタイミングによっては同時にバリデーションが成功してしまうと思います。
    一つの解決方法は、会員登録のプロセスをクラスタをまたいだシングルトンに管理させて(すなわち、クラスタシングルトンのProcess Manager)、そいつは会員登録プロセスを1つずつ実行し、直近に登録されたメールアドレスリストを保持している。そのリストに含まれていたらバリデーションエラーで弾く方法が考えらますが、単一障害点になったり、なによりも無駄に煩雑です。

    よくよく考えてみると、クエリサイドに反映されるまでの数ミリ秒の間に同時に同じメールアドレスを思いついて入力する人が世界中にどれだけいるか?
    を考えてみると、たいして問題にならないケースがあります。問題になるケースもあるので、ビジネスのインパクトに合わせてトレードオフになると思います。

    もちろん、コマンドよりも手前でQueryサイドのEmailListを元にバリデーションしているので、過去に登録済みのメールアドレスとの重複チェックはできます。問題になるのはクエリサイドに反映される数ミリ秒の間です。

  • イベント列から状態の復元が基本なので、RDBに起因するインピーダンスミスマッチがない。
    ORMで悩まない。
  • Materialised View PatternによってViewが柔軟に作れる。


  • REST APIはAkka HTTP, レガシーシステムとの統合はAkka AMQP
  • 流れの説明

    内部状態に基づく条件判定は①この段階でやる。
    例:countが100以上だったら、別のイベントを発行する。
  • senderにイベントを返す、
    ACK[Unit]を返すか ACK[DomainEvent] を返すか。

  • 「バグで間違った処理をしてしまったので取り消しました」イベント

    ドメインイベントの設計は、ドメイン自体を理解しないとできないから難しいのは当たり前。
  • Stamina イベントのバージョン管理を手助けしてくれる。
    ScalaMatsuriの応募セッションでもStaminaのセッションがあったが、落選してた。
  • (スライドの説明)

    EventsByTagQueryを使うためには、Journalに書き込む前にタグを付与する必要があります。
    それが、次のスライドの。。。


  • これは個人的に作っている、Redditのクローンのサンプルコードなのですが、
    WriterEventAdapterを使って、”Thread” というタグをドメインイベントに付与して、
    Taggedイベントを作成し、それをJournalに保存します。

    CQRSを目的とするのであれば、通常はAggregateRoot名(ここでいうThread)をタグとして付与します。
    Cassandra Journal プラグインではタグ数は最大3個が上限になっているので、タグのつけすぎにご注意ください。

    どのイベントにどのAdapterを使うかはapplication.confで設定できます。
  • イベントにタグがついたのでそれをReadJournalで取得して、Viewを構築します。
  • まずはReadJournalを取得、ここではLeveldbReadJournalを取得、AkkaのInMemoryプラグインはReadJournalにはまだ対応していないので、
    ProjectionのテストではLevelDBしか使えない。

    ReadJournalはAkka Streamがベースになっているので、マテリアライザをセットする。

    ・記録してどのイベントまで投影できたかをoffset を使って記録する。

    projection.updateでeventをDTOに変換して保存


  • CQRS+ESのコマンドサイドとクエリサイドを Akka Persistenceと Akka Persistence Queryで実装した.
    experimental
  • RMP:
    サブシステムやマイクロサービスをどのようなメッセージングパターンで統合していくかという説明に重きが置かれている。
    ClusterShardingやAkka Persistence、ProcessManagerの話題も。
  • CQRS+ESをAkka Persistenceを使って実装してみる。

    1. 1. CQRS+EventSourcingをAkka Persistence を使って実装してみる。〜コツとハマりポイン ト〜 2016/03/16 Reactive Messaging Patternsプレ読書会 - CQRS、ESの基本を学ぶ - Satoshi Matsushita
    2. 2. 自己紹介 • Satoshi Matsushita @satoshi_m8a • Scala, Akka, DDD, フロントエンド, コンピュータビジョン, 機械学 習 • ゲヒルン株式会社 Python, Go, Erlang, Scala, OCaml, TypeScript 「Gehirn Infrastructure Services」 セキュリティ診断 • ECのシステムをAkka Persistenceを使って開発していた。
    3. 3. Akka + DDD 気運の高まり(1) • Scala Matsuri 2016 二日目アンカンファレンス DDD+CQRS+EventSourcing実装する会 (Akkaパフォーマンスチューニングについて話してみよう会) by かとじゅんさん(@j5ik2o)
    4. 4. Akka + DDD 気運の高まり(2) • Vaughn Vernon 氏の書籍
    5. 5. Akka + DDD 気運の高まり(3) • Lightbendの一貫したツールキット • DDDを意識したもの Akka Persistence Akka Persistence Query
    6. 6. Akka + DDD 気運の高まり(4) • Lagom マイクロサービスを構築するためのフレームワーク CQRS+ESがベースになっている
    7. 7. Akka + DDD 気運の高まり(5) • マイクロサービス化の流れ • リアクティブという考え方の広まり
    8. 8. 目次 • CQRS • イベントソーシング • コマンドサイド • クエリサイド • 参考
    9. 9. CQRS Command Query Responsibility Segregation コマンド・クエリ責務分離
    10. 10. よくある階層化パターン プレゼンテーション層 アプリケーション層 ドメイン層 インフラストラクチャ層
    11. 11. CQRS概念図 プレゼンテーション層 アプリケーション層 ドメイン層 インフラストラクチャ層 データアクセス層 コマンドサイド クエリサイド
    12. 12. ドメインモデル • 例:Twitterのフォロワー / フォロイー ユーザー フォロワーのリスト フォロイーのリスト ブロックリスト
    13. 13. ドメインモデル • フォローするという振る舞いに着目すると ユーザー フォローする(userId) ブロックされる(userId) フォロイーのリスト ブロックされてい るユーザーのリス ト
    14. 14. CQRS ユーザー フォロイーのリスト ブロックされている ユーザーのリスト コマンドサイド クエリサイド フォロワーのリスト フォロイーのリスト ブロックされている ユーザーのリスト ブロックしている ユーザーのリスト … ユーザーのリスト
    15. 15. 複雑さに立ち向かう • 複雑なドメインを、そのまま複雑なドメインモデル に落として満足しがち • まずは、コンテキスト分割を検討 • ドメインをよく観察し、振る舞いにフォーカスする • CQRSやESの検討はそのあと
    16. 16. Event Sourcing
    17. 17. Event Sourcing カートID : “cart1” 商品: “A”->0, “B”->1 カート作成 商品Aを追加 商品Bを追加 商品Aを削除 カートID : “cart1” 商品: “A”->1, “B”->0
    18. 18. Snapshot 1 2 Snapshot 101 100 • 全てのイベントを初めから復元してい ては時間がかかる • スナップショットをとって途中から復 元
    19. 19. CQRS+ES コマンドサイド クエリサイド Journal Aggregate Root Command Service Projection DAO Query Service DB DB Command Domain Event Domain Event DTO DTO Polling
    20. 20. データベース選択のポイント • コマンドサイド ・Cassandra, DynamoDB, Riak ・書き込みをスケールできるもの、可用性の高いものが良 い • クエリサイド ・各種RDB, NoSQL(ドキュメント指向・グラフ指向) ・クエリに強いものが良い ・組み合わせOK
    21. 21. Materialized View Pattern • コマンドサイドのDBが正のデータを保持する、 クエリサイドはそれのView • リードレプリカの構築(読み込みをスケール) https://msdn.microsoft.com/ja-jp/library/dn589782.aspx から引用
    22. 22. サービス統合も容易 • 新しいサービスを追加したら、ドメインイベントを流 し込む。 • あたかも、その新しいサービスが最初から統合されて るかのように振る舞う。 • 現在のイベントまで追いついたら、システムに馴染ん でいる。
    23. 23. 結果整合性 コマンドサイド クエリサイド Journal Aggregate Root Command Service Projection DAO Query Service DB DB Command Domain Event Domain Event DTO DTO Polling
    24. 24. Over Kill • 例:ID、名前、パスワード、E-Mailアドレスを持つ、 会員AR • パスワードやE-Mailアドレスの変更履歴を追うことで 、ビジネスの価値を生むのか? • CQRSだけ、もしくは単純なCRUDができるだけでよ いのでは?
    25. 25. 余談:純粋なREST APIはDDDに向かない • REST APIで一旦少なくなった情報を復元するのは困難 • 純粋なRESTにこだわらない。 CQRSで作った折角のリッチなコマンドモデルが意味をなさなくなる 。 業務で発生する操作 情報量:大 REST API 情報量:小 リッチなコマンドモデル 情報量:大 > < ×
    26. 26. ESのメリット・デメリット • メリット インピーダンスミスマッチがない。 履歴管理が不要、データ解析やデバッグにも使える。 イベントは追記のみなのでパフォーマンスが良い。 機能追加も容易。 • デメリット イベントの修正が煩雑(後述) データサイズの問題
    27. 27. CQRS+ESのメリット・デメリット • メリット ドメインの振る舞いが明確になる Viewを柔軟につくれる スケールも柔軟に • デメリット 結果整合性
    28. 28. Akkaで作る CQRS+ES コマンドサイド クエリサイド Journal Aggregate Root Command Service Projection DAO Query Service DB DB Command Domain Event Domain Event DTO DTO Polling Akka Persistence Akka Persistence Plugin Akka Persistence Query Slick3 Akka Cluster Sharding
    29. 29. コマンドサイド
    30. 30. Akka Persistence • Actorの内部状態を永続化することができる • AkkaのCQRSとイベントソーシングに使われる • メッセージの再送の仕組みも提供(At least once delivery)
    31. 31. 例:カウントするActor • CounUpコマンドを受け取り、内部のカウントを増 加させていく。
    32. 32. PersistentActor Persistent Actor Journal persistenceId = “c100” count = 0 CountUp CountIncreased Ack(永続化完了) • コマンドを受け付け、ドメインイベントを発行する。 ① ② ③
    33. 33. PersistentActor Persistent Actor Journal persistenceId = “c100” count = 1 Ack ④ Ack ⑤ • JournalからのAckを待ち、内部状態を更新する
    34. 34. ポイント • 内部状態(count)の更新はドメインイベントの永続化 完了を待ってから行う • 永続化されていないイベントは起こっていないイベ ントと同義
    35. 35. PersistentActorの復元 • クラッシュ、タイムアウト時の停止、シャードの移 動など様々な理由でActorは再起動する。 • 再起動したActorを元の状態に戻し、コマンドを受 け付けたい。
    36. 36. PersistentActorの復元 Persistent Actor Journal persistenceId = “c100” count = 3 CountIncreased ① ② CountIncreased CountIncreased Select Events where persistenceId = “c100” ③
    37. 37. Akka Persistence class CountUpActor extends PersistentActor { override def persistenceId: String = self.path.name context.setReceiveTimeout(120.seconds) var count: Int = 0 def updateState(event: Increased) = { this.count = this.count + event.amount } override def receiveRecover: Receive = { case e: Increased => updateState(e) } override def receiveCommand: Receive = { case c: CountUp => persist(Increased(c.amount)) { event => updateState(event) sender() ! event } case ReceiveTimeout => context.parent ! Passivate(stopMessage = Stop) case Stop => context.stop(self) } } <- ここで永続化 <- 永続化が終わった後に状態を更新 <- 復元したイベントを元に状態を更新
    38. 38. ポイント • Recoveryが完了するまで、コマンドを処理しないよ うになっている。 • Recovery時は内部状態の更新だけを行う、 外部へコマンドやメッセージを発行してはならない 。
    39. 39. Aggregate Root • 実際はPersistentActorを継承して、AggregateRoot アクターを作ると良い。(c.f. akka-ddd) https://github.com/pawelkaczor/akka-ddd/blob/master/akka-ddd- core/src/main/scala/pl/newicom/dddd/aggregate/AggregateRoot.scala • スナップショット操作, GracefulPassivation, リカバリを隠蔽
    40. 40. ドメインイベントの設計 • ドメインイベントは起こった事実を表す。 イベント名は過去形 (Increased, Decresed, Created) • 「住所を変更しました」 vs 「引っ越しました」 • 「旧システムからデータを移行しました」イベント • きっかけとなったコマンドをイベントのメタデータとして保持することも • 粒度は細かすぎても良くない。 e.g.「郵便番号を変更しました」
    41. 41. ドメインイベントのシリアライズ • ドメインイベントはシリアライズされて、 コマンドサイドのDBに保存される。 • デフォルトではJavaのシリアライザが使われる • Javaのシリアライザは速度面でも、拡張面でも問題がある • 実運用するのであれば、 Google Protocol Buffers が無難
    42. 42. ドメインイベントのスキーマ変更 • フィールドを追加したり、一つのイベントを分割など • EventAdapterを使ったり、一応の解決方法はあるが煩雑 • Stamina https://github.com/scalapenos/stamina
    43. 43. Persistence Plugin • Cassandra, JDBC, DynamoDB, Riak 向けのPlugin • テスト用のInMemory Pluginや LevelDB Plugin • ReadJournal API(後述)の実装しやすいDBがおすすめ • Cassandra PluginはAkka公式
    44. 44. クエリサイド
    45. 45. Akka Persistence Query • CQRSのクエリサイドの実装に使われる • クエリサイド全体ではなく、 Journalからクエリ側のDBへの投影に使われる • experimental (Akka 2.4.2) Pluginも出揃っていない
    46. 46. Journal Projection DAO DB Domain Event DTO Polling クエリサイド DTO • Read Journal APIを実装したPersistence Plugin を使う • JournalをPollingして、ドメインイベントを待ち受ける
    47. 47. ReadJournal API • EventsByTagQuery タグを元にイベントを取得 • EventsByPersistenceIdQuery PersistenceIdを元にイベントを取得 • AllPersistenceIdsQuery すべてのPersistenceIdを取得 • CurrentPersistenceIdsQuery 現在存在する全てのPersistenceIdを取得(ポーリングなし) • すべてのJournal Pluginがこれら実装しているわけではない 実装が困難なものもあるので、Journal用のDB選びは慎重に
    48. 48. イベントにタグを付与する class ThreadEventAdapter extends WriteEventAdapter { override def manifest(event: Any): String = "" val tags = Set("Thread") override def toJournal(event: Any): Any = event match { case e: ThreadEvent => Tagged(event, tags) case _ => event } }
    49. 49. Projection • Read Model Projection / Read Model Updaterともいう • ドメインイベントを元に、Viewを構築する
    50. 50. Projection val readJournal = PersistenceQuery(system) .readJournalFor[LeveldbReadJournal](LeveldbReadJournal.Identifier) implicit val mat = ActorMaterializer()(system) val dao = new ThreadsDao(dbConfig) val projection = new ThreadProjection(dao) readJournal .eventsByTag("Thread", projection.lastOffset) .mapAsync(1) { envelope => projection.update(envelope.event).map(_ => envelope.offset) } .mapAsync(1) { offset => projection.saveProgress(offset) } .runWith(Sink.ignore)
    51. 51. クエリ • Slick3などを使ってクエリする。
    52. 52. その他 • Process Manager 複数のAggregate Rootにまたがった処理を順序 良く実行する PersistentFSMを使う。 • Cluster Sharding Aggregate Rootを分散させる。 Cluster Singleton
    53. 53. まとめ • CQRS+ESのコマンドサイドとクエリサイドを Akka Persistenceと Akka Persistence Queryで実装 した • Lagom
    54. 54. 参考 • CQRS Journey https://msdn.microsoft.com/ja-jp/library/jj554200.aspx • .NETのエンタープライズアプリケーションアーキテクチャ • 実践ドメイン駆動設計
    55. 55. Reactive Messaging Patterns with the Actor Model 読書会 興味のある方はお声がけください。
    56. 56. ありがとうございました

    ×