本文へスキップ
Starone

ケイパビリティ

変更と負荷に耐えるソフトウェアアーキテクチャ

ドメインの境界、障害モード、そして運用するチームを軸にシステムを設計し、変更しやすさを保つプラクティスで構築します。

概要

ソフトウェアエンジニアリングは、さまざまな人が手を加え続けるなかで、負荷のもとでも何年にもわたって正しく、変更しやすい状態を保つシステムを構築する実践です。アーキテクチャとは、後から変更するのに大きなコストがかかる部分を指します。境界をどこに引くか、どのコンポーネントがどのデータを所有するか、コンポーネント同士がどう通信するか、そのいずれかが故障したときに何が起きるかです。

アーキテクチャの問題の多くは境界の問題です。データベースを共有するサービスは独立してデプロイできません。互いのテーブルに直接アクセスするモジュールは、切り離して考えることができません。5つの同期呼び出しが連なれば、全体の可用性は5つの可用性の積にしかなりません。境界を正しく引くことは、モノリスかマイクロサービスかの選択よりも重要であり、同じドメイン分析がどちらにも役立ちます。

Staroneのアーキテクトは、制約から構造を選びます。チームの規模、変更の頻度、整合性の要件、負荷の特性、運用の成熟度です。本ページではアーキテクチャとエンジニアリングプラクティスを扱います。フロントエンド、バックエンド、モバイルの各エンジニアリングには、それぞれ個別のケイパビリティページがあります。

エンジニアリング領域

技術領域

01

アーキテクチャスタイル

モジュラーモノリスは、内部の境界が強制された一つのデプロイ単位です。マイクロサービスは、それぞれが自らのデータを所有し、独立してデプロイできるサービス群です。サービス指向アーキテクチャはその中間にあり、より粒度の粗いサービスがインフラストラクチャを共有することも少なくありません。独立してリリースする必要のあるチームの数と、各部分のスケーリング要件の違いの大きさに合ったスタイルを選びます。

  • モジュラーモノリス
  • マイクロサービス
  • サービス指向アーキテクチャ
  • ヘキサゴナルアーキテクチャ
  • APIゲートウェイとBackend for Frontend
  • ストラングラーフィグによる移行
02

ドメイン駆動設計

ドメイン駆動設計(DDD)は、データベーススキーマではなく業務を分析することで境界を見いだします。境界づけられたコンテキストとは、ある用語が一つの意味を持ち、一つのチームが所有する領域です。集約は、単一のトランザクションで一緒に変更されなければならない範囲を定めます。これらの境界がモジュールやサービスとなり、システムのなかで最も長く残る決定になります。

  • 境界づけられたコンテキストとコンテキストマッピング
  • 集約と不変条件
  • ユビキタス言語
  • ドメインイベント
  • 腐敗防止層
03

イベント駆動と非同期システム

イベントは、プロデューサーと、それに反応するコンシューマーとを、時間と可用性の両面で疎結合にします。即時の応答を必要としない処理はキューに移し、バーストを吸収して遅い依存先を切り離します。その代償は結果整合性とデバッグの難しさです。そのため、疎結合にする価値がある箇所にはイベントを、呼び出し元が結果を必要とする箇所には直接呼び出しを使います。

  • KafkaとRabbitMQによるメッセージング
  • トランザクショナルアウトボックス
  • 冪等なコンシューマー
  • 複数ステップのトランザクションのためのSaga
  • デッドレターキューとリプレイ
  • スキーマをバージョン管理したイベント
04

分散システムとデータパターン

システムが複数のプロセスにまたがると、ネットワークは故障し、メッセージは二重に届き、時計は一致しなくなります。私たちはこれを前提として明示的に設計します。操作ごとに整合性のレベルを選び、書き込みを冪等にし、読み取りモデルと書き込みモデルの形や負荷が大きく異なる箇所ではCQRSで両者を分離します。イベントソーシングは、台帳や監査要件の厳しいドメインのように、変更の履歴そのものが要件である場合に用います。

  • CQRS
  • イベントソーシング
  • 冪等性キー
  • 楽観的同時実行制御
  • 分散キャッシュ
  • 整合性と可用性のトレードオフ
05

耐障害性と水平スケーリング

耐障害性とは、ある依存先の障害が一つの機能を劣化させるにとどまり、システム全体には及ばないことを意味します。水平スケーリングにはステートレスなサービスが必要で、状態はそのために作られたデータストアに保持します。どちらも、タイムアウト、回数を限定したリトライ、ロードシェディングによって最初から設計に組み込み、負荷テストと障害テストで検証します。

  • タイムアウトとバックオフ付きリトライ
  • サーキットブレーカーとバルクヘッド
  • レート制限とバックプレッシャー
  • ステートレスなサービスとオートスケーリング
  • パーティショニングとリードレプリカ
  • グレースフルデグラデーション
06

エンジニアリングプラクティス

アーキテクチャは、日々のプラクティスがそれを守ってはじめて維持されます。私たちは階層化されたテスト戦略を採り、すべての変更をレビューし、継続的に統合し、APIコントラクトをバージョン管理された成果物として扱います。重要な決定はアーキテクチャデシジョンレコード(ADR)として記録し、次にシステムを保守する人がその理由を参照できるようにします。

  • ユニットテスト、結合テスト、コントラクトテスト
  • コードレビューとトランクベース開発
  • 継続的インテグレーション
  • OpenAPIとgRPCのコントラクト
  • アーキテクチャデシジョンレコード(ADR)
  • トレーシング、メトリクス、構造化ログ

モジュラーモノリスかマイクロサービスか

マイクロサービスが解決するのは組織上の課題です。多数のチームがそれぞれ独立したスケジュールでリリースし、システムの各部分のスケールの仕方や故障の仕方が異なる、という状況です。その代償として、関数呼び出しがネットワーク呼び出しに置き換わり、分散トランザクション、サービスごとのパイプライン、そしてはるかに広い運用対象を抱えることになります。

モジュラーモノリスは、構造上の利点の大半をより低いコストで実現します。モジュールは明示的な公開インターフェースを持ち、自らのテーブルを所有し、プロセス内呼び出しまたは内部イベントで通信します。境界はビルドルールやアーキテクチャテストによって強制されます。デプロイは一つ、トランザクション境界も一つ、デバッグする場所も一つです。

現実的な進め方は、モジュラーな構成から始め、具体的な理由が生じたときにサービスを切り出すことです。あるモジュールに独自のスケーリング特性、リリース頻度、技術、分離が必要になった場合です。境界がきれいに保たれていれば、切り出しは機械的な作業で済みます。保たれていなかったのであれば、マイクロサービスにしていても分散モノリスになっていたはずで、それは両者の欠点を併せ持つものです。

アーキテクチャ

マイクロサービスアーキテクチャ
  1. クライアント

    • Webアプリ
    • モバイルアプリ
    • パートナー連携
  2. APIゲートウェイ / BFF

    • ルーティング
    • 認証
    • レート制限
    • レスポンス整形
  3. ドメインサービス

    各サービスが1つの境界づけられたコンテキストを担う

    • 注文
    • 決済
    • カタログ
    • アカウント
    • 通知
  4. 非同期メッセージング

    • Kafkaトピック
    • アウトボックス発行
    • イベントコンシューマー
    • デッドレターキュー
  5. データストア

    サービスごとにデータベースを持ち、テーブルは共有しない

    • サービス別PostgreSQL
    • Redisキャッシュ
    • 検索インデックス
    • オブジェクトストレージ
  6. プラットフォーム

    • トレーシングとメトリクス
    • ログの集中管理
    • CI/CDパイプライン
    • サービス設定
ゲートウェイの背後に、独立してデプロイできるサービスを配置します。各サービスは自身のデータを所有し、同期呼び出しが不要な箇所ではイベントで連携します。これだけ多くの可動部品を運用可能にするのがプラットフォーム層です。

サービス境界をまたぐ整合性

単一のデータベースであれば、アトミックなトランザクションが得られます。サービスをまたぐとそのような保証はなく、2フェーズコミットは、ほとんどのシステムが受け入れられない形で可用性を結合させます。標準的な答えはSagaです。ローカルトランザクションを連ね、それぞれがイベントを発行し、後続のステップが失敗したときには補償アクションを実行します。

これを安全にする仕組みが二つあります。トランザクショナルアウトボックスは、状態の変更と送信するイベントを同じデータベーストランザクションで書き込みます。これにより、イベントが失われることも、ロールバックされた変更についてイベントが発行されることもありません。冪等なコンシューマーは再配信を無害にします。ブローカーは実際には少なくとも一回の配信を行うためです。この二つが揃えば、結果整合性は予測可能なものになります。

ただし、すべての操作が結果整合性を許容できるわけではありません。残高、在庫の引当、一意性のチェックには通常、強整合性が必要です。そのため、これらの不変条件は一つの集約、一つのサービスの内側に保ちます。

部分的な障害を前提とした設計

分散システムでは、部分的な障害が通常の状態です。応答の遅い依存先は、停止している依存先よりも厄介です。呼び出し元は待っている間スレッドとコネクションを保持し続け、その遅さが上流へと広がっていくためです。

そのため、すべてのリモート呼び出しには、呼び出し元自身のレイテンシ予算から導いたタイムアウト、回数を限定しジッターを加えたリトライ、そして不健全な依存先への呼び出しを止めるサーキットブレーカーを設定します。バルクヘッドは依存先ごとに専用のコネクションプールを割り当て、一つの依存先が共有リソースを使い果たすことを防ぎます。過負荷のときには、際限なくキューに積むのではなく、優先度の低い処理を早い段階で破棄します。

  • エンドツーエンドのレイテンシ予算から設定したタイムアウト
  • 冪等な呼び出しに限定した、指数バックオフとジッター付きのリトライ
  • 依存先ごとのサーキットブレーカーとバルクヘッド
  • バックプレッシャーとロードシェディングを備えた上限付きキュー
  • 重要な機能ごとに定義されたフォールバック動作

エンジニアリングの進め方

私たちのアプローチ

アーキテクチャの検討はドメインと制約から始め、早い段階で動くコードによって検証します。

  1. 01

    ドメインの理解

    業務プロセスを、それを実際に担う人々とともに確認し、境界づけられたコンテキスト、主要なエンティティ、重要なイベントを整理します。

  2. 02

    品質特性の明文化

    可用性、レイテンシ、スループット、整合性、コンプライアンスの制約を、計測可能な目標として書き出します。構造を左右するのは、機能よりもこれらです。

  3. 03

    アーキテクチャの選定と記録

    スタイル、データの所有、通信パターンを選び、重要な選択はそれぞれ、却下した代替案とともにADRとして残します。

  4. 04

    ウォーキングスケルトンの構築

    範囲を広げる前に、CI、デプロイ、オブザーバビリティを含むすべてのレイヤーを貫く薄いスライスを一つ届けます。変更のコストが低いうちに、アーキテクチャを現実に照らして検証できます。

  5. 05

    ガードレール付きの段階的デリバリー

    自動テスト、コントラクトチェック、コードレビューを通して小さな変更をリリースし、境界を越えた場合にビルドを失敗させるアーキテクチャテストを備えます。

  6. 06

    負荷と障害のもとでの検証

    ローンチ前に負荷テストを実施し、依存先の障害を注入します。その後、実際のトラフィックデータが得られるのに合わせて決定を見直します。

トレードオフ

設計を左右する判断

アーキテクチャを確定する前に、お客様のチームとともに検討する問いです。

マイクロサービスから始めるべきか
通常はそうすべきではありません。複数のチームがすでに独立したリリースサイクルを必要としている場合や、システムの各部分のスケーリング要件や分離要件が明らかに異なる場合を除き、モジュラーモノリスから始めます。特定の境界でその必要性が明らかになったときに、サービスを切り出します。
コンポーネント間は同期APIか、非同期イベントか
クエリやバリデーションのように、呼び出し元が処理を続けるために応答を必要とする場合は、同期呼び出しを使います。他の部分が何かが起きたことを知るだけでよい場合、処理を後回しにできる場合、コンシューマーが停止していてもプロデューサーが稼働し続けなければならない場合は、イベントを使います。長い同期呼び出しの連鎖は避けます。ホップが一つ増えるごとに可用性が下がり、レイテンシが増えるためです。
CQRSは導入する価値があるか
読み取りと書き込みのワークロードの違いが大きく、一つのモデルではどちらにもうまく対応できない場合に導入します。厳格な不変条件を通じて書き込まれたデータに対する複雑なクエリや、読み取りが書き込みを桁違いに上回る場合です。通常のCRUDでは、同期を保つべきモデルが二つに増えるわりに、得られるものはわずかです。イベントソーシングは別個の判断であり、完全な履歴を必要とするドメインに限定すべきです。
REST、gRPC、GraphQLのどれを選ぶか
OpenAPIを伴うRESTは、広く理解されておりキャッシュも効くため、公開APIやパートナー向けAPIに適しています。gRPCは、厳密なコントラクト、ストリーミング、低いオーバーヘッドが活きるサービス間の内部呼び出しに適しています。GraphQLは、多くの画面が同じデータを異なる形で必要とするクライアント向けの集約に適しており、クエリコストの制御が必要です。
データベースは一つか、サービスごとか
各サービスまたはモジュールは自らのデータを所有し、インターフェースを通じてのみ公開すべきです。モジュラーモノリスでは、一つのPostgreSQLインスタンス内でスキーマを分ける形にでき、オーナーシップを保ちながら運用を単純に保てます。物理的に分離したデータベースが妥当なのは、サービスに独立したスケーリング、異なるストレージエンジン、障害の分離が必要な場合です。
レガシーシステムは書き直すか、段階的に移行するか
ほぼすべての場合、段階的に移行します。ストラングラーフィグのアプローチでは、ファサードの背後で機能を一つずつ新しいコードへ振り向けます。事業を止めずに進められ、早い段階でフィードバックが得られます。全面的な書き直しが正当化されるのは、既存システムをまったく安全に変更できない場合に限られ、その場合でもスライス単位で届けるべきです。

本番対応

本番運用に耐える条件

セキュリティの確保

セキュリティは設計の一部です。アーキテクチャ段階での脅威モデリング、すべてのサービス境界で強制する認証と認可、入力の検証、保管庫(ボールト)で保持するシークレット、CIでの依存関係スキャンを行います。サービスは必要最小限の権限で実行します。

スケーリング

サービスはステートレスで、ロードバランサーの背後で水平にスケールします。状態は、必要に応じてパーティショニングまたはレプリケーションされたデータベース、キャッシュ、ブローカーに保持します。キャパシティの限界は、インシデントではなく負荷テストによって把握します。

監視

すべてのサービスが、共通の相関IDを付けた構造化ログ、メトリクス、分散トレースを出力します。サービスレベル目標(SLO)が正常な状態を定義し、アラートはユーザーが気づく症状に紐づけます。

デプロイ

すべての変更は自動テストを通過し、イミュータブルな成果物としてビルドされ、変更を加えられることなく各環境へ昇格します。リリースにはローリング、ブルーグリーン、カナリアの各戦略を用い、データベースマイグレーションは後方互換とし、フィーチャーフラグでデプロイとリリースを分離します。

システム連携

連携はバージョン管理されたコントラクトを通じて行います。呼び出しにはOpenAPIまたはgRPC、メッセージングにはスキーマ登録されたイベントを用います。腐敗防止層がドメインモデルをサードパーティやレガシーのシステムから隔離し、コンシューマー駆動のコントラクトテストが破壊的変更をリリース前に検出します。

使用技術

ワークロードとお客様の既存環境に合わせて選定します。

  • Java
  • Spring Boot
  • Go
  • TypeScript
  • Node.js
  • PostgreSQL
  • Redis
  • Apache Kafka
  • RabbitMQ
  • gRPC
  • OpenAPI
  • Docker
  • Kubernetes
  • OpenTelemetry

ゼロから本番へ

エンジニアリングライフサイクル

  1. 01

    アイデア

  2. 02

    ディスカバリー

  3. 03

    システムアーキテクチャ

  4. 04

    UX・プロダクトデザイン

  5. 05

    アプリケーション開発

  6. 06

    AI・データレイヤー

  7. 07

    クラウドインフラ

  8. 08

    セキュリティ

  9. 09

    QA・パフォーマンス

  10. 10

    デプロイ

  11. 11

    オブザーバビリティ

  12. 12

    継続的な進化

FAQ

よくあるご質問

新しいシステムのアーキテクチャはどのように決めますか

制約から決めます。ドメインの境界、チームの数、求められる可用性と整合性、想定される負荷、そして構築したものを運用する組織の能力を確認したうえで、それらを満たす最も単純な構造を選び、その理由を記録します。

既存のコードベースを引き継いで作業できますか

はい。まずコードとその運用履歴を読み込み、変更する箇所の周囲にテストを追加し、機能開発と並行して構造を段階的に改善します。

テストはどの程度あれば十分ですか

チームが自信を持ってシステムを変更できる程度です。実際には、ドメインロジックに対する高速なユニットテスト、データベースなど実際の依存先に対する結合テスト、サービス境界でのコントラクトテスト、そして重要なユーザージャーニーに対する少数のエンドツーエンドテストを指します。

どのようなドキュメントを残しますか

アーキテクチャデシジョンレコード、コンテキスト図とコンテナ図、API仕様、運用作業のランブック、そして新しいエンジニアがシステムを動かせるようになるREADMEです。ドキュメントはリポジトリに置き、コードとともに更新されるようにします。

本番環境で動くシステムが必要な方へ

構築しようとしているものをお聞かせください。エンジニアが内容を拝見し、2営業日以内にご返信します。