Chatworkでは、リアクティブシステムとCQRS / Event Sourcingを反映した次世代基盤を構想しています。
まだ検討段階ではありますが、なぜそれらを採用するのかメリット・デメリットも含めてご説明します。
- リアクティブなソフトウェア・アーキテクチャを実現するためには? - なぜCQRS(Command Query Responsibility Segregation)を用いるのか? - なぜEvent Sourcingを用いるのか? - 次世代基盤の実現に向けて
SESSIONリアクティブシステムと次世代基盤について2021/05/26 Chatwork Dev DayChatwork株式会社加藤潤一
View Slide
自己紹介● @j5ik2o● Chatwork のテックリード● 10 歳で初めてプログラミングに触れる。SIerとしてさまざまな現場での業務を経験した後、2011 年より某d社、2013 年より大手ソーシャルゲーム企業で、それぞれ Scala やドメイン駆動設計を採用したシステム開発に従事● 2014 年 7 月より Chatwork に参画。現在はChatwork 次期アーキテクチャのプランニングや設計、開発に携わる● 最近はまっていること。オートミールうまいすよ
AGENDA1. リアクティブシステムとCQRS/Event Sourcingについて2. 新アーキテクチャの概要
1リアクティブシステムとCQRS/Event Sourcingについて
次世代基盤のためのキーワード・リアクティブシステム・CQRS/Event Sourcing
アーキテクチャ刷新(Sagrada)の目的と手段● 目的○ 2025年の事業計画に合わせて、生産性を維持・向上できるプロダクトと組織を構築する● 手段○ プロダクトの健全性を維持・向上させること○ チームが独立して改善活動をおこなえること○ 大規模なアジャイル開発が実践できること○ いつでもレスポンスを返せる状態になっていること■ リアクティブシステムを反映したアーキテクチャを実現する■ CQRS/Event Sourcingシステムを実現する社会インフラを担うからこそ必要になる手段この課題に対する考え方を解説します
ところで…ユーザの前でこういう言い訳できますか…● 「サーバ台数が足りなくて…」とか…。確かに難しい問題だが…● 「いやー、よくあることですよ」とは口が裂けても言えない。それは提供側の理屈● 使いたいときに使えないシステムは、利用者から見限られる● 競合は探せばいくらでも見つかる時代、利用者は競合へ乗り換え可能
そこで【リアクティブシステム】
リアクティブシステムとはどんな状況でも即応性を確保し「いつでも使えるシステム」を実現する●即応性とは○ 状況に応じてすばやく行動すること。「事故に即応した処置」○ 状況・情勢にあてはまること。「現実に即応した考え」「そんなの当たり前だ」といえばそうだが、実現することはそれなりに難しい…
「止まらないシステム」ではなく「回復力があるシステム」が求められている● 東京証券取引所, 株式売買システムで全銘柄で売買不能になった (2020/1)○ NASのフェイルオーバーができなかったことが原因■ NASのマニュアルでは待機系への切替の初期設定が「15秒で切替」となっていたが、実際の設定は「OFF」だった。人為的な要素が強い…■ NASが故障することで売買システムの全機能を失ってしまった○ 恒久対策は回復力を向上させること。異常を起こした部分を切り離し障害から回復できるアーキテクチャに変更していくために、MSAに移行していく、とのこと止まらないシステム(全く障害を起こさない完全なシステム)を目指すのではなく【障害から回復する能力を設計すること】に価値がある
ここからは、リアクティブなソフトウェア・アーキテクチャを実現するにはどうすればよいかという論点になります
リアクティブシステムとは支える原理手段届けたい価値即応性(Responsive)メッセージ駆動(Message-driven)伸縮性(Elastic) 耐障害性(Resilient)最終的な目的非同期・ノンブロッキング、位置透過性回復力のある設計Resilient By Design必要な手段
メッセージ駆動とはリアクティブシステムは【⽴同期・ノンブロッキング】なメッセージ・パッシングによってコンポーネント間の境界を確⽴するリアクティブシステムは【⽴同期・ノンブロッキング】なメッセージ・パッシングによってコンポーネント間の境界を確⽴するCartClientCartAddCartItem {cartId = 1,cartItemId = 4,itemId = 1,itemNum = 1,}CartItem { 1, 1, … }CartItem { 1, 2, … }CartItem { 1, 3, … }タスクの完了を待たないAddCartItemSucceededAddCartItemFailedタスクの成否をメッセージで返答するタスクがなければリソースを消費しないメッセージに反応するかどうか受信コンポーネント次第メッセージが届くならばリモートでもローカルでもよいタスクを依頼するためにメッセージを送信する
【FYI】「リアクティブシステム」と「リアクティブプログラミング」は同じ概念ではない● リアクティブシステムとリアクティブプログラミングという用語に頻繁に遭遇するが。これらは等価ではない● リアクティブシステムはアーキテクチャレベルでリアクティブ原則を適用する。リアクティブシステムを実現する手段としてFuture/Promise, Reactive Streams, アクターモデルなどのリアクティブプログラミングが利用されます。だからといって、自動的にリアクティブシステムになりません● 例えば、アプリケーションを1ノードだけにデプロイした場合、そのノードが故障したら全システムを失う。これではリアクティブ宣言の耐障害性(回復力)がないので、リアクティブプログラミングを使っていても、リアクティブシステムではないNode 1リアクティブプログラミングを使っていても1台のみで運用すると、この 1台が故障すると全システムを失う故障
リアクティブ原則(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点を解説
失敗を受け入れる/Embrace Failure● 物事がうまくいかないことを期待し、回復力のために構築する● キーとなる考え方はBulkheading○ Bulkheadingは船舶由来の用語。大型貨物船の船倉は隔壁によって多くの区画に分割される。船底が何らかの原因で破損した場合でも、影響を受けた区画だけが浸水し、他の区画は適切に密閉された状態を維持できるため浮力を維持できる● 以下の図はReactive Design Patternsで紹介されている
アクターモデルでどのようにBulkheadingするか● スーパーバイザであるコンポーネントは簡単に故障するような仕事はせずに、失敗しやすい仕事はヒエラルキー下層の専門のコンポーネントに任せる。このような構造を採用することで障害が発生しても、全体に障害が波及することを抑制する● 障害発生時はスーパーバイザに判断を委任し、その指示に従ってコンポーネントを再起動して復旧する。 このような階層的な再起動を用いる障害処理によって、障害モデルを大幅に簡素化でき、予期しない障害に直面しても生き残る可能性が高める
なぜアクターモデルなのか● 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つの言語はマルチスレッドプログラミングおける、多くの苦しみを取り除いてくれると期待されていた
ChatworkのAkka導入● 2016年末 メッセージング基盤にて、Akka,HBase,Kafkaを使って、CQRS+ESシステムを構築・運用開始 ● 他のマイクロサービスでもAkkaを積極的に採用 ● SagradaではCoreApplicationを刷新する計画
なぜCQRSなのか
自律性を表明する/Assert Autonomy● 独立して行動し、協調的に相互作用するコンポーネントを設計する● 自律性とは、各マイクロサービスが境界を維持し独立して運用できること○ 当該サービスの動作保証には、連携している他のサービスは関係がない。常に自分のサービスの行動を保証するのみ● 自律性を保つにはアプリケーションを分離する必要があります。分離には主に以下の観点がある○ DDDの境界づけられたコンテキスト単位で分離する○ CQRS/Event Sourcing観点でのコマンドとクエリに分離する在庫 EC在庫予測Command QueryCommandに障害が起きてもQueryできるようにするにはお互いに分離する必要があるドメイン境界で分割C/Qで分割
CQRSとは● Command and Query ResponsibilitySegregation = コマンド・クエリ責務分離 のこと。分離というより隔離という解釈が正しい● コマンド(書き込み)とクエリ(読み込み)をスタックごとにそれぞれに隔離することを意味する。単にドメインモデルをコマンド用・クエリ用に分割することではない● CQRSはDDDを前提としています(ドメインから本質的ではないクエリ責務を排除するための設計パターンです)。詳しくはCQRS Documents by Greg Young を参照のことWrite DB Read DBInterface AdaptorCommand ProcessorDomainInterface AdaptorInterface AdaptorRead Model UpdaterQuery ProcessorCommand Side Query SideRead Model UpdaterClient
書き込み(コマンド)と読み込み(読み込み)の要件が違うのに一つのシステムで解決しようとして無理が生じる例
RDBへの書き込みがスケールしない問題● Writerをスケールアウトしたいが無理スジ○ Write Sharding: Writer, Read Repclicaのセットを分割して、書き分ける○ Vitess(https://vitess.io/)● いずれにしてもアプリケーション(もしくはミドルウェア)で書き込みを分割する。柔軟なクエリができなくなるという代償を払う必要がある○ 書き込むデータからヒントを得て、どのDBに書き込むかを決める(ヒントを間違えると…)○ 分割されたデータどうしでは結合するクエリはできない○ 2台→4台→8台と手動でWriterを増やすさいデータの移動が必要になるそもそもRDBが向かない要件をRDBで解決しようとして複雑化する…25WriterアプリケーションA-1B-1A-2B-2ReadReplicaReadReplicaWriterアプリケーションA-1A-2ReadReplicaReadReplicaB-1 A-1B-2 A-2WriterB-1B-2A-1B-1A-2B-2A-1B-1A-2B-2
NoSQLでRDBのクエリのような使い方をしてしまう問題● NoSQLはハッシュキーでスケールアウトできる○ ハッシュキーで自動的にシャーディング(パーティショニング)される○ ただしキーでしかエンティティを解決できない● エンティティの属性でもクエリしたい…○ GSIを多用する■ 個数の上限がある○ 上限があるなら、転置インデックスを作ろう■ エンティティの更新以外にインデックスデータも更新する必要がある■ エンティティの取得の前にインデックスの解決が必要になるNoSQLで複雑なクエリ要件を取り込もうとしてシステムが複雑化する…DynamoDBアプリケーションA-1B-1A-2B-2DynamoDB{ A-1, 技術部, KATO } B-1{ A-2, 総務部, SATO } B-2GSIで部署名で検索できるように …EMP{ A-1, 技術部, KATO }{ A-2, 総務部, SATO }EMP_DEPT_IDX{ 技術部, [ A-1, A-3 ] }{ 総務部, [ B-2, B-4 ] }逆引きする際は、EMP_DEPT_IDXでIDを解決してからEMPを引くことになる…転置インデックス
そもそもコマンドとクエリでは要件が異なる● データ構造だけではなく他の要件も異なるので、コマンドとクエリをそれぞれを隔離するコマンド クエリ一貫性/可用性 トランザクション整合性を使い強い一貫性を重視する結果整合を使い可用性を重視するデータ構造 トランザクション処理をおこない正規化されたデータを保存することが好まれる(集約単位など)非正規化したデータ形式を取得することが好まれる(クライアント都合のレスポンスなど)スケーラビリティ 全体のリクエスト比率とごく少数のトランザクション処理しかしない。必ずしもスケーラビリティは重要ではない全体のかなりのリクエスト比率を占める処理をおこなうため、クエリ側はスケーラビリティが重要
【FYI】「注文するコマンド要求」と「注文情報クエリ結果」の違い● コマンド要求はシステムに送られるメッセージ○ システム向けのデータは正規化される● クエリ結果はシステムから返されるメッセージ○ 人間向けのデータは非正規化される注文ID注文日時商品ID注文数購入者ID注文ID注文日時商品ID商品名注文数購入者ID購入者名商品アカウント注文するコマンド要求 注文情報のクエリ結果system
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) // アプリケーション空間で結合及びデータを捨てる}ドメインはドメインの、クエリはクエリの都合で最適化が求められる
CQRSではない場合の問題(2/2)● コマンドを意識しないデータ指向では、エンドポイント、アプリケーションサービス、ドメインがCRUDの用語に汚染されてしまう、という仮説がある○ 商品の注文がcreatePurchaseItem?○ 注文のキャンセルがupdatePurchaseItem?● ドメインの動詞を重視するコマンド指向では、orderItem, cancelOrderなどユビキタス言語にフォーカスできるようになる。コマンドの表現によって意図が明白なインターフェイスを作ることができる○ ただこの考え方は、CRUDであっても注意深く設計すれば可能…。○ 実装というより分析の段階でコマンドを使うことのメリットが強いCQRSは非機能の観点が注目されがちだが、本来の目的はコマンド指向のドメインモデリングにある…
CQRSではない場合の問題(2/2)● 利点○ コマンドとクエリが分離しているため、耐障害性が高くなる(耐障害性に寄与する)。別々にデプロイできる○ コマンドとクエリを必要に応じて個別に最適化できる(弾力性に寄与する)。別々にスケールさせることもできる● 欠点○ 非CQRSと比べてコストが掛かる。目的ごとにサブシステムを分けるので構成要素が多くなる○ CQRSではC/Qごとにモデルが分離するため、単一モデルとしてシンプルだがネットワーク全体としては複雑になる。
なぜEvent Sourcingなのか
Event Sourcingとは● 唯一信頼できる情報源(Single Source Of Truth)は、状態(ステート)ではなく(ドメイン)イベントという考え方○ CRUDは、従来からの最新状態を常に上書きする● コマンドとクエリを統合するために使うEvent SourcingCRUD(State Sourcing)Account { ID=1,NAME=KATO }Account { ID=1,NAME=SATO }AccountCreated{ ID=1, NAME=KATO }AccountRenamed{ ID=1, NAME=SATO }最新のエンティティを上書きする そのときのイベントを追記する
【FYI】ドメインイベントとは● イベントは過去に起きた出来事を意味する● ドメインイベントは、ドメイン上のイベントを意味する● 一般的には過去形の動詞として表現される○ CargoShipped○ CustomerRelocated● イベントからコマンドが想起可能○ ShipCargo○ RelocateCustomer● イベントとコマンドは似ているが別概念○ コマンドは拒否されることがある○ イベントはすでに起こったことを示すショッピングカートのイベント
なぜイベントを使うのか● CQRSはコマンド側からクエリ側に変更を伝える必要がある○ コマンド側のドメインイベントをクエリ側に伝える● 上記の現実的な実現手段として以下がある○ Event Sourcing○ CDC(変更データキャプチャ)+ Outbox■ ミドルウェアレベルではESと酷似● 結局はEvent Sourcing以外に現実的な選択肢はない○ 詳しくは CQRSはなぜEvent Sourcingになってしまうのか を参照35CQCQ変更がないときもポーリングで負荷をかけてしまう変更があるときだけイベントを通知するポーリングはスケールしない EventをPub/Subする
Event Sourcingの利点と欠点● 利点○ イベントは更新されず追記のみなので、スケーラビリティが確保しやすい○ 特定の時点のリードモデルをイベントから導出することができる○ ドメインイベントがあれば、リードモデルの設計をいつでもやり直せる■ データマイグレーションコストではゼロではないが○ 監査ログや行動履歴の分析に利用することができる● 欠点○ 大量のイベントから状態をリプレイする際に時間がかかる■ 最新状態を保存したスナップショットを使うとリプレイ時間を短縮できる○ 原則的にすべてのイベントをストレージに保存する必要がある■ スナップショット保存時に、古いイベントを消すことも可能
2新アーキテクチャの概要
新アーキテクチャの概要38stateShardShardShardRegionRoomAggregateActorJournal DB(DynamoDB)SnapshotStore(S3)MessageBusRMU Read DBReadAPIReadAPIReadAPIControllerUseCaseWrite APIServerWrite APIServerWrite APIServerコマンドサイドakka-clsuter他のMSへlogicClientID = 1クエリサイドサーバーサイド・チームクライアントサイド・チームリアクティブシステムとCQRS/ESを反映したアーキテクチャへ変更する● 非同期・ノンブロッキング● スーパービジョン● 位置透過性● ステートフル● DBとの完全な同期によって読み込み不要● ワークロードのパーティショニング● 正規化されたデータ構造を扱う● ネットワーク分断時は一貫性を重視コマンドサイドではドメインロジックを実行してドメイン状態を変える機能のみを提供するクエリサイドはドメインイベントをもとにクライアントにとって都合のよいリードモデルを構築する● 非同期・ノンブロッキング● ステートレス● 非正規型データを扱う● ネットワーク分断時は可能性を重視● ラムダアーキテクチャでも十分可能MessagePostedMessageDTOPostMessageMessageDTOID = 2ID = 3
【FYI】InfoQ Architecture and Design 2021● 本日紹介した技術はアメリカではキャズムを超えていることになっている…。日本でも、当社の事例がキャズムを超える事例の一つとなれるように!
まとめ● リアクティブシステムを目指すにはそれなりのアーキテクチャが必要。当たり前を実現するには、高い技術力が求められる。ただし、全体が複雑になりすぎないように、濃淡をつけていけるようにしたい● 今回触れなかった、モジュラモノリスをどのようにマイクロサービスに分割していくかという、組織的にも大きな課題がある。組織戦略の問題と併せて改善が求められる● まだまだ これから事業的にも技術的にも成長の余地があるので、プロダクトを共に発展させていくエンジニアを募集中です!!!何かあれば気軽にご相談ください!
本日はご静聴ありがとうございました