ソフトウェアアーキテクチャの基礎
1. イントロダクション
ソフトウェアアーキテクチャとは
- 明確な定義はないが「重要なもの」
- 非常に広範囲である
- 変化と拡大を続けている
- 文脈なしに成り立つ銀のアーキテクチャは存在しない
ソフトウェアアーキテクチャを構成する 4 要素
- 構造
- 単にスタイルのこと(マイクロサービス/レイヤード...)
- これだけではアーキテクチャ全体は定まらない
- アーキテクチャ特性
- システムに直接的には関わらないものの、成功のために必要な基準のこと
...ility
(可用性、信頼性、スケーラビリティ、セキュリティ...)
- アーキテクチャ決定
- システムをどう構築するかのルール
- 許されることと許されないことを決めたもの
- 設計指針
- ルールではなく、こうしたほうがいいよ的なガイドライン
アーキテクトがやるべきこと
- アーキテクチャ決定を下したり、設計指針を定義したりする
- アーキテクチャを継続的に分析・改善する
- 最新のトレンドを把握する
- 決定の遵守を徹底する
- 広く浅く多様なものに触れ、経験し、知る
- 事業ドメインの知識を学ぶ
- 対人スキルを身につける
- 政治を理解し、舵取りをする
プロセスとエンジニアリングプラクティスを分けて考えろ、後者に注目せよ
- プロセス
- チームの作り方、ミーティングの進め方、ワークフローの整理方法などを決めるもの
- e.g. スクラム、ウォーターフォール...
- 未知の未知には対処しやすいのはイテレーティブなプロセス
- エンジニアリングプラクティス
- 再現性のある、プロセスに依存しない、技術的な方法論
- e.g. 継続的インテグレーション、テスト駆動開発、自動化...
開発から運用までトータルで見た方が良い結果を生むよね、というのが DevOps という考え方。
- アーキテクチャの法則
- すべてはトレードオフである
- How よりも Why を重視せよ
2. アーキテクチャ思考
アーキテクチャ思考とは、アーキテクトの目線で物事を見ること。
アーキテクチャはアーキテクトが作り、設計は開発者が作るもの。 ただし、その流れは一方向ではなく、双方向に影響し合うものでなければならない。
アーキテクトは、技術の深さよりも幅が大事。 これを認識しておかないと、全ての技術を習得しようとしてボロボロになったり、古く陳腐化した専門性で戦ったりしてしまう。
アーキテクチャとは Google で見つけられないものである。 つまり、"It depends"であり、すべてはトレードオフなのだ。 プラス面とマイナス面を洗い出し、どれを大事なのか考えよう。
ビジネスドライバーの求める要件を、アーキテクチャ特性(ility)に変換せよ。 このためには事業ドメインの知識と、ステークホルダーとの協力的な関係が必要。
アーキテクチャのスキルだけではなく、コーディングスキルを維持することも大事。うまくやるには以下がおすすめ。
- ボトルネックにならない部分の開発をする。例えば、負債解消、バグ修正、小さな機能追加など。
- PoC を頻繁に行う。この時、なるべくプロダクションレベルのコードにすること。ひどいコードが再利用されることを防ぐためと、自身の鍛錬のため。
3. モジュール性
定義
モジュールとは、関連するコードを論理的にグループ化したもの。モジュール性とは、グループ化がどれだけうまく行われているかを示す指標。
ほとんどの言語にはモジュールを定義するための仕組み、例えばクラスや関数や名前空間などが備わっている。開発者はそれらを活用しながらモジュール性を実現していく。
ソフトウェアは常にエントロピー増大の危機にさらされており、良い状態を保つには常にエネルギーを費やし続けて秩序(≒ モジュール性)を維持する必要がある。
モジュール性の話はアーキテクチャ特性には一般的には含まれず暗黙的ではあるものの、重要な要素である。
モジュール性の指標とその計測手法
凝集度 / Cohesion
モジュール内の要素がどれだけ密接に関連しているかを示す指標で、高いほど良い。凝集度が低いということは、別のモジュールと結合しないかぎり有益な結果がえられないということだから。
LCOM という、フィールドを介して結合されていないメソッド群の数により、クラスの凝集度の低さを計測する方法がある。
結合度
モジュール間の依存関係の強さを表す指標。
求心性結合(Afferent Coupling)は外部からの接続数であり、これが多いということは、他の複数のモジュールからよく利用されている状態を指す。システムにとって重要なサービスを提供している可能性がある。
遠心性結合(Efferent Coupling)は外部に向いた接続数であり、これが多いということは、他の複数のモジュールを利用している状態を指す。他のモジュールに多く依存しているため、変更の影響を受けやすい可能性がある。
不安定度
求心性・遠心性結合のバランスを示す指標。両方の結合に占める遠心の割合が高いほど、不安定度が高いといえる。
抽象度
モジュール内における、抽象(抽象クラスやインターフェース等)と実装の比率のこと。抽象度が高いということは、モジュールが抽象的で、実装の詳細が隠蔽されているということ。
main メソッドしかない巨大な関数なら抽象度は 0 で、逆にインターフェースだけのクラスなら抽象度は 1 になる。
主系列からの距離
主系列からの距離は、ソフトウェアのモジュール性において、抽象度と不安定度という 2 つの指標のバランスを評価するための概念である。主系列からの距離が遠いということは、以下の 2 つの可能性を示唆する。
- 無駄ゾーン: モジュールの利用者が少ないにもかかわらず、抽象化が進んだ状態。全部ベタで書けばすむんじゃないの?的な。
- 苦痛ゾーン: モジュールの利用者が多いにもかかわらず、抽象化がされていない状態。変更と保守を容易にするために抽象化しようぜ!的な。
コナーセンス / Connascence
コナーセント = 接続
コナーセンスは、ソフトウェアの変更容易性を測るための概念である。2 つのコンポーネント間にコナーセンスがある場合、一方を変更すると他方にも変更が必要になる。コナーセンスの種類とその解説は以下の通り。1-5 は静的なコナーセンス、6-9 は動的なコナーセンスである。1 に近いほど、より良く、より弱く、より変更容易性が高い。
-
名前のコナーセンス (Connascence of Name - CoN)
- 変数名、関数名、メソッド名、クラス名、モジュール名等、名前による参照のこと
- 例: あるメソッド名が変更されると、そのメソッドを呼び出している全ての箇所も修正が必要になる
-
型のコナーセンス (Connascence of Type - CoT)
- 型による参照のこと
- 例: ある型が変更されると、その型を使っている全ての箇所も修正が必要になる
-
意味のコナーセンス (Connascence of Meaning - CoM)
- あるコンポーネントが、他のコンポーネントの動作や意味に依存している状態です。Magic Number など。
- 例: 真を
1
と仮定して書かれているコード群は、その変数の意味が変更されると全て修正が必要になる。なお、Magic Number 等の代わりに名前付き定数を使うことで「名前のコナーセンス」に昇格できる。
-
位置のコナーセンス (Connascence of Position - CoP)
- あるコンポーネントが、他のコンポーネントの物理的な位置に依存している状態です。
- 例: 引数の順序
-
アルゴリズムのコナーセンス (Connascence of Algorithm - CoA)
- 複数のコンポーネントが、同じアルゴリズムに依存している状態です。
- 例: ある暗号化アルゴリズムを使用しているコードは、そのアルゴリズムが変更されると修正が必要になります。
-
実行順序のコナーセンス (Connascence of Execution - CoE)
- あるコンポーネントの実行が、他のコンポーネントの実行順序に依存している状態です。
- 例: ある関数が他の関数の後に実行されることを前提としているコードは、実行順序が変更されると修正が必要になります。
-
タイミングのコナーセンス (Connascence of Timing - CoT)
- あるコンポーネントの実行が、他のコンポーネントの実行タイミングに依存している状態です。
- 例: 複数スレッド間で同時に実行されると競合が発生するため、同期処理が必要になるケースなど
-
値のコナーセンス (Connascence of Value - CoV)
- 複数のコンポーネントが、同じ値に依存している状態です。
- 例: 分散トランザクションのように、全ての値を同時に更新するか、全く更新しないかのどちらかでなければならない場合など
-
アイデンティティのコナーセンス (Connascence of Identity - CoI)
- 複数のコンポーネントが、同じオブジェクトのインスタンスに依存している状態です。
- 例: あるシングルトンオブジェクトにアクセスするコードは、そのシングルトンオブジェクトが変更されると修正が必要になります
コナーセンスのベストプラクティス
- 強いコナーセンスは弱いコナーセンスに置き換える
- コード間の距離が遠くなるにつれ、弱いコナーセンスを使用する
- システムを分割して全体的なコナーセンスを最小に抑える
4. アーキテクチャ特性
アーキテクチャ特性(architectural characteristics)とは、ドメイン領域とは直接関連しないがソフトウェアが満たすべき考慮事項のこと。 非機能要件(non-functional requirement)や品質特性(quality characteristics)という呼び名を改善したもの。 すべての特性を満たそうとするのではなく、設計の構造に変更を与えうるものや、成功のために不可欠または重要であるものだけを選択することが大事。
特性の定義は曖昧だったり、重複していたり、文脈によって異なる意味を持ったりする。 また、特性は個々のソフトウェア固有の要因を元に、独自に発明すべきものである。 とはいえ、ISO/IEC 25010 という ISO が定義している特性のリストが一つの参考になりそう。
https://iso25000.com/index.php/en/iso-25000-standards/iso-25010
以下の 9 つのカテゴリと、配下にサブカテゴリがある。
- Performance efficiency
- 時間、リソース、容量の効率
- Compatibility
- 共存可能性や相互運用性
- Interaction Capability
- ユーザビリティのこと。ユーザーが効果的・効率的・満足に利用できるか。
- Appropriateness recognizability / Learnability / Operability / Inclusivity ...
- Reliability
- 指定した条件と期間においてシステムが正常に動作し続けるか
- Faultlessness / Availability / Fault tolerance / Recoverability
- Security
- 意図的な悪意ある行動に対して、システムが保護された状態にあるか
- Confidentiality / Integrity / Non-repudiation / Accountability / Authenticity / Resistance
- Maintainability
- 変更や修正が容易か
- Modularity / Reusability / Analyzability / Modifiability / Testability
- Flexibility
- 多様な目的に応じて柔軟に対応できるか
- Adaptability / Scalability / Installability / Replaceability
- Safety
- 非意図的あるいは非人為的な行為に起因して、人の生命、健康、財産、または環境が危険にさらされることがないか
- Operational constraint / Risk identification / Fail safe / Hazard warning / Safe integration
- Functional Suitability
- ユーザーのニーズを満たしているか
- 著者はこれは特性には含まれないと主張している
サポートするアーキテクチャ特性を増やせば増やすだけ複雑さは増し、うまくいかなくなる。 最初からすべての特性を満たす「最善のアーキテクチャ」を狙ってはいけない。 「少なくとも最悪ではないアーキテクチャ」からスタートし、必要に応じてイテレーティブに改善していけ。
5. アーキテクチャ特性を明らかにする
特性を明らかにする方法は以下の 3 つ。
ドメインの関心ごとからアーキテクチャ特性を導出する
ドメイン領域で大事にされていることを ility に変換する。ステークホルダーと議論して決めていく。
- 時間と予算 -> シンプルさ
- ユーザー満足度 -> パフォーマンス、可用性、耐障害性、テスト容易性、デプロイ用意性、アジリティ、セキュリティ
- 合併・買収 -> 相互運用性、スケーラビリティ、拡張性
要件からアーキテクチャ特性を導出する (明示的な特性)
要件に書いてある条件をアーキテクチャ特性に変換する。 例えば、ユーザー数やユースケースから必要なパフォーマンスやスケーラビリティを導出するなど。
要件に書いていないアーキテクチャ特性を導出する (暗黙的な特性)
可用性、信頼性、セキュリティなどはわざわざ要件に書いていないことが多い。 必要であればこれらも選択肢に加える。
6. アーキテクチャ特性の計測と統制
アーキテクチャ特性は定義が様々かつ複合的であるため、議論が難しい。 これらを解決するには、客観的定義、つまり計測が有用である。
- 運用面の計測
- Performance を測るために First Contentful Paint を使うなど
- 構造面の計測
- Maintenability を測るために循環的複雑度を使うなど
- プロセス面の計測
- Agility を測るためにテストカバレッジやデプロイ失敗率を使うなど
アーキテクチャ特性を守ることを開発者に徹底してもらう、つまり統制も大事。 例えばモジュール性の統制を取るには、以下のような事柄について CI などでチェックできる仕組みを作るとよい。
- 循環依存
- 主系列からの距離
- コード上のふさわしくない場所での DB アクセス
7. アーキテクチャ特性のスコープ
アーキテクチャ量子とは、アーキテクチャ特性を考える時に一度に見渡すべき最小の範囲のこと。 システム全体とは限らず、より狭い範囲になることもある。以下の 3 つの勘案して決定する。
まずは、機能的凝集性を持つこと。目的が単一なら求められる特性も限定しやすいから。DDD の境界づけられたコンテキストと合わせるのが一般的である。
つぎに、スコープ内の要素同士は同期的なコナーセンスを持つこと。コナーセンスを持つもの同士は、片方を変更したらもう片方も変更しないといけないので、同じアーキテクチャ量子に含まれなければならない。ただし、動的かつ非同期なコナーセンスを持つもの同士は、相手を待つ必要がなく独立しているため、同じアーキテクチャ量子に含まれる必要はない。
最後に、単独でデプロイが可能であること。例えば API サーバーと DB は同じ量子に含める必要がある。
8. コンポーネントベース思考
コードの集まりをモジュールと呼ぶ。 コンポーネントとは、モジュールを言語特有の仕組みによってパッケージに変換したもの。 もっとも単純なコンポーネントは、関数やクラスをよりメタな何かでラップしたものであり、しばしばライブラリとも呼ばれる。 コンポーネントはアーキテクチャを構成するもっとも小さな要素である。
コンウェイ
コンウェイの法則とは、組織設計がシステム構造にそのまま転写されがち、という法則である。
逆コンウェイ戦略とは、望ましいシステム構造のために組織を改善していくことである
アーキテクトの役割
アーキテクチャの最上位をどう分割するかを考えるのはアーキテクトの重要な役割である。
技術による分割は、技術的な関心(実装の詳細)により分割する方法。例えばレイヤードアーキテクチャである。 コンポーネント間が技術的に疎結合になり、専門部隊を割り当てやすい。 反面、現実の大半の作業では技術を横断した作業を必要とする点に注意が必要。
ドメインによる分割は、ビジネスドメインにより分割する方法。例えばマイクロサービスやモジュラーモノリスである。 もっとも頻繁に発生する変化(ドメイン単位での機能実装など)に素早く対応することに適している。
最近は、モノリスでも分散アーキテクチャでも、ドメインによる分割が増えている。
開発者の役割
コンポーネント分割はアーキテクトと開発者が強調してイテレーティブに進めていくべきものではあるが、 コンポーネントをクラスや関数などに細分化していくのは主に開発者の仕事だ。
コンポーネント構造の作り方
- ざっくりコンポーネント群を作ってみる
- 要件を割り当ててみて、適切な粒度になるよう調整する(e.g. このコンポーネント役割多すぎじゃない?)
- ロール・責務を分析して、適切な粒度になるよう調整する(e.g. 認証とユーザー管理は別のコンポーネントにしたほうがよくね?)
- アーキテクチャ特性を分析して、適切な粒度になるよう調整する(e.g. この部分は冗長性いらないよね?)
- イテレーティブにコンポーネントを再構成する
コンポーネントの粒度
粒度が荒いと、コードが結合された状態になり、デプロイ容易性やテスト容易性が下がる。 粒度が細かすぎると、結果を得るまでのオーバーヘッドが大きくなる。
コンポーネント設計
テーブルをそのままコンポーネントにする、「エンティティの罠」にハマるな。それはただの ORM だ。
- アクター・アクションアプローチ
- 特定の役割を持つアクターが、どのようなアクションをするかを考える
- 汎用的である(モノリスでも分散でも、ウォーターフォールでもアジャイルでも)
- イベントストーミング
- メッセージやイベントを中心に設計する
- 分散向き
- ワークフローアプローチ
- イベントストーミングに代わるもので、作業や手順に着目する
モノリス or 分散
設計プロセスでアーキテクトが発見した量子の数により、アーキテクチャスタイルを決定する。 この決定は早ければ早いほどいい。
量子が 1 つならモノリスを採用できる。モノリスはいいぞ。 複数のアーキテクチャ特性を持たせたい場合は、自ずと分散アーキテクチャになる。
9. アーキテクチャスタイル
アーキテクチャパターンともいう。
基礎的なパターン
例えば「レイヤー」の概念は古くから存在する。
巨大な泥団子 / Big Ball of Mud はアーキテクチャ構造が存在しないクソコードのこと。残念ながら現実世界で非常によく見られる。
1 層アーキテクチャ(ユニタリーアーキテクチャ)は、ただ 1 つのコンピューターとその上で動くソフトウェアのこと。コンピューター黎明期のメインフレームなどが該当する。現代にはあまり存在しない。
2 層アーキテクチャ(クライアント・サーバー)は、Access のようなデスクトップアプリ + DB サーバーの構成や、現在の Web アプリのようなブラウザ + Web サーバーの構成を指す。
3 層アーキテクチャもある。JS が動くフロントエンド + Java などのアプリケーションサーバー + 強力な商用 DB サーバーなどの構成からなる。90 年代後半に流行った。
モノリシック vs 分散
- モノリシック
- レイヤードアーキテクチャ
- パイプラインアーキテクチャ
- マイクロカーネルアーキテクチャ
- 分散
- サービスベースアーキテクチャ
- イベント駆動アーキテクチャ
- スペースベースアーキテクチャ
- マイクロサービスアーキテクチャ
分散アーキテクチャは強力だが、以下のような問題に対処する必要があり、大きなトレードオフが発生する。
ネットワークは信頼できないので、タイムアウトやサーキットブレーカーのような機構が必要になる。
関数呼び出しのレイテンシーが大きい。ローカル呼び出しならナノ秒で住むが、RPC などはミリ秒単位になる。ロングテールレイテンシー(95-99%タイルのレイテンシー)を把握して必要な対処をすることが大事。
帯域幅は有限であるため、例えば GraphQL のような必要最低限のデータを取得する仕組みがないと、あっという間に食いつぶす。
ネットワークは安全ではない。分散システムは攻撃を受ける表面積が飛躍的に増えるので、高度なセキュリティ対策が必要になる。
常に変化するネットワークトポロジー(接続形態)への対応が必要。些細な機器の更新がレイテンシーに影響を与えた結果、タイムアウトを引き起こしてシステムダウンするようなこともある。
ネットワーク管理者はたくさんいる。コミュニケーションを取るべき管理者は一人では済まず、コストがかかる。
転送コストがかかる。これはレイテンシーの話ではなく RESTful 呼び出しにかかる費用の話で、モノリスと比べるとかなり高い。
ネットワークにはムラがある。ネットワーク内には違なるメーカーの様々な機器が存在するし、それらが協調して完全にうまく動く保証はない。
ロギングが難しい。分散ロギングしないと問題の追跡すらできない。
トランザクションが難しい。分散トランザクションにより結果整合性を担保するのが関の山。ACID トランザクションのように即時の一貫性は保証できない。
10. レイヤードアーキテクチャ
ドメインではなく技術による分割を行うアーキテクチャ。 例えばプレゼンテーション層、ビジネス層、永続化層、データベース層などに分ける。
コンウェイの法則により、無意識にこれを選択している組織も多い。
小規模・低予算・シンプルな Web サイトに適している。 また、アーキテクチャスタイルを未決定の場合における暫定の出発点としても最適な選択肢と言える。 その場合は、極力少ないレイヤーからスタートし、のちの変更を容易にしておくことが重要。
特性としては、以下のようなものがある。
- スケールしない - 常に単一のアーキテクチャ量子なので
- デプロイ容易性、テスト容易性が低い - たった三行の変更でも全体のデプロイやテストが必要
- 耐障害性が低い - 1 箇所クラッシュすれば全部止まる
開放レイヤーと閉鎖レイヤーという区分がある。 開放レイヤーは飛ばして次の層にアクセスされて構わないが、閉鎖レイヤーは必ず経由する必要がある。 原則は閉鎖だが、特定のケースでは開放を使うことが理にかなっている場合もある。
アーキテクチャシンクホールアンチパターンに注意。 レイヤーをまたいで単にデータを受渡しているだけのバケツリレーのこと。 リクエストの 80%がそれになっているなら、このアーキテクチャは適切でないことを示しているといえる。
DDD と相性が悪い(マジ?)。
11. パイプラインアーキテクチャ
シェルにおけるパイプのように、データをフィルターに通して処理を行うアーキテクチャ。 ETL などに最適。
- 登場人物
- フィルター - 以下の4種類
- プロデューサー - 出力を担当する。ソースともいう。
- トランスフォーマー - データを変換する。map といえる。
- テスター - 検査に基いてオプショナルにデータを生成する。reduce といえる。
- コンシューマー - 処理の終了点
- パイプ - フィルター間の通信経路
- フィルター - 以下の4種類
技術による分割であるといえる。 アーキテクチャ量子は 1 つになる。 シンプルで理解しやすくコストも低い。 モジュール性が高いので、デプロイ容易性とテスト容易性はレイヤードよりは少し高いといえる。 とはいえモノリスなので、スケールや耐障害性などのマイナス面はレイヤードとほぼ一緒。
12. マイクロカーネルアーキテクチャ(プラグインアーキテクチャ)
コアシステムとプラグインからなるアーキテクチャである。
e.g. Eclipse などのカスタマイズ性の高いソフトウェア e.g. アメリカの税務申告書である Form1040
コアシステムはレイヤードやモジュラーモノリスとして実装され、ごく基本的な機能だけを実装する。 DB へのアクセスやプラグインの呼び出しを担当する。
コアシステムを強化・拡張するための特殊な処理や追加機能などは、ほぼ全てプラグインに委ねられる。 プラグイン同士には依存関係がないのが理想的である。 プラグインは DB にはアクセスせず、コアシステムを介してアクセスする。
コアシステムとプラグインの間の通信は、通常は Point to Point(同期)である。 REST のように非同期にすることも可能だが、そうすると分散システムになるので前述のデメリットが発生する。
プラグインが大量にある場合は、発見&取得できるようにレジストリを作ることがある。
コントラクト(コアシステムとプラグインの間のデータ受け渡しに関する規約)は、全体で統一した標準を定めるのが一般的である。 もしサードパーティーのプラグインが適合せず管理権もない場合は、アダプタを作って適合させるのが一般的。
アーキテクチャ特性は以下の通り。
- デプロイ容易性、テスト容易性がやや高い - プラグイン単位でテストが可能だし、デプロイの危険性も下がるので
- モジュール性もやや高い - プラグイン単位で追加削除が用意
- パフォーマンスはやや高い - コアから余計なものを排除できることにより、レイヤードのように肥大化しないので
- シンプルさ、コスト、スケーラビリティ、耐障害性、弾力性はレイヤードとほぼ同じ
13. サービスベースアーキテクチャ
マイクロサービスアーキテクチャの一種。 他の分散アーキテクチャよりシンプルでコストが低いので人気である。 サービス単位で個別デプロイ可能なので、アジリティ・テスト容易性・デプロイ容易性・耐障害性・可用性が比較的高い。 量子は 1 以上にすることも可能ではあるものの、マイクロサービスアーキテクチャと比べるとリソース効率やコスト面で劣る。
- UI
- 単一 or サービスごと
- サービス層へのアクセスは、直接サービスの API にアクセスするか、前段に API レイヤー(プロキシなど)を挟む
- あるサービスのインスタンスが複数デプロイされる場合は、間になんらかの負荷分散機能が必要
- サービス層へのアクセスは REST が一般的だが、RPC や SOAP なども使える
- サービス
- ドメインに基づき荒い(広い)粒度で分割する
- 数は 4-12 個程度
- 荒い分割なのでデータの完全性や一貫性は高くなるが、変更により壊れるリスクはマイクロサービスアーキテクチャよりは高くなる
- サービスは自己完結し、絶対に相互の通信は行わず、原則として DB のテーブルも共有しない
- ほとんどの場合、トランザクションはドメインによって分割されるよね?という前提に立っている
- DB
- 通常はモノリシック
- BASE ではなく ACID を採用できる、つまり失敗したらロールバックすればいい
- ドメイン単位で論理分割しておくと、変更の影響範囲が小さくなってよい
- スケーラビリティが必要なときは物理分割も可能
分散システムの基礎知識
トランザクションの種類
- ACID
- Atomicity, Consistency, Isolation, Durability
- DB が単一な場合に採用可能
- BASE
- Basically Available, Soft state, Eventually consistent
- DB が複数の場合は必然的にこちらになる
複数サービスを組み合わせる場合に必要になるもの
- オーケストレーション
- 独立した指揮者がフローを制御・管理する
- コレオグラフィ
- サービスが自律的に協調してフローを制御・管理する
14. イベント駆動アーキテクチャ
ほとんどのアプリはユーザー等からのリクエスト(例えば過去 6 ヶ月に落札した商品一覧の要求)に反応することで動作する。 一方、イベント駆動アーキテクチャは発生したイベント(例えばオンラインオークションでの入札の発生)に反応することで動作する。
複雑で動的なユーザー処理を伴う、高いレベルの応答性をスケールを必要とする、柔軟なアクションベースの処理に向いている。例えば EC サイトの購入処理とか。
結果整合性のみを保証する。また、管理やテストは複雑になりがち。
ブローカー方式
- イベントが分岐や統合をしつつ伝播する
- 拡張を容易にするため、イベントプロセッサーは後段の処理の有無に関わらず、処理後には必ず新たなイベントを発生させる(自分が何をしたのか他者に伝える)のがベストプラクティスである
- 誰もイベントに興味がなくなるまで走り続けるリレーのようなイメージ
- イベントは「何がおこったか」を記したもの
- メリット
- スケーラブルで、パフォーマンスや耐障害性が高い
- デメリット
- イベントのコンテキストを知る手段に乏しく、ワークフローの制御が難しい
- 仲介者がいないので障害発生時やエラー処理が難しく、容易にデータ不整合が発生する
メディエーター方式
- ブローカー方式の欠点を解消するため、メディエーターというワークフローを制御をする人を置くパターン
- メディエーターが開始イベントを受け取り、必要なイベントプロセッサーにイベントを伝達する
- ブローカー方式と異なり、イベントプロセッサーは処理結果をメディエーターにだけ返却し、システムの残りの部分には何も知らせない
- メディエーターはドメイン単位で複数作成する
- イベントは「何をすべきか」を記したもの
- メリット
- ワークフローの制御が容易
- 例えばエラー発生時などにはワークフローを一時停止したり、後で再開したりできる
- ワークフローの制御が容易
- デメリット
- 処理の宣言的な記述が難しい
- パフォーマンスやスケーラビリティはブローカー方式より劣る
非同期の力
- Request/Reply 方式
- イベントのコンシューマーはレスポンスを返す
- 擬似同期通信という
- イベントのプロデューサーは(実のところ非同期ではあるものの)レスポンスを待つ
- パフォーマンスが悪いと応答性も悪くなる
- Fire and Forget 方式
- イベントのコンシューマーはレスポンスを返さない
- パフォーマンスが応答性に影響を与えないので、応答性が向上する
- エラー処理を難しくさせる
エラー処理(ワークフロー委譲)
エラー発生時の修復を行うために、ワークフロー委譲というパターンが使える。 イベントコンシューマーは、エラー発生時に即座にワークフロープロセッサーと呼ばれるイベント処理者に処理を委譲する。 ワークフロープロセッサーはエラーを解析し、修復済みのイベントを発行して処理を再実行するか、失敗の通知を管理者に送る。
データロスの防止
イベント駆動アーキテクチャではデータロスが発生しやすい。 例えば SQS なら VisibilityTimeout を使うなど、必要な対策を講じることが重要。
ブロードキャスト能力
ブロードキャストは、イベントを複数のコンシューマーに送信すること。 イベントプロセッサー間を疎結合にすることができる。 結果整合性や複合的なイベント処理を行うために不可欠の要素である。
15. スペースベースアーキテクチャ
- 高いスケーラビリティ、弾力性、同時実効性を実現するアーキテクチャ
- レプリケートされたインメモリデータグリッドにより、DB への直接の読み書きの必要性を排除する
- スケーラビリティはほぼ無限な一方、高価でありテスト容易性も低い
- 同時接続ユーザー数が急増するものや、同時使用ユーザーが 1 万人を超えるようなものに向いている
- e.g. チケット販売アプリやオークションアプリ
16. オーケストレーション駆動サービス思考アーキテクチャ
今となっては無用の長物
17. マイクロサービスアーキテクチャ
- DDD の考え方、特に「境界づけられたコンテキスト」に強い影響を受けている
- コードの再利用は DRY という意味で有効ではある一方で、結合をもたらす(これは継承またはコンポジションにより行われる)という名の負の側面もある
- このアーキテクチャでは、分離を優先するためあえて複製することが多い
- 各サービスは独立して動作するために必要なすべての部品を持つ
- サービスの粒度を慎重に決めることが成功の秘訣
- 極度に機能がまとまっていること、トランザクションがサービスをまたがないこと、データ分離性、呼び出しコストなどを考慮
- 細かく分けすぎるのはもっとも多い失敗(マイクロという単語に惑わされるな)
データ分離
広範囲から参照されるデータについて、モノリスにおける Source of Truth 的な選択肢をとることはマイクロサービスにおいてはもはや不可能である。 命令的に外部のサービスから値を取得するか、レプリケーションやキャッシュによりアーキテクチャ全体に分散するか、いずれかである。 自サービスの外にある値は常に結果整合としてしか取得できない点に注意せよ。 このことも、粒度を決める際の重要な判断材料になる(必要なものは 1 つのサービスにまとめろ)。
API 層
API 層にはプロキシ的な役割を持たせることが多い。
なお、メディエーター的な役割を持たせるのはアンチパターンである。なぜならマイクロサービスは、技術による分割ではなくドメインによる分割なのだから、
運用
ドメイン面に関するものは結合より複製が好ましい一方で、運用面に関することは結合が好ましい。 モニタリング、ロギング、サーキットブレーカー、サービスディスカバリーといった運用の機能は、サイドカーパターンを使ってサービスメッシュを構築し、アーキテクチャ全体に統一的に提供する。
フロントエンド
- モノリシック
- マイクロフロントエンド
- バックエンドと同じ粒度で分ける
- 各フロントエンドは UI コンポーネントを提供し、最終的にそれらを組み合わせて画面を構築する
通信
サービス間の通信方法を決める際、大枠として同期通信か非同期通信かをまず決める必要がある。
同期通信
同期通信を採用する場合、「プロトコルを意識した異種間相互運用性」を利用する。
- 「プロトコルを意識」他のサービスを呼び出すには使い方を知る必要があるから
- 「異種間」サービスが違えばプラットフォームも違うから
- 「相互運用性」サービスが互いに呼び合うから
非同期通信
非同期通信を採用する場合、イベントやメッセージを使い、内部的にはイベント駆動アーキテクチャを利用する。
コレオグラフィは中央のコーディネーターを持たない。 ブローカー型のイベント駆動アーキテクチャと似たスタイル。 サービス間の調整が必要な場合には、最初に呼び出されたサービスがその責務を担うフロントコントローラーパターンを使う。
オーケストレーションは中央のコーディネーターを持つ。 メディエーター型のイベント駆動アーキテクチャと似たスタイル。 サービス間の調整はオーケストレーターが管理する。
トランザクションとサーガ
サービス境界を超えたトランザクションを構築することは、値のコナーセンスという最悪の結合を生み出す。 サービス間のトランザクションを構築することは、可能ではあるが、マイクロサービスを選択した理由に相反する。 まずは、サービスの粒度が小さすぎないか、または間違っていないか、見直せ。 原則としてトランザクション境界でサービスを分けろ。
どうしても分散トランザクションが必要な場合はサーガパターンを使う。 メディエーターはリクエスト一旦保留し、複数サービスにリクエストを送信する。 もし一部が失敗した場合には前のリクエストを取り消すようすべてのサービスに指示する。 この調整を補償ランザクションという。
アーキテクチャ特性
デプロイ容易性、テスト容易性が高い。
サービス間通信を多用すれば一般的にはパフォーマンス、耐障害性、信頼性が下がるが、対処方法はある。
スケーラビリティ、弾力性、進化性は高い。
18. 適切なアーキテクチャスタイルを選ぶ
トレンドの変遷
選ぶべきアーキテクチャスタイルは以下の要因などにより変化し続ける。
- 過去の失敗や経験からの学び
- ドメインの変化 e.g. ビジネス方針の変更、他者との合併
- エコシステムの変化や新しい能力の登場 e.g. Kubernetes, Docker
- 外部要因 e.g. ライセンス料金の変更
判断基準
設計上の決定をするときに必要な知識や考慮事項
- ドメインへの一般的な理解
- (構造に影響を与えそうな) アーキテクチャ特性
- (構造に影響を与えそうな) データ構造
- 組織的な要因
- e.g. M&A の予定
- プロセス・チーム・運用
- e.g. うちでアジャイルできんのか?とか
- ドメインとアーキテクチャの同型性
- e.g. カスタマイズが必要ならマイクロカーネルがいいよね、とか
- e.g. 大規模&スケーラブルならモノリシックじゃダメだよね、とか
決めるべきこと
- モノリスか分散か
- システムの各部分に別のアーキテクチャ特性が必要かどうかで判断する
- (分散の場合) データをどこにおくべきか
- 処理の流れを考慮して決める
- イテレーティブに設計することを恐れるな
- (分散の場合) サービス間通信は同期か非同期か
- 原則として同期、必要に応じて非同期にすべし
コンポーネントの設計段階で量子の分析を行なっておくと、アーキテクチャスタイルの選択が容易になるよ。
19. アーキテクチャ決定
アンチパターン
アンチパターンとは、始めた時はいいアイディアのように見えるが、トラブルにつながるもの。または、ネガティブな結果を生む、繰り返されるプロセスのこと。
アーキテクチャ決定に関する代表的なアンチパターンは以下の通り。
- 資産防御アンチパターン
- 選択を誤ることを恐れて、決定を避けたり先延ばしすること
- 少なくとも、代替案がなくなる直前までに決定を下すこと
- 分析のしすぎで麻痺して動けなくならないように注意せよ
- Goundhog Day アンチパターン
- 下した決定についてあとから何度も議論が繰り返されること
- 技術的な理由だけでなく、ビジネス的な理由も提示することが大事
- ビジネス的な理由には、コスト、市場投入までの時間、ユーザー満足度、戦略的なポジショニングなどがある
- メール駆動アーキテクチャアンチパターン
- 下されたアーキテクチャ決定を見失ったり、忘れたり、そもそも知らなかったりする状況が発生すること
- 決定はメールや Slack の本文に書くのではなく、 ADR などのドキュメントに書いてそこへのリンクにするべき
アーキテクチャ決定
アーキテクチャ決定とは以下に影響を与える決定のこと。
- 構造
- サービスやコードの構造
- 非機能特性
- ility のこと
- 依存性
- コンポーネントやサービス間の依存関係
- インターフェース
- サービスやコンポーネントへのアクセス方法のこと
- e.g. API プロキシなど
- コントラクトの定義、そのバージョン管理戦略などを含む
- 構築手法
- プラットフォーム、フレームワーク、ツール、プロセスなど
ADR / Architecture Decision Record
ADR はアーキテクチャ決定を文書化するための手法である。AsciiDoc や Markdown で書かれた、1-2 ページからなる短いテキストファイルである。
ADR の保存については、非開発者を含めで誰でも閲覧できように、wiki でフォルダ分けして管理するのがおすすめ。
主な構成要素は以下の通り。
- タイトル
- 短く、簡潔に、曖昧さがない程度に説明的に
- 連番を振るとよい
- ステータス
- 提案済み or 承認済み or 破棄
- 破棄は、決定が変更されて別の ADR にとって変わった事を示す。新旧で相互にリンクを張る。強力。
- コスト、他チームへの影響、セキュリティを鑑み、承認レベルを上下させる
- コンテキスト
- どのような状況でこの決断を迫られているのか
- 具体的な状況や問題点を説明し、可能な代替案を簡潔に説明する
- 代替案を詳細に書く必要がある場合は別にセクションを設ける
- 決定
- 決定の内容とその理由
- How より Why を重視して書く
- 影響
- 全体的な影響を良いもの悪いものどちらも書く
- トレードオフ分析になる (コストや、他のプランにおける ility との差など)
- コンプライアンス
- 決定が守られているか評価したり徹底したりする方法(統制)
- 自動テストで監視できるといいね
- 備考
- 著者、承認者と承認日、最終更新日、変更履歴、置換日などがあると便利
20. アーキテクチャ上のリスクを分析する
リスク発生時の影響度をリスク発生の可能性と掛け合わせてリスクマトリックスを作り、リスクを点数化する。
次に、マトリックスを元にして、アーキテクチャ全体のリスクをまとめるリスクアセスメントという作業を行う。 サービスやドメインを横列に、スケーラビリティ・可用性・パフォーマンスといったリスク基準を縦列にとり、セルには点数を入れる。
リスクに関して関係者で共同作業を行うリスクストーミングも有効である。 ストーミングを行う際は、一つの ility に絞って順番に検討していくと良い。
これらの作業は開発初期に一度だけ行うのではなく、定期的に行うことが望ましい。
21. 図解とプレゼン
アーキテクトは図解とプレゼンのスキルを磨いておくことが大事。
作図に時間をかけすぎると「アーティファクトへの不合理な愛着」が発生しがち。 最初は手書きなり落書き帳なりでサクッと描こう。
UML はシーケンス図でよく使う。それ以外ではほぼ使わない。
22. 効果的なチームにする
コントロールの強度
アーキテクトは適切なサイズの箱を用意することが役目である。 小さすぎると開発者にとってウザいし、大きすぎると混乱を招く。 開発者が適切なツールやライブラリを使用して効率的に実装できるようにすることが重要。
- コントロールフリークアーキテクト
- 実装詳細まで口出ししちゃう、いわゆるマイクロマネジメント
- なりたての時にやりがち
- アームチェアアーキテクト
- 長らくコードを書いていない人
- 開発チームと離れていることが多い
以下の要素によって、コントロールの強度も上げる必要がある。
- チームメンバー間の親しさが薄いほど
- チームサイズが大きいほど
- 初心者が多いほど
- プロジェクトが複雑であるほど
- プロジェクトが長期であるほど
チームサイズの決め方
チームサイズは以下を考慮して決める。
プロセスロスは、チームの潜在能力と実際の出力の差分のこと。 例えば Git で頻繁にコンフリするなら、チームサイズが大きすぎるかもしれない。
多元的無知は、多数が心の中で思っていてかつそれが正しいにもかかわらず、臆して誰も発言しなくなること。 裸の王様的な。
責任の分散は、「誰かやるだろう」と皆が思ってボールの取りこぼしが発生すること。
ガイダンスの提供
チームにわかりやすくガイダンスを提供することはとても大事。 例えばライブラリ選定に関するガイダンスであれば、以下のような基準を作ることができるだろう。
- 特定・特別な用途に使用するライブラリは、開発者が選定・承認する
- 汎用的なライブラリは、開発者が選定しアーキテクトが承認する (技術上・ビジネス上の根拠があるか、機能が重複していないかなどを見るため)
- フレームワークは、アーキテクトが選定・承認する (アーキテクチャ特性に影響を与えるため)
23. 交渉とリーダーシップ
「成功のために必要なたった一つの成分は、人とうまくやっていく方法を知ること」
交渉
- 客との交渉
- 交渉の前にできるだけ多くの情報を収集する
- コストと時間を理由にするのは最後にしておけ
- 分割統治も有効な手段である
- 他のアーキテクトとの交渉
- 意見の相違があるときは、実証で説明する
- 議論のしすぎや、物事を個人的なものにしすぎない、時間をおくのも手
- 冷静なリーダーシップと簡潔な理由づけがあれば常に勝てる
- 開発者との交渉
- 上から目線はダメ
- 正当な根拠(理由)を示そう
- 同意が得られない場合は開発者自身に解決策を見つけてもらうのも手
リーダーシップ
「偶発的な複雑さ」を導入するのは無能なアーキテクトである。 明確で簡潔なコミュニケーションをとって協調することで、複雑さは回避できるし、尊敬されるようにもなる。
- Communication / コミュニケーション
- Collaboration / 協力
- Clarity / 明確さ
- Conciseness / 簡潔さ
アーキテクトは、プラグマティックかつビジョナリーであれ。
- プラグマティック
- 机上の空論ではなく、実践に基づいたやり方で、賢明かつ現実的に物事に対処すること
- 予算、時間、スキル、トレードオフ、制限事項などの考慮
- ビジョナリー
- 想像力や知恵をもって、未来を考えたり、計画したりすること
手本を示してチームをリードせよ。肩書きでは人は動かない。
24. キャリアパスを開く
20 分ルールとは、1 日最低 20 分は新しいトピックについて学ぶこと。
未知の未知
を既知の未知
に変える作業である。
朝イチでやるのがおすすめ。
パーソナルレーダーという、既存技術と新興技術のリスクとリターンを評価するための、常に更新され続ける生きたドキュメントを作るとよい。 年に 2 回くらい更新するのがおすすめ。 https://www.thoughtworks.com/radar/byor