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

メッセージ駆動が可能にする結合の最適化

 メッセージ駆動が可能にする結合の最適化

Avatar for かとじゅん

かとじゅん

November 21, 2025
Tweet

More Decks by かとじゅん

Other Decks in Technology

Transcript

  1. プロフィール 加藤潤一 経歴 10 歳からプログラミング開 始 2014-2024 年: kubell (旧

    Chatwork ) 2025 年1 月: 独立 (IDEO PLUS 社) 専門分野 ドメイン駆動設計(DDD ) 関数型プログラミング 分散システム設計 15 年以上のDDD 実践経験 | Chatwork の大規模リアーキテクチャを主導
  2. 今日伝えたい2 つの視点 本タイトルの「結合の最適化」とは、関心を分離して疎結合にすることを指しま す。 1️⃣ 分離( 疎結合) の 3 つの次元を理解する

    ⏰ 時間的分離 と 🗺️ 空間的分離 と 📖 読み書き の分離 3 つの次元すべてで分離(疎結合)を実現する必 要性 2️⃣ メッセージ駆動による 統合的解決 3 つの技術による包括的なアプローチ 非同期メッセージング(時間) 位置透過性(空間) イベント/CQRS (読み書き分離) 理論と実践: Chatwork での5 年以上の本番運用から学んだ知見を共有
  3. 前提:イベントとメッセージの関係 技術的な観点から メッセージの種類 コマンド(Command ) 命令・要求を表す コマンドリクエスト コマンドレスポンス イベント(Event )

    過去に起きた出来事 ドメインイベント システムイベント イベント駆動とメッセージ駆動 関係性 メッセージ駆動: より広い概念 コマンドもイベントも使う イベント駆動: メッセージ駆動の一種 主にイベントを使う このセッションでは メッセージング全般(コマンドとイベントの両方)で結 合を最適化することに焦点を当てます
  4. 疎結合の2 つの分離軸 ⏰ 時間的分離 コマンドがいつ処理されるか 呼び手と受け手を処理タイミング(同時存在) への依存から切り離す 非同期化で待ち時間を断つ 分離を阻害するもの ⚠️

    メソッド呼び出し(同期) 同期API 呼び出し ブロッキングI/O 🗺️ 空間的分離 コンポーネントがどこにあるか 呼び手と受け手を物理配置・デプロイ場所への 依存から切り離す デプロイメントトポロジー非依存 分離を阻害するもの ⚠️ 配置場所への依存 デプロイメント境界 ネットワーク依存性に縛られた実装 💡 重要: 空間と時間、両方の次元で疎結合を実現する必要がある
  5. モジュラーモノリスの限界 🏗️ アーキテクチャ モノリスアプリケーション 認証モジュール 注文モジュール 在庫モジュール 決済モジュール 単一プロセス 論理的な分離のみ

    できていること ✅ モジュール境界が明確 パッケージ/ レイヤーで分離 インターフェースで依存管理 できていないこと ❌ 物理的な空間分離: 単一プロセス → 独立デプロ イ不可 時間的分離: 同期呼び出し → 呼び出し元がブロ ック 😰 結果として スケールアップしかできない 一部の障害が全体に影響 デプロイが密結合(一括デプロイ) ⏰🗺️ 時間と空間
  6. 【最初に考えること】同期呼び出しのトレードオフ 同期API 呼び出し 同一故障単位(共倒れリスク) サービスC サービスB サービスA サービスC サービスB サービスA

    A はブロック B もブロック 同期リクエスト 同期リクエスト レスポンス レスポンス ⚖️ トレードオフ ✅ 同一故障単位内なら最適 シンプルなプログラミングモ デル 強い整合性保証 トランザクション境界が明確 ⚠️ 故障単位を超えると課題 共倒れ(連鎖障害) 全サービス同時稼働必須 可用性への影響 ⏰ 時間的分離
  7. メッセージ駆動による分離 非同期メッセージング サービスB の故障単位 サービスA の故障単位 サービスB メッセージ基盤 サービスA サービスB

    メッセージ基盤 サービスA すぐに次の処理へ A への返信は不要 (Fire-and-Forget) メッセージ送信 他の処理を継続 メッセージ配信 処理実行 ACK 🎯 適用場面 故障単位を分離したい場合 共倒れを避けたい 独立した可用性が必要 段階的なデグレードを許容 🎯得られること 1. 障害の分離: B の障害がA に波及しな い 2. 独立した可用性: サービス個別に稼 働可能 3. 柔軟なスケーリング: 負荷に応じた 個別調整 ⏰ 時間的分離
  8. イベント購読によるリードモデル構築 パターン サービスA の故障単位 サービスB の故障単位 A のリードモデル サービスA メッセージ基盤

    サービスB A のリードモデル サービスA メッセージ基盤 サービスB B のデータを ローカル保持 B が故障 ドメインイベント発行 イベント購読 リードモデル更新 読み取り継続可能 🎯 API 呼び出しとの違い 従来(Pull 型): サービスA に都度問い合わせ A が故障すると読み取り不可 イベント購読(Push 型): イベントを購読してローカル保持 A が故障しても読み取り継続可能 得られること 1. 故障の分離: A の障害がB の読み取り に影響しない 2. 独立した可用性: B は自律的に稼働可 能 3. 結果整合性の受容: 若干の遅延を許 容して可用性を優先 ⏰ 時間的分離
  9. Kafka/Kinesis などによるメッセージング 外部ミドルウェアの活用 サービスB メッセージ基盤(Kafka/Kinesis) サービスA サービスB メッセージ基盤(Kafka/Kinesis) サービスA 永続化

    順序保証 複数の購読者 対応可能 イベント発行 イベント配信 処理実行 🎯 特徴 ミドルウェアに依存 永続化とリプレイ機能 高い信頼性とスケーラビリティ マルチテナント対応 配送保証: at-least-once (最低1 回配信) トレードオフ メリット 確実なメッセージ配信 複数購読者への配信 運用実績のある基盤 コスト インフラ運用の負荷 追加のミドルウェア依存 学習コストと複雑性 ⏰ 時間的分離
  10. アクターシステムによるメッセージパッシング (1/2) コード例 送信側アクター 受信側アクター ⏰ 時間的分離 object OrderProcessManager {

    sealed trait Command case class SubmitOrder( items: List[String] ) extends Command def apply( stockActor: ActorRef[StockActor.Command] ): Behavior[Command] = Behaviors.receiveMessage { case SubmitOrder(items) => val orderId = generateOrderId() // メッセージ送信(ノンブロッキング) stockActor ! ReserveStock(orderId, items) // 即座に次の処理へ Behaviors.same } } object StockActor { sealed trait Command case class ReserveStock( orderId: String, items: List[String] ) extends Command def apply(): Behavior[Command] = Behaviors.receiveMessage { case ReserveStock(orderId, items) => // 在庫引当処理(非同期) reserveStock(orderId, items) Behaviors.same } }
  11. アクターシステムによるメッセージパッシング (2/2) メッセージ配信の仕組み 受信側アクター ディスパッチャ 受信側メールボックス 受信側アクター参照 送信側アクター 受信側アクター ディスパッチャ

    受信側メールボックス 受信側アクター参照 送信側アクター 即座に制御を返す ( ノンブロッキング) ! メッセージ エンキュー デキュー メッセージ配信 処理実行 ( 非同期) ⏰ 時間的分離
  12. 従来のアプローチの問題 スケールアップ 問題点: 単一プロセス内でしか動作しない スレッド数の制限がある 分散環境では使えない スケールアウト 問題点: ネットワークエラー処理が必要 ローカルとは異なるAPI

    レイテンシとタイムアウト管理 😰 根本的な問題 スケールアップとスケールアウトで異なるプログラミングモデル → コードの重複、保守性の低下、アーキテク チャ変更時の大規模な書き換え 🗺️ 空間的分離 final ExecutorService executor = Executors.newFixedThreadPool(10); CompletableFuture.supplyAsync(() -> { // 非同期でタスクを実行 return orderRepository.save(order); }, executor); HttpPost post = new HttpPost("http://host/api/orders"); post.setEntity(new StringEntity( toJson(order), ContentType.APPLICATION_JSON )); CloseableHttpResponse response = httpClient.execute(post);
  13. 統一的なプログラミングモデル 同一のコード ✨ メリット 1. アーキテクチャの進化 単一プロセス → 分散クラスタへコード変更なし 2.

    スケーリング戦略の統一 垂直・水平スケーリングが同じモデル 3. 境界の柔軟な調整 モノリス ⇄ マイクロサービス間の移行が容易 🗺️ 空間的分離 object OrderActor { sealed trait Command case class CreateOrder(order: Order, replyTo: ActorRef[OrderCreated]) extends Command case class OrderCreated(result: Result) def apply(): Behavior[Command] = Behaviors.receiveMessage { case CreateOrder(order, replyTo) => val result = processOrder(order) replyTo ! OrderCreated(result) Behaviors.same } } // 配置方法が違っても同じコード orderActorRef ! CreateOrder(order, replyTo)
  14. 位置透過性とは コンポーネントの物理的な位置を意識せずに設計 できること 同一プロセス内(ローカル) 別プロセス・別マシン(リモート) なぜ重要か 1. デプロイメントトポロジーからの独立 同じコードで単一プロセス〜分散システム 2.

    進化可能性 モノリス → マイクロサービス移行が容易 スケーリング戦略の変更が柔軟 3. コンテキスト境界の見直し 実装を変えずに境界を調整 🗺️ 空間的分離
  15. 位置透過性を支える仕組み(リモート配送) システムB システムA アクターB ディスパッチャ メールボックスB リモート機構 ネットワーク リモート機構 アクターA

    アクターB ディスパッチャ メールボックスB リモート機構 ネットワーク リモート機構 アクターA コードは同じ / 経路が自動で選択される メッセージ送信 シリアライズ・送信 受信 デシリアライズ デキュー 配信 処理実行 🗺️ 空間的分離
  16. DDD の集約とアクターモデル モジュラーモノリス 同一プロセス ローカル配送 ローカル配送 «Actor» 注文プロセスマネージャ «Actor» 在庫集約

    «Actor» 決済集約 クライアント マイクロサービス化 決済MS 在庫MS 注文MS リモート配送 リモート配送 «Actor» 注文プロセスマネージャ «Actor» 在庫集約 «Actor» 決済集約 クライアント ✨ 位置透過性の威力 注文プロセスマネージャのコードは全く変わらない: 在庫集約・決済集約の配置場所が変わっても、ActorRef が適切な経路を自動選択。 ビジネスロジックに一切の 変更が不要で、アーキテクチャが段階的に進化可能になります。 ⏰🗺️ 時間と空間の統合 stockAggregateRef ! ReserveStock(orderId, items) paymentAggregateRef ! ProcessPayment(orderId, amount)
  17. クエリ要件がドメインモデルを複雑にする 保持 1 * GroupChat -GroupChatId id -GroupChatName name -Members

    members -Messages messages +postMessage(content) Message +MessageId id +MessageBody content +Member author +DateTime sentAt 課題: メンバーやメッセージを集約内で無限に保持す ることはできない 特にメッセージ本文は容量が大きく、メモリを 圧迫する ジレンマ: 集約内に保持する → メモリ消費が増大、スケー ラビリティの問題 外部集約として分離する → 振る舞いの実装が複 雑化 解決の方向性: イベントを活用して読み取り責務を分離 する
  18. 集約アクターの設計 📥 コマンド(Command ) クライアントの意図を表現 検証が必要 例: PostMessage , DeleteMessage

    , DeleteGroupChat 📤 イベント(Event ) 発生した事実を記録 過去形で命名 例: MessagePosted , MessageDeleted , GroupChatDeleted ⏰🗺️ 時間と空間の統合 集約アクターのインターフェースは、CRUD ではなくドメインの語彙で表現します。コマンドとイベントを通じて、 業務の意図を明確に伝えることができます。 クライアント CreateGroupChat PostMessage DeleteMessage DeleteGroupChat ≪Actor≫ GroupChat GroupChatCreated MessagePosted MessageDeleted GroupChatDeleted イベントストア
  19. 読み取り要求をイベントに委譲する 従来のアプローチ 保持 1 * GroupChat -GroupChatId id -GroupChatName name

    -Members members -Messages messages +postMessage(content) Message +MessageId id +MessageBody content +Member author +DateTime sentAt 課題: 集約が Messages 全体を保持し、読み取り要求 に直接応答するため、メモリ消費とスケーラビリティに 課題がある。 読み取り要求を委譲するアプローチ 保持 1 * GroupChat -GroupChatId id -GroupChatName name -Members members -MessageIdAndAuthors messageAndAuthors +postMessage(content) : Tuple<GroupChatEvent, GroupChat> «interface» GroupChatEvent MessageIdAndAuthor +MessageId id +Member author +DateTime sentAt MessagePosted +EventId id +GroupChatId aggregateId +MessageId messageId +MessageBody content +Member author +DateTime sentAt 改善点: メッセージ本体をイベントに委譲。集約はID と 著者のみ保持し、読み取りはリードモデルで対応。 ⏰🗺️ 時間と空間 前述のイベントをうまく使えば、読み取り要求に伴う結合度を下げることができます。
  20. CQRS( コマンド・クエリ責務分離) 非CQRS 特徴: 読み取りと書き込みが同じモデル シンプルだが、複雑化すると保守が困難 スケーリングの柔軟性が低い CQRS 特徴: 読み取りと書き込みを分離

    それぞれ独立に最適化可能 複雑性は増すが、スケーラビリティが向上 ドメインモデルはドメイン状態を変える書き込み(コマンド)要求に集中し、読み取り(クエリ)要求は別のシステ ムに委譲することをCQRS と呼びます。
  21. Chatwork での実践例 Falcon プロジェクト 2016 年リリース: CQRS/ES システム 5 年以上の本番運用実績

    Akka アクターベース Event Sourcing + CQRS 成果 高可用性とスケーラビリティを実現 モノリスから段階的にマイクロサー ビス化 メッセージ駆動の有効性を実証 ⏰🗺️ 実践
  22. 参考:実践で使えるツール Apache Pekko JVM 向けアクターシステム Akka のフォーク(Apache 2.0 ) 位置透過性、Event

    Sourcing 対応 Scala/Java apache/pekko Proto.Actor (Go) Go 向けアクターシステム Go 言語で実装されたアクター 高性能・軽量 クラスタリング、リモーティング対応 asynkron/protoactor-go event-store-adapter アクターレスなCQRS/ES アクター不要のEvent Sourcing シンプルで導入が容易 Java/Scala/Kotlin/TS/PHP/Go/Rust j5ik2o/event-store-adapter fraktor-rs 🚧 Rust 版アクターシステム Pekko 相当の機能を提供予定 Rust 初の本格的実装(cluster 機能開発中) tokio/embassy/wasm 対応 j5ik2o/fraktor-rs 💡 **JVM**: Pekko / **Go**: Proto.Actor / ** 多言語ES**: Event Store Adapter / **Rust**: fraktor-rs (開発中)