lasciva blog

開発して得た知見やwebビジネスのストック

「マイクロサービスアーキテクチャ(Building Microservices)」を読んだ

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

目的、モチベーション

  • マイクロサービスのサービスに携わり始め、基礎を学びたかった
  • 英語のお勉強

全体の感想

モノリスとマイクロサービスの比較から始まり、どのようにマイクロサービスに移行していくか、設計や実際の運用する上での話などが一通り網羅されていて、全体感を掴めました。
一方で、個別の細かいレベルでのツールや技術の選定や導入、運用方法は別途学ぶ必要があると思います。
マイクロサービスの導入によって生まれる課題も想像以上にあるので、安易に流行ってるというだけで導入するのは危ないなと思いました。

マイクロサービスやってるバックエンドエンジニアには前提となる知識が網羅されていて、必読な感じの本に思えました。
マイクロサービスの経験のないチームで導入の検討をしているときは特に参考になると思います。

今回は英語版を読んだのですが、いつもの5倍前後ぐらい時間がかかった気がします。
効率がかなり悪かったので、今後は暇な時期か日本語版がないときだけにしようかと思います。

目次

概要、気になったところ

前提として、私はマイクロサービス歴は数ヶ月で、モノリスでの経験も中規模程度で、モノリスでかなり苦しんだ経験もないです。
その中で、気になったところを中心にまとめました。

1章 マイクロサービス

1.1 マイクロサービスとは
  • 単一の役割の責任を満たすぐらい小さいこと
    • 十分小さい状態とは、大き過ぎない状態のこと
    • 小さくし過ぎると、恩恵が最大化される一方でデメリットも最大化される
  • 自治的であること
    • 他のサービスを意識せずに独立してデプロイ等を行えるか
1.2 主な利点
  • 適材適所な技術を使える
    • 試験的な導入も影響範囲を狭めて試す
  • resilenceでエラーを局所化できる
  • 必要な部分だけスケールさせられる
  • デプロイが速く、安全に行える
  • 少人数のチームで開発効率を維持できる
  • リプレイスが容易
1.4 他の分解テクニック

共有ライブラリ

  • どのプラットフォームでも動くようにしないといけないので、言語をサービス間で同じにしないと辛い面もある
  • ライブラリの更新によって、デプロイの独立性が阻害されないように工夫が必要
  • ライブラリの種類によってはサービス間が密結合になってしまうかもしれない
1.5 銀の弾丸などない

マイクロサービスも良いところばかりでないので、考慮した上で導入すること。

2章 進化的アーキテクト

2.1 不正確な比較

よく建設の設計者に例えられるが、ソフトウェアエンジニアは歴史が浅く、何か明確になるような比較対象を求めがちだが、役割に誤解を招いて幅を狭めるのでよくない。

2.2 進化するアーキテクト像
  • もし例えるなら、市の設計者が適切である。
  • 市は外部の影響を受けるし、長期的な時間を見越してみんながハッピーになれるよう考える必要がある。
  • 地域や、インフラ整備なども似ている。
2.3 区画指定
  • zone間でのやり取りに気を配るべき。
  • zone内での技術等はzoneに任せるべき。
    • ただし、zone毎に最適化しすぎて様々な技術を使いすぎると、採用や人の異動を行いにくくなる
2.4 原則に基づいたアプローチ
  • ビジネス的な戦略を把握する
    • 考える形では、アーキテクトは関わらないことが多い。
  • 原則
    • 戦術を遂行するためのもの
  • 実践
    • 原則を実現するための方法
2.10 チームの構築

偉大なソフトウエアは偉大な人から生まれるので、人を育てるのは重要。マイクロサービスはモノリスよりもサービスのライフサイクルや成長を自主的に把握しやすいので、相性がいい。技術だけしか興味を持ててないのは、成長の半分にしか貢献できてない。

3章 サービスのモデル化方法

3.2 優れたサービスにするには

各サービスが他のサービスとの調整なしに、独立してデプロイでき、影響範囲を局所化するために、疎結合と高凝集性が重要。

3.3 境界づけられたコンテキスト

DDDなどを参考に、ドメインを切り分けていく。
一気にサービスを独立させるのではなく、まずはモノリスでモジュールレベルで分解して検討していく。
誤ってサービスを切り分けると開発コストがよりかかるため、モノリスで十分様子を見てからサービスを独立させる。

4章 統合

4.1 理想的な統合技術の探索

下記を満たすべき。

  • 破壊的変化を避ける
  • 特定の技術に限定されない、変更可能
  • 他サービスからシンプルに使えるように
  • 詳細の実装を隠蔽
4.3 共有データベース

以下の理由からアンチパターン

  • スキーマ変更するだけで、大きな影響を与える
  • サービス毎に適切な技術選定が行えない
  • 変更箇所が各サービスに渡る
4.5 オーケストレーションとコレオグラフィ
  • オーケストレーションだとシンプルだが、それぞれのレスポンスを束ねる神サービスができて、密結合になりがち
  • もう一方では、それぞれの処理を各サービスに委譲できて疎結合サービスになるが、全体で適切に処理が行われたか監視しないといけない。
4.13 バージョニング
  • 原則として、破壊的な変更はなるべく行わないこと。
  • Tolerant Readerを使って変更に強くする。
  • protobufでネームスペースを使うと、変更がないバージョン変更が辛いので、オススメしない。
4.15 サードパーティソフトウェアとの統合
  • カスタマイズ
    • ベンダーの提供するカスタマイズを行うには、スクラッチで作るよりもコストがかかりうる。
    • ベンダー側に合わさせるのではなく、ベンダーに合わせる。
    • アップグレードで動かなくなることも多々ある。
  • CMSの例
    • カスタマイズしようとして、秘伝のCSSなどができがち。
    • コンテンツの取得、更新をAPI経由で行い、フロントエンドは自前で対応するとよい。
  • CRMの例
    • CRMは社内の組織に大きく影響を及ぼし、後々に肥大化する。
    • CRMのサービスは何でも対応できて、巨大で一つに集約されてしまう。
    • facadeパターンで、ドメインの関心ごとにサービスをつくり、外部からはシンプルなインターフェースで使えるようにする。
  • レガシーなサービスとの共存
    • リプレイスや機能削除を行おうにも、どこが使われてるか不明で、進めにくい。
    • 他のパターンと同様に中間のサービスを用意して、直接参照しないようにする。
    • 徐々にリプレイスもでき、ビッグバンリリースを避けれる。

5章 モノリスの分割

全体としては以下の順に進める。

  1. コードの境界を定めて、パッケージなどで分割していく
  2. 分割できたら、一つ一つ徐々にサービス毎に切り出す
5.3 モノリスを分割する理由
  • 変更できるスピードが上がる
  • チームごとに責任を持って進めやすくなる
    • 例えば、ロンドンとハワイにチームが開発者が別れてるケースなど
  • セキュリティを個別に強化するなど、個別に必要な機能追加を行いやすい
  • サービスごとに適切な言語選定や新しい技術の導入が行いやすい
5.5 データベース
  • まず、DBへの参照や更新をそれぞれの境界ごとにリポジトリレイヤーに切り出す
  • 境界間で外部キーなどの依存関係がある場合は、SchemaSpyで依存関係を可視化する
5.7 例:外部キー関係の削除
  • DBは別々にして、API経由で参照する。
  • パフォーマンスが低下するかもしれないが、トレードオフなので、どれだけパフォーマンスが求められるか考慮する。
  • 外部キー制約が失われるが、別のサービスで監視するなどの工夫が必要。また、データを本当に削除してもよいかなどの挙動の取り決めも必要。
5.9 例:共有データ

別々のサービスが同じデータを参照する場合は、そのデータのサービスを別途切り分ける。

5.10 例:共有テーブル

列の多いテーブルはそれぞれのサービス毎に縦に分割する。

5.11 データベースリファクタリング

他の例はデータベース・リファクタリングを読むべし。

5.11.1 段階的な分割

段階的に移行すべし。

  • 初めにDBの分割を行ってから、アプリケーション側の分割を行うことで、リバートできるようにしておく
  • ただし、DB間のトランザクションを扱ったりする必要がある
5.12 トランザクション境界

DBが分割されると、整合性を保つのに工夫が必要。

  • あとでリトライして、結果整合性を担保する
  • ロールバックを実現するために、delete文を発行する
  • 分散トランザクション
    • 上述の方法だと、複雑なものに対応できなくなる。
    • 2段階コミット
      • 1段階目でそれぞれのDBでコミットできるか確認する
      • 2段階目で全てokなら実際に各DBで実行し、NGなら実行しない
      • 1つでも障害が起こると死んでしまうなどの欠点がある。
  • 複雑なので、本当に整合性を守らないといけないのかの検討をする
5.13 レポート
  • モノリスのDBを直接参照するのはデメリットがある
    • スキーマの変更に影響される
    • 本番環境のパフォーマンスに影響を及ぼしうる
    • 適切な技術選定ができない
  • マイクロサービスのAPIコールで実現するには工夫が必要
    • バッチ用のAPIを用意して、ポーリングし続けてCSV等の形式で共有データに出力されたものを参照するなどの方法がある
    • アプリケーションレイヤーで処理するには、データ量が多く危険な上、処理中に整合性が保てなくなる
  • 各サービスがデータをpushして一箇所で管理する方法の方がベター
    • 疎結合ではなくなるが、反するメリットの方が大きい
5.21 根本原因の理解
  • まず分割の重要性を理解した上で開発すること
  • 対応困難なレベルになる前に分割すること
  • 徐々に進めていけるものなので、恐れないこと

6章 デプロイ

サービス毎にリポジトリを分割すべき。

  • CIでのテストの短縮や、デプロイの時間短縮につながる
  • インフラは出来れば1サービス毎に1ホスト用意すべき
    • 複数のサービスが同一ホストにあると、依存関係ができる
    • 1サービスがメモリを喰ったりすると、他のサービスも道連れになってしまう
    • 監視が困難
  • 1ホストに対して1サービス提供するために、dockerなどの軽量なコンテナを使うべき
    • VMはオーバーヘッドが大きく、サービスの数が増えるとスケールしにくい
    • 各サービスに必要なミドルウェアなどはバージョン管理等のことを考えるとイメージとして保存しておくべきだが、容量が大きくなってしまう
  • コンテナ間の制御はk8sなどを使う

7章 テスト

テストは分類でき、何を目的にしてるのかを明確にする

ユニットテスト

  • 関数単位とかのレベル
  • 実行時間は短いので、どんどん書くべき

サービステスト

  • stubやmockを活用

E2Eテスト

  • 全体でちゃんと動くか保証できるので安心感がある
  • マイクロサービスでは実現するのにコストがかかる
  • 複数のサービスにまたがると、全部ビルドしないといけないので、実行時間がかなりかかる
  • どのサービスでE2Eのテストをカバーするのかの取り決めを行わないと、サービス間でテスト内容が重複したりする
  • 後述のCDT(Consumer Driven Test)を増やして、必要最低限に数を減らすこと
7.8 救いとなるコンシューマ駆動テスト
  • サービス間で期待するインターフェースを取り決めておき、それに従うようにテストを行う
  • PactPactoなどでは、JSONなどでインターフェースを定義しておけて、モックも提供してくれる
    • JSONなどの形式だと、サービスの言語に依存しないので嬉しいケースが多い
7.10 本番リリース後のテスト
  • デプロイとリリースのタイミングを分離することで、内部でテストを行うことができる
    • ブルーグリーンデプロイなどのこと
    • テストを行ってから、新しいサービスにリクエストを投げ始める
    • 古いサービスは少しの間は残しておいて、エラーが発生したら切り戻せるようにしておく
  • canary releasing
    • 新旧のサービスを共存させて、徐々に新サービスへのリクエストの割合を増やしていく
    • 新旧でレスポンスを計測して、パフォーマンスに問題がないかなどをウォッチできる

8章 監視

  • サービス間で共通の形式でログを出力すること
  • ログは一つのサーバに集約させて調査しやすいようにすること
  • 外部からのリクエストはどのサービスでも同一だと認識できるように共通IDをログに含めることで、調査できるようにすること

9章 セキュリティ

9.2 サービス間の認証と認可

大抵のパターンでは、ユーザ名・パスワードをセキュアに管理する必要があり、そこが厳密には難しい。
また、サービス間をHTTPSで通信を行う場合には、それぞれのサーバのSSL証明書を用意する必要がある。

9.2.1 境界内のすべてを許可する

一旦ネットワーク内に侵入されてしまうと、中間者攻撃を防げないので、あまりよろしくない。

9.2.2 HTTP(S)ベーシック認証

HTTPの場合は、ベーシック認証の情報がセキュアでないため、基本的にはHTTPSで行うべき。

9.4 徹底的な防御
  • ファイアウォール
  • ロギング
    • 攻撃されたあとの調査の材料になる
    • セキュアな情報は残さないこと
  • 侵入検知(および侵入防止)システム
    • 侵入者の検知のみならず、ファイアウォールとは異なり、内部での不審な動きも検知することができる
  • ネットワーク分離
    • AWSVPCのようなサービスで分離すること
  • OS
    • アプリケーションレイヤーが完璧に対策されていても、OSが古ければ脆弱性がある状態になるので、最新の状態に更新し続けること
9.6 節約する

不必要なデータを削除することは、データを盗まれる可能性を減らす上に節約につながる。

10章 コンウェイの法則とシステム設計

コンウェイの法則の事例等が紹介されていた。

11章 大規模なマイクロサービス

マイクロサービスで、扱うサーバやノードが増えれば増えるほど、問題の起こってる状態は増える。
GoogleNetflixでは、自らエラーを発生させてシステムが動き続けるかどうかの確認を行なっている。

11.5 アンチフラジャイルな組織

ハードウェアやネットワークに問題が起こる前提で、反脆さに強い分散システムをつくる必要がある。

  • タイムアウトを適切に設定すること
  • サーキットブレーカー
    • タイムアウトや5xxエラーが増えた時に、サービス間の通信を遮断して即座にエラーを返すようにする仕組み。
    • 遮断している間に、少しだけリクエストを送って回復したかを検知して、回復したは復旧する。
11.8 データベースのスケーリング
  • Read: replicaを増やす
    • ただし、結果整合性を許容することになる
  • Write: シャード
    • 集合を扱うとき、シャード毎の結果を結合しないといけない
    • シャードを増やすとき、リバランスを行わないといけない
  • Shared Database Infrastructure
    • 便利な反面、SPOFになりうるリスクがある
  • CQRS(Command-Query Responsibility Segregation)
    • TODO
11.10 オートスケーリング

トラフィックの増減に応じてインスタンス数をコントロールするだけでなく、最低値を設定して自動で復旧させるのも便利

11.11 CAP定理

「一貫性(Consistency)」「可用性(Availability)」「分断耐性(Partition-tolerance)」の3つを同時に完璧に満たすのは不可能だという定理。
分散システムにおいては、分断耐性を犠牲にすることはできない。そのため、通信の遅延が起こった際に、システムの要件に応じて一貫性と可用性のトレードオフのバランスを取ることが求められる。

11.13 動的サービスレジストリ

下記のサービスが紹介されていた。

  • ZooKeeper: 本番運用もされてることが多く信用性は高いが、ヘビーで理解が難しめ
  • Consul: 本番運用例はあまりないが、有名なコミュニティが開発してるらしく、一見の価値あり
  • Eureka: Netflixによるもので、上記の2つよりは用途を限定している

12章 まとめ

12.2 マイクロサービスを使用すべきでない場合
  • 境界を適切に区切るのに十分なドメイン知識がないとき
    • 間違って境界を設定すると、よりコストがかかる
  • サービスの数が増えれば増えるほど、マイクロサービス特有の監視などの課題は重くなっていくので、徐々に進めていくべき
12.3 最後に

マイクロサービスにすることは、決断ではなく旅のように長くコツコツやっていくつもりで対応すること

次のアクション

概要はわかった気にはなれたので、サービス運用しながら学んでいこうと思います。
サービスの切り分けや、サービス間のデータの整合性の保ち方などは特にもう少し深堀りたいなと思います。
マイクロサービスの課題も少しは掴めたので、k8sで何を解決しているのかをもう少し理解したいと思います。

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ