AWS / SAP-C02

SAP-C02-176 · セクション 20 / タスク 4.4

Application Componentを疎結合化する

Tight Coupling / Loose Coupling / Amazon SQS / Asynchronous Processing / Failure Isolation / Scalability

既存アプリケーションをモダナイズするときは、Amazon SQSなどのサービスを最初に選ぶのではなく、現在の構成でどの依存関係が変更・障害・負荷を他のコンポーネントへ波及させているかを特定します。

すでに分離されたコンポーネントでも、

個別にデプロイできない
個別にスケールできない
一方の障害が無関係な機能まで停止
共有データ構造の変更で同時改修が必要

という状態なら、Tight Couplingが残っています。

モダナイゼーションでは、このような依存関係を観察し、Loose Couplingへ変更する価値が高い境界を見つけます。

1. 運用上の兆候から疎結合化候補を特定する

既存システムでは、設計書だけではTight Couplingが見えない場合があります。

そのため、実際の運用で発生している次のような兆候を確認します。

下流の遅延
↓
上流の応答時間も急激に悪化

1コンポーネントの障害
↓
無関係な機能まで停止

一方だけ変更したい
↓
複数コンポーネントを同時デプロイ

一方だけ負荷増加
↓
複数コンポーネントを同じ単位で増強

例えば、注文APIから呼び出している付加情報生成処理が遅延すると、注文結果ではその情報を利用していないにもかかわらず、注文APIのスレッドが待機してタイムアウトするとします。

この場合、注目するのは、

Amazon SQSを使えるか

ではなく、

本来独立していてよい処理の状態が
注文受付の性能・可用性へ
波及している

という事実です。

このような依存関係を、モダナイズ時の優先候補として抽出します。

2. Tight Couplingを複数の軸で探す

Tight Couplingは、同期通信だけに現れるとは限りません。

既存アプリケーションでは、少なくとも次の軸を確認します。

確認する結合兆候
実行時の結合一方の遅延・停止が他方へ波及する
変更時の結合一方の変更で他方も同時改修・同時デプロイが必要になる
データの結合複数コンポーネントが相手の内部データ構造へ直接依存する
拡張時の結合片方だけ負荷が増えても複数コンポーネントを同時に増強する
リソースの結合一方の高負荷が共有リソースを枯渇させ、他方まで停止する

例えば、

帳票生成処理が高負荷
↓
共有接続プールを使い切る
↓
顧客向けAPIもデータベースへ接続できない

のであれば、

帳票生成の障害領域
と
顧客向けAPIの障害領域

が十分に分離されていないことが分かります。

疎結合化の機会は、このような意図しない影響の伝播から探します。

3. Asynchronous Processingへ切り出せる境界の兆候を探す

Asynchronous Processingの基礎を改めて選択するのではなく、既存システムの中にすでに存在する時間的に分離可能な処理を探します。

例えば、

要求を受け付ける
↓
ワークテーブルへレコードを登録
↓
定期処理がポーリング
↓
後から処理

という仕組みがある場合、アプリケーション自体がすでに、

受付
と
実処理

を時間的に分離している可能性があります。

他にも、

夜間バッチで後処理
再実行スクリプトで処理
一時ファイルへ要求を書き出す
共有テーブルを作業キューとして利用

などは、非同期の受け渡し境界が既存システム内に隠れている兆候です。

モダナイゼーションでは、

すでに後から処理している
↓
独自実装で受け渡している
↓
明示的な非同期境界へ置き換えられるか

を確認します。

4. Amazon SQSへ置き換えられる独自の作業受け渡しを探す

既存システムでは、Amazon SQSの代わりにデータベーステーブルを作業キューとして使用している場合があります。

例えば、

受付API
↓
WORK_TABLEへINSERT

処理ワーカー
↓
WORK_TABLEを定期ポーリング
↓
未処理行を取得

という構成です。

この方式では、受付側と処理側が同じテーブル構造や状態管理方法へ依存することがあります。

さらに、

ポーリング処理
ロック制御
処理済み状態
再実行状態

などをアプリケーション側で管理している場合があります。

このように、

1件の作業を
処理側へ後から渡す

という用途を独自実装しているなら、Amazon SQSへ受け渡し責任を移せるかを検討します。

受付API
↓
Amazon SQS
↓
処理ワーカー

ここでのモダナイゼーション判断は、

SQSは非同期処理に使える

という機能確認ではありません。

既存の独自作業キュー
↓
管理対象となっている受け渡し責任
↓
Amazon SQSへ移せるか

を評価することです。

5. Failure Isolationは現在の障害範囲から評価する

Failure Isolationの機会を探すときは、過去の障害でどこまで影響が広がったかを確認します。

例えば、

画像変換処理の異常
↓
CPU使用率上昇
↓
共有ワーカープール枯渇
↓
画像変換とは無関係な受付処理も停止

していたとします。

この場合、画像変換の障害が受付処理の障害へ拡大しています。

モダナイゼーションでは、

どの障害を
どのコンポーネント内に
閉じ込めたいか

を明確にします。

例えば目標を、

画像変換が停止
↓
画像変換処理だけが遅延

受付処理
↓
要求受付を継続

と定義します。

重要なのはAmazon SQSそのものではなく、障害の影響範囲をどこまで縮小するかを先に決めることです。

その目標を実現する手段として、非同期境界、独立したリソース、独立した実行単位などを評価します。

6. Loose Couplingでは内部実装への依存も減らす

疎結合(Loose Coupling)は、通信を非同期化することだけではありません。

コンポーネント間で受け渡す情報が、相手の内部実装へ強く依存していないかも確認します。

例えば送信側が、

受信側データベースの
テーブル構造そのもの

をメッセージ形式として使用しているとします。

この状態では、

受信側のテーブル変更
↓
送信側も同時改修
↓
同時デプロイ

が必要になりやすくなります。

代わりに、

注文ID
処理種別
必要な業務情報

など、そのコンポーネント間で必要となる情報を明示した契約として受け渡します。

送信側
↓
明示されたメッセージ契約
↓
受信側
↓
受信側自身の内部形式へ変換

とすれば、受信側の内部実装変更を送信側へ波及させにくくできます。

疎結合化では、実行時の依存と変更時の依存の両方を確認します。

7. Scalabilityではコンポーネントごとに拡張できる状態を作る

密結合された構成では、異なる処理特性を持つコンポーネントを同じ能力で拡張していることがあります。

例えば、受付処理と画像変換処理を一体として拡張している場合、受付だけ増えた場合でも両方を同じように増やす必要が生じることがあります。

疎結合化によって、

受付処理
↓
非同期の受け渡し
↓
画像変換処理

と分離すると、

受付側
→ 受付側の需要に合わせる

画像変換側
→ 画像変換側の処理特性に合わせる

というように、それぞれを独立したScalabilityの境界として扱いやすくなります。

疎結合化は処理能力が無限になることを意味しません。

目的は、片方の需要や処理時間にもう片方の拡張方法を必ず合わせなくてもよい状態を作ることです。

8. 業務上維持すべき依存関係を候補から除外する

既存システムから疎結合化候補を抽出するときは、すべての依存関係を変更対象にしません。

まず、

業務上、その結果を確認しなければ
現在の処理を確定できない

依存関係を識別します。

例えば、

注文確定
↓
決済承認

予約確定
↓
座席確保

のように、結果そのものが業務上の確定条件となっている場合があります。

このような依存関係は、疎結合化候補として自動的に選ぶのではなく、業務上維持すべき依存として分類します。

一方、運用上たまたま同期している、実装上たまたま同じ処理経路にあるだけの依存関係を、優先的なモダナイゼーション候補として評価します。

9. コンポーネント単位で段階的に疎結合化する

既存アプリケーションのすべての依存関係を一度に変更する必要はありません。

例えば、

注文受付
├─ 決済
├─ 在庫確保
├─ 通知
└─ 分析

という構成なら、

決済
在庫確保
→ 必要な依存として維持

通知
分析
→ 疎結合化を評価

というように、依存関係ごとに判断できます。

疎結合化では、アプリケーション全体を一度に作り直すのではなく、不要な依存関係を1つずつ見つけ、適切な境界を設けるという段階的なモダナイゼーションが可能です。

10. Containerへ分けることと疎結合化を混同しない

コンポーネントが別々のContainerで動作していても、

Container A
↓ 同期呼び出し
Container B

で、

Bが停止するとAも必ず失敗する
Bの内部変更でAも必ず変更する

なら、コンポーネント間には強い依存関係が残っています。

反対に、実行基盤が異なっていても、明示された契約、独立した障害境界、独立した拡張境界が成立していれば、疎結合な構成を作れます。

つまり、配置単位を分けることと、コンポーネント間の依存を減らすことは別の判断です。

11. Publish / Subscribeとは判断対象を分ける

ここで扱う中心は、

1つの作業要求
↓
1つの論理処理系へ受け渡す

という既存依存関係のモダナイゼーションです。

一方、

1つの出来事を
複数の独立した処理へ届ける

イベント内容によって
複数の処理先へ振り分ける

といったPublish / Subscribeやイベントルーティングでは、別の判断軸が必要になります。

したがって、疎結合化したいからすべてを同じメッセージング構成にする、とは判断しません。

まず、現在の依存関係の形を確認します。

12. Tight CouplingとLoose Couplingを比較する

観点Tight CouplingLoose Coupling
障害範囲他コンポーネントまで波及しやすい影響を限定しやすい
変更同時改修・同時デプロイが発生しやすい個別変更しやすい
データ構造相手の内部形式へ依存しやすい明示した契約へ依存する
拡張複数コンポーネントを同じ単位で増強しやすいコンポーネントごとに拡張しやすい
リソース共有リソース枯渇が他処理へ波及しやすい障害・負荷の境界を分けやすい
モダナイズ対象業務上必要な依存も含めて維持されがち不要な依存だけを分離できる

重要なのは、Loose Couplingの方が常に正しい、ではなく、業務上必要な依存と実装上残っている不要な依存を区別することです。

13. 疎結合化の機会を特定する判断フロー

既存コンポーネント間の依存を列挙
        ↓
障害・変更・負荷が
別コンポーネントへ波及しているか
        ↓
業務上維持すべき依存か
        ↓
いいえ
        ↓
どの結合が原因か確認
        │
        ├─ 時間的な結合
        ├─ 内部データ構造への結合
        ├─ 共有リソースへの結合
        └─ 同一拡張単位への結合
        ↓
既存の独自非同期処理や
作業受け渡しが存在するか
        ↓
Amazon SQSなどへ
受け渡し責任を移せるか
        ↓
障害範囲を限定できるか
        ↓
コンポーネントごとに
独立してスケールできるか
        ↓
変更時の影響も限定できるか
        ↓
Loose Couplingへ改善

最終的には、サービスを導入したかではなく、

障害の波及
変更の波及
リソースの共有
拡張単位の結合
内部実装への依存

をどこまで減らせるかで判断します。