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

ChatworkDevDay_リアクティブシステムと次世代基盤について_加藤

 ChatworkDevDay_リアクティブシステムと次世代基盤について_加藤

Chatworkでは、リアクティブシステムとCQRS / Event Sourcingを反映した次世代基盤を構想しています。

まだ検討段階ではありますが、なぜそれらを採用するのかメリット・デメリットも含めてご説明します。

- リアクティブなソフトウェア・アーキテクチャを実現するためには?
- なぜCQRS(Command Query Responsibility Segregation)を用いるのか?
- なぜEvent Sourcingを用いるのか?
- 次世代基盤の実現に向けて

かとじゅん

May 26, 2021
Tweet

More Decks by かとじゅん

Other Decks in Programming

Transcript

  1. 自己紹介 • @j5ik2o • Chatwork のテックリード • 10 歳で初めてプログラミングに触れる。SIer としてさまざまな現場での業務を経験した後、

    2011 年より某d社、2013 年より大手ソーシャ ルゲーム企業で、それぞれ Scala やドメイン 駆動設計を採用したシステム開発に従事 • 2014 年 7 月より Chatwork に参画。現在は Chatwork 次期アーキテクチャのプランニン グや設計、開発に携わる • 最近はまっていること。オートミールうまいす よ
  2. アーキテクチャ刷新(Sagrada)の目的と手段 • 目的 ◦ 2025年の事業計画に合わせて、生産性を維持・向上できるプロダクトと 組織を構築する • 手段 ◦ プロダクトの健全性を維持・向上させること

    ◦ チームが独立して改善活動をおこなえること ◦ 大規模なアジャイル開発が実践できること ◦ いつでもレスポンスを返せる状態になっていること ▪ リアクティブシステムを反映したアーキテクチャを実現する ▪ CQRS/Event Sourcingシステムを実現する 社会インフラを担うからこそ必要になる手段 この課題に対する考え方を解説します
  3. 「止まらないシステム」ではなく「回復力があるシステム」が求められている • 東京証券取引所, 株式売買システムで全銘柄で売買不能になった (2020/1) ◦ NASのフェイルオーバーができなかったことが原因 ▪ NASのマニュアルでは待機系への切替の初期設定が「15秒で切替」と なっていたが、実際の設定は「OFF」だった。人為的な要素が強い…

    ▪ NASが故障することで売買システムの全機能を失ってしまった ◦ 恒久対策は回復力を向上させること。異常を起こした部分を切り離し障害か ら回復できるアーキテクチャに変更していくために、MSAに移行していく、 とのこと 止まらないシステム(全く障害を起こさない完全なシステム)を目指すのではなく 【障害から回復する能力を設計すること】に価値がある
  4. メッセージ駆動とは リアクティブシステムは【⽴同期・ノンブロッキング】なメッセージ・パッシングによってコン ポーネント間の境界を確⽴する リアクティブシステムは【⽴同期・ノンブロッキング】なメッセージ・パッシングによってコン ポーネント間の境界を確⽴する CartClient<Actor> Cart<Actor> AddCartItem { cartId

    = 1, cartItemId = 4, itemId = 1, itemNum = 1, } CartItem { 1, 1, … } CartItem { 1, 2, … } CartItem { 1, 3, … } タスクの完了を待 たない AddCartItemSucceeded AddCartItemFailed タスクの成否をメッセージで 返答する タスクがなければリ ソースを消費しない メッセージに反応する かどうか受信コンポー ネント次第 メッセージが届くなら ばリモートでもローカ ルでもよい タスクを依頼するた めにメッセージを送 信する
  5. 【FYI】「リアクティブシステム」と「リアクティブプログラミング」は同じ概念ではない • リアクティブシステムとリアクティブプログラミングという用 語に頻繁に遭遇するが。これらは等価ではない • リアクティブシステムはアーキテクチャレベルでリアクティブ 原則を適用する。リアクティブシステムを実現する手段として Future/Promise, Reactive Streams,

    アクターモデルなどのリ アクティブプログラミングが利用されます。だからといって、 自動的にリアクティブシステムになりません • 例えば、アプリケーションを1ノードだけにデプロイした場 合、そのノードが故障したら全システムを失う。これではリア クティブ宣言の耐障害性(回復力)がないので、リアクティブプ ログラミングを使っていても、リアクティブシステムではない Node 1 リアクティブプログラミン グを使っていても1台の みで運用すると、この 1台 が故障すると全システム を失う 故障
  6. リアクティブ原則(The Reactive Principles) • 応答性を維持する/Stay Responsive • 不確実性を受入る/Accept Uncertainty •

    失敗を受け入れる/Embrace Failure • 自律性を表明する/Assert Autonomy • 一貫性を調整する/Tailor Consistency • 時間を分離する/Decouple Time • 空間を分離する/Decouple Space • ダイナミクスを処理する/Handle Dynamics @see https://principles.reactive.foundation/principles/index.html 今回はこの2点を解説
  7. なぜアクターモデルなのか • Erlang 1998年 初版リリース(OSSとして公開された) 
 ◦ 1986年にエリクソンがアクターモデルをベースにしたErlangを開発(当初はOSSではなく企 業内で開発・利用されていた)。電話交換機にて稼働率99.9999999%を実現した。2011年 にWhatAppが1台のサーバで100万クライアントをさばいた実績がある

    
 • Akka/Scala 2010年に1.0、2012年に2.0と進化 
 ◦ Akkaはリアクティブ原則をサポートしたツールキット。AkkaはErlangからインスピレーショ ンを受け、Lightbend社のCTO Jonas Bonér氏によって開発。 非同期・ノンブロッキングな メッセージ駆動でC10K問題を解決。2011年の記事ではErlangの2倍のスループットを発揮 したという事例もある
 • マルチコア危機の解決をターゲットにした言語として、Erlang(1998)やScala(2004)が登場。この 2つの言語はマルチスレッドプログラミングおける、多くの苦しみを取り除いてくれると期待され ていた

  8. 自律性を表明する/Assert Autonomy • 独立して行動し、協調的に相互作用するコンポー ネントを設計する • 自律性とは、各マイクロサービスが境界を維持し 独立して運用できること ◦ 当該サービスの動作保証には、連携している

    他のサービスは関係がない。常に自分のサー ビスの行動を保証するのみ • 自律性を保つにはアプリケーションを分離する必 要があります。分離には主に以下の観点がある ◦ DDDの境界づけられたコンテキスト単位で分 離する ◦ CQRS/Event Sourcing観点でのコマンドと クエリに分離する 在庫 EC 在庫 予測 Command Query Commandに障害が起きてもQueryできるよう にするにはお互いに分離する必要がある ドメイン境界で分割 C/Qで分割
  9. CQRSとは • Command and Query Responsibility Segregation = コマンド・クエリ責務分 離

    のこと。分離というより隔離という解 釈が正しい • コマンド(書き込み)とクエリ(読み込み)を スタックごとにそれぞれに隔離すること を意味する。単にドメインモデルをコマ ンド用・クエリ用に分割することではな い • CQRSはDDDを前提としています(ドメイ ンから本質的ではないクエリ責務を排除 するための設計パターンです)。詳しくは CQRS Documents by Greg Young を参照 のこと 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
  10. RDBへの書き込みがスケールしない問題 • Writerをスケールアウトしたいが無理スジ ◦ Write Sharding: Writer, Read Repclica のセットを分割して、書き分ける

    ◦ Vitess(https://vitess.io/) • いずれにしてもアプリケーション(もしくはミド ルウェア)で書き込みを分割する。柔軟なクエリ ができなくなるという代償を払う必要がある ◦ 書き込むデータからヒントを得て、どの DBに書き込むかを決める(ヒントを間違え ると…) ◦ 分割されたデータどうしでは結合するク エリはできない ◦ 2台→4台→8台と手動でWriterを増やす さいデータの移動が必要になる そもそもRDBが向かない要件をRDBで解決しようと して複雑化する… 25 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
  11. NoSQLでRDBのクエリのような使い方をしてしまう問題 • NoSQLはハッシュキーでスケールアウトできる ◦ ハッシュキーで自動的にシャーディング(パー ティショニング)される ◦ ただしキーでしかエンティティを解決できない • エンティティの属性でもクエリしたい…

    ◦ GSIを多用する ▪ 個数の上限がある ◦ 上限があるなら、転置インデックスを作ろう ▪ エンティティの更新以外にインデックス データも更新する必要がある ▪ エンティティの取得の前にインデックス の解決が必要になる NoSQLで複雑なクエリ要件を取り込もうとしてシステムが複 雑化する… 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を引くことになる… 転置インデックス
  12. そもそもコマンドとクエリでは要件が異なる • データ構造だけではなく他の要件も異なるので、コマンドとクエリをそれぞれを隔離する コマンド クエリ 一貫性/可用性 トランザクション整合性を使い強い一 貫性を重視する 結果整合を使い可用性を重視する データ構造

    トランザクション処理をおこない正規 化されたデータを保存することが好 まれる(集約単位など) 非正規化したデータ形式を取得するこ とが好まれる(クライアント都合のレス ポンスなど) スケーラビリティ 全体のリクエスト比率とごく少数のト ランザクション処理しかしない。必ず しもスケーラビリティは重要ではない 全体のかなりのリクエスト比率を占め る処理をおこなうため、クエリ側はス ケーラビリティが重要
  13. CQRSではない場合の問題(1/2) • クエリ要件を満たすことでリポジトリが複雑になる。クエリするだけでドメインロ ジックを呼び出さない。他にもページングやソートも扱うケースがある…。 • レスポンス用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) // アプリケーション空間で結合及びデータを捨てる } ドメインはドメインの、クエリはクエリの都合で最適化が求められる
  14. CQRSではない場合の問題(2/2) • コマンドを意識しないデータ指向では、エンドポイント、アプリケーションサービ ス、ドメインがCRUDの用語に汚染されてしまう、という仮説がある ◦ 商品の注文がcreatePurchaseItem? ◦ 注文のキャンセルがupdatePurchaseItem? • ドメインの動詞を重視するコマンド指向では、orderItem,

    cancelOrderなどユビキタ ス言語にフォーカスできるようになる。コマンドの表現によって意図が明白なイン ターフェイスを作ることができる ◦ ただこの考え方は、CRUDであっても注意深く設計すれば可能…。 ◦ 実装というより分析の段階でコマンドを使うことのメリットが強い CQRSは非機能の観点が注目されがちだが、本来の目的はコマンド指向の ドメインモデリングにある…
  15. CQRSではない場合の問題(2/2) • 利点 ◦ コマンドとクエリが分離しているため、耐障害性が高くなる(耐障害性に 寄与する)。別々にデプロイできる ◦ コマンドとクエリを必要に応じて個別に最適化できる(弾力性に寄与す る)。別々にスケールさせることもできる •

    欠点 ◦ 非CQRSと比べてコストが掛かる。目的ごとにサブシステムを分けるの で構成要素が多くなる ◦ CQRSではC/Qごとにモデルが分離するため、単一モデルとしてシンプ ルだがネットワーク全体としては複雑になる。
  16. Event Sourcingとは • 唯一信頼できる情報源(Single Source Of Truth)は、状態(ステート)ではなく(ドメイ ン)イベントという考え方 ◦ CRUDは、従来からの最新状態を常に上書きする

    • コマンドとクエリを統合するために使う 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 } 最新のエンティティを上書きする そのときのイベントを追記する
  17. 【FYI】ドメインイベントとは • イベントは過去に起きた出来事を意味する • ドメインイベントは、ドメイン上のイベント を意味する • 一般的には過去形の動詞として表現される ◦ CargoShipped

    ◦ CustomerRelocated • イベントからコマンドが想起可能 ◦ ShipCargo ◦ RelocateCustomer • イベントとコマンドは似ているが別概念 ◦ コマンドは拒否されることがある ◦ イベントはすでに起こったことを示す ショッピングカートのイベント
  18. なぜイベントを使うのか • CQRSはコマンド側からクエリ側に変更を伝 える必要がある ◦ コマンド側のドメインイベントをクエ リ側に伝える • 上記の現実的な実現手段として以下がある ◦

    Event Sourcing ◦ CDC(変更データキャプチャ)+ Outbox ▪ ミドルウェアレベルではESと酷似 • 結局はEvent Sourcing以外に現実的な選択 肢はない ◦ 詳しくは CQRSはなぜEvent Sourcing になってしまうのか を参照 35 C Q C Q 変更がないときも ポーリングで負荷 をかけてしまう 変更があるとき だけイベントを 通知する ポーリングはスケールしない EventをPub/Subする
  19. Event Sourcingの利点と欠点 • 利点 ◦ イベントは更新されず追記のみなので、スケーラビリティが確保しやすい ◦ 特定の時点のリードモデルをイベントから導出することができる ◦ ドメインイベントがあれば、リードモデルの設計をいつでもやり直せる

    ▪ データマイグレーションコストではゼロではないが ◦ 監査ログや行動履歴の分析に利用することができる • 欠点 ◦ 大量のイベントから状態をリプレイする際に時間がかかる ▪ 最新状態を保存したスナップショットを使うとリプレイ時間を短縮できる ◦ 原則的にすべてのイベントをストレージに保存する必要がある ▪ スナップショット保存時に、古いイベントを消すことも可能
  20. 新アーキテクチャの概要 38 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 クエリサイド サーバーサイド・チーム クライアントサイド・チーム リアクティブシステムとCQRS/ESを反映したアーキテクチャへ変更する • 非同期・ノンブロッキング • スーパービジョン • 位置透過性 • ステートフル • DBとの完全な同期によって読み込み不要 • ワークロードのパーティショニング • 正規化されたデータ構造を扱う • ネットワーク分断時は一貫性を重視 コマンドサイドではドメインロジックを実行してドメイン状 態を変える機能のみを提供する クエリサイドはドメインイベント をもとにクライアントにとって 都合のよいリードモデルを構 築する • 非同期・ノンブロッキング • ステートレス • 非正規型データを扱う • ネットワーク分断時は可能性を重視 • ラムダアーキテクチャでも十分可能 MessagePosted MessageDTO PostMessage MessageDTO ID = 2 ID = 3