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

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

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

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

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 (開発中)