Click here to load reader
Upload
matsushita-satoshi
View
5.034
Download
0
Embed Size (px)
Citation preview
CQRS+EventSourcing を Akka Persistence を使って実装してみる。〜コツとハマりポイント〜
2016/03/16 Reactive Messaging Patterns プレ読書会 - CQRS 、 ES の基本を学ぶ -
Satoshi Matsushita
自己紹介
• Satoshi Matsushita @satoshi_m8a• Scala, Akka, DDD, フロントエンド , コンピュータビジョン , 機械学習• ゲヒルン株式会社
Python, Go, Erlang, Scala, OCaml, TypeScript 「 Gehirn Infrastructure Services 」 セキュリティ診断
• EC のシステムを Akka Persistence を使って開発していた。
Akka + DDD 気運の高まり (1)
• Scala Matsuri 2016 二日目アンカンファレンスDDD+CQRS+EventSourcing 実装する会(Akka パフォーマンスチューニングについて話してみよう会 ) by かとじゅんさん (@j5ik2o)
Akka + DDD 気運の高まり (2)
• Vaughn Vernon 氏の書籍
Akka + DDD 気運の高まり (3)
• Lightbend の一貫したツールキット• DDD を意識したもの
Akka PersistenceAkka Persistence Query
Akka + DDD 気運の高まり (4)
• Lagom マイクロサービスを構築するためのフレームワーク CQRS+ES がベースになっている
Akka + DDD 気運の高まり (5)
• マイクロサービス化の流れ• リアクティブという考え方の広まり
目次
• CQRS• イベントソーシング• コマンドサイド• クエリサイド• 参考
CQRSCommand Query Responsibility Segregation
コマンド・クエリ責務分離
よくある階層化パターン
プレゼンテーション層
アプリケーション層
ドメイン層
インフラストラクチャ層
CQRS 概念図
プレゼンテーション層
アプリケーション層
ドメイン層
インフラストラクチャ層
データアクセス層
コマンドサイド クエリサイド
ドメインモデル• 例: Twitter のフォロワー / フォロイー
ユーザー
フォロワーのリスト
フォロイーのリストブロックリスト
ドメインモデル• フォローするという振る舞いに着目すると
ユーザー フォローする (userId)ブロックされる (userId)
フォロイーのリストブロックされているユーザーのリスト
CQRS
ユーザー
フォロイーのリスト
ブロックされているユーザーのリスト
コマンドサイド クエリサイド
フォロワーのリストフォロイーのリスト
ブロックされているユーザーのリスト
ブロックしているユーザーのリスト
…ユーザーのリスト
複雑さに立ち向かう
• 複雑なドメインを、そのまま複雑なドメインモデルに落として満足しがち• まずは、コンテキスト分割を検討• ドメインをよく観察し、振る舞いにフォーカスする• CQRS や ES の検討はそのあと
Event Sourcing
Event Sourcing
カート ID : “ cart1”商品: “ A”->0, “B”->1
カート作成
商品 A を追加
商品 B を追加
商品 A を削除
カート ID : “ cart1”商品: “ A”->1, “B”->0
Snapshot1
2
Snapshot
101
100• 全てのイベントを初めから復元していては時間がかかる• スナップショットをとって途中から復元
CQRS+ES
コマンドサイド クエリサイド
Journal
Aggregate Root
Command Service
Projection DAO
Query Service
DB DB
Command
Domain Event
Domain Event
DTO
DTO
Polling
データベース選択のポイント
• コマンドサイド ・ Cassandra, DynamoDB, Riak ・書き込みをスケールできるもの、可用性の高いものが良い• クエリサイド ・各種 RDB, NoSQL( ドキュメント指向・グラフ指向 ) ・クエリに強いものが良い ・組み合わせ OK
Materialized View Pattern
• コマンドサイドの DB が正のデータを保持する、クエリサイドはそれの View• リードレプリカの構築(読み込みをスケール)
https://msdn.microsoft.com/ja-jp/library/dn589782.aspx から引用
サービス統合も容易
• 新しいサービスを追加したら、ドメインイベントを流し込む。• あたかも、その新しいサービスが最初から統合されてるかのように振る舞う。• 現在のイベントまで追いついたら、システムに馴染んでいる。
結果整合性コマンドサイド クエリサイド
Journal
Aggregate Root
Command Service
Projection DAO
Query Service
DB DB
Command
Domain Event
Domain Event
DTO
DTO
Polling
Over Kill
• 例: ID 、名前、パスワード、 E-Mail アドレスを持つ、会員 AR• パスワードや E-Mail アドレスの変更履歴を追うことで、ビジネスの価値を生むのか?• CQRS だけ、もしくは単純な CRUD ができるだけでよいのでは?
余談:純粋な REST API は DDD に向かない
• REST API で一旦少なくなった情報を復元するのは困難• 純粋な REST にこだわらない。
CQRS で作った折角のリッチなコマンドモデルが意味をなさなくなる。
業務で発生する操作情報量:大 REST API情報量:小 リッチなコマンドモデル情報量:大> <×
ES のメリット・デメリット
• メリットインピーダンスミスマッチがない。履歴管理が不要、データ解析やデバッグにも使える。イベントは追記のみなのでパフォーマンスが良い。機能追加も容易。• デメリットイベントの修正が煩雑 ( 後述 )データサイズの問題
CQRS+ES のメリット・デメリット
• メリットドメインの振る舞いが明確になるView を柔軟につくれるスケールも柔軟に
• デメリット結果整合性
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
コマンドサイド
Akka Persistence
• Actor の内部状態を永続化することができる• Akka の CQRS とイベントソーシングに使われる• メッセージの再送の仕組みも提供( At least once
delivery )
例:カウントする Actor
• CounUp コマンドを受け取り、内部のカウントを増加させていく。
PersistentActor
Persistent
Actor
Journal
persistenceId = “c100”count = 0
CountUp
CountIncreased Ack(永続化完了 )
• コマンドを受け付け、ドメインイベントを発行する。①
② ③
PersistentActor
Persistent
Actor
Journal
persistenceId = “c100”count = 1
Ack
④
Ack⑤
• Journal からの Ack を待ち、内部状態を更新する
ポイント
• 内部状態 (count) の更新はドメインイベントの永続化完了を待ってから行う• 永続化されていないイベントは起こっていないイベントと同義
PersistentActor の復元
• クラッシュ、タイムアウト時の停止、シャードの移動など様々な理由で Actor は再起動する。• 再起動した Actor を元の状態に戻し、コマンドを受け付けたい。
PersistentActor の復元
Persistent
Actor
Journal
persistenceId = “c100”count = 3
CountIncreased
① ②
CountIncreased
CountIncreased
Select Events where persistenceId = “c100”
③
Akka Persistenceclass 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) }}
<- ここで永続化<- 永続化が終わった後に状態を更新
<- 復元したイベントを元に状態を更新
ポイント
• Recovery が完了するまで、コマンドを処理しないようになっている。• Recovery 時は内部状態の更新だけを行う、外部へコマンドやメッセージを発行してはならない。
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, リカバリを隠蔽
ドメインイベントの設計
• ドメインイベントは起こった事実を表す。イベント名は過去形 (Increased, Decresed, Created)
• 「住所を変更しました」 vs 「引っ越しました」• 「旧システムからデータを移行しました」イベント• きっかけとなったコマンドをイベントのメタデータとして保持することも• 粒度は細かすぎても良くない。
e.g. 「郵便番号を変更しました」
ドメインイベントのシリアライズ
• ドメインイベントはシリアライズされて、コマンドサイドの DB に保存される。• デフォルトでは Java のシリアライザが使われる• Java のシリアライザは速度面でも、拡張面でも問題がある• 実運用するのであれば、 Google Protocol Buffers が無難
ドメインイベントのスキーマ変更
• フィールドを追加したり、一つのイベントを分割など• EventAdapter を使ったり、一応の解決方法はあるが煩雑• Stamina https://github.com/scalapenos/stamina
Persistence Plugin
• Cassandra, JDBC, DynamoDB, Riak 向けのPlugin
• テスト用の InMemory Plugin や LevelDB Plugin• ReadJournal API( 後述 ) の実装しやすい DB がおすすめ• Cassandra Plugin は Akka公式
クエリサイド
Akka Persistence Query
• CQRS のクエリサイドの実装に使われる• クエリサイド全体ではなく、
Journal からクエリ側の DBへの投影に使われる• experimental (Akka 2.4.2)
Plugin も出揃っていない
Journal Projection DAO
DB
Domain Event DTO
Polling
クエリサイドDTO
• Read Journal API を実装した Persistence Plugin を使う• Journal を Polling して、ドメインイベントを待ち受ける
ReadJournal API
• EventsByTagQuery タグを元にイベントを取得• EventsByPersistenceIdQuery PersistenceId を元にイベントを取得 • AllPersistenceIdsQuery すべての PersistenceId を取得• CurrentPersistenceIdsQuery 現在存在する全ての PersistenceId を取得(ポーリングなし)• すべての Journal Plugin がこれら実装しているわけではない 実装が困難なものもあるので、 Journal 用の DB選びは慎重に
イベントにタグを付与する
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 }}
Projection
• Read Model Projection / Read Model Updater ともいう• ドメインイベントを元に、 View を構築する
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)
クエリ
• Slick3 などを使ってクエリする。
その他
• Process Manager 複数の Aggregate Root にまたがった処理を順序良く実行する PersistentFSM を使う。
• Cluster Sharding Aggregate Root を分散させる。 Cluster Singleton
まとめ
• CQRS+ES のコマンドサイドとクエリサイドをAkka Persistence と Akka Persistence Query で実装した
• Lagom
参考• CQRS Journey
https://msdn.microsoft.com/ja-jp/library/jj554200.aspx• .NET のエンタープライズアプリケーションアーキテクチャ• 実践ドメイン駆動設計
Reactive Messaging Patterns with the Actor Model 読書会
興味のある方はお声がけください。
ありがとうございました