cqrs+esをakka persistenceを使って実装してみる。

Post on 16-Apr-2017

5.034 Views

Category:

Software

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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 読書会

興味のある方はお声がけください。

ありがとうございました

top related