キーポイント
- Microservices are most appropriate for stateful processes, not easily scaled web servers.
- Each microservice needs its own network credentials and a unique number, or application key, that people can easily recognize in logs and records.
- A common application framework is necessary for ensuring timely updates of dependencies.
- Centralized configuration is vital for reducing errors when settings change.
- Microservices that make synchronous calls to other microservices should be avoided when possible.
- If your database provides tools for building an abstraction layer, use them.
マイクロサービスはホットな新しいアーキテクチャパターンですが、「ホット」と「新しい」問題は、アーキテクチャパターンの実際のコストが明らかになるまでに何年もかかることです。幸い、パターンは新しいものではなく、名前だけです。したがって、これを10年以上行っている企業から学ぶことができます。
このケーススタディでは、マイクロサービスが金融ファームでどのように実装されたかを見ていきます。私が入社したとき、彼らはすでに5年から10年このパターンを使用しており、私の監督の下でさらに5年もこのパターンを使い続けました。
マイクロサービスは何でしょうか?
この道を歩み始めたときには「マイクロサービス」という用語は存在していなかったため、85以上のマイクロサービスを「プロセスアプリケーション」と呼んでいました。私たちのマイクロサービスの特徴は、それぞれが1つのアクティビティのみを実行することです。これらの活動には次のものが含まれます:
- 処理するファイルのネットワークドライブを監視。
- ビジネスパートナからのメッセージのTCPポートを監視 (あなたに株価を誰かがストリーミングしていると想像してください)。
- 他のサービスからのメッセージを標準化された形式に集約してから処理し、結果をデータベースとキャッシュにプッシュ。
- メッセージキューを監視 (当時はMSMQでしたが、現在はより優れた選択肢が利用できます)。
- 専用キャッシュのホスティング。
- HTTPSポートのホスティング (サーバとクライアントの証明書の処理を容易にするため、HTTP.sysを使用しました)。
- 処理するレコードのデータベーステーブルの監視 (通常はタイマでポーリングしますが、最近では代わりに SqlDependency を使用することを勧めます。Chain ORM にはこの機能が組み込まれています)。
- デスクトップアプリケーションからビジネスパートナに接続されているTCPポートにWCFメッセージを渡す (繰り返しになりますが、gRPC などの新しいテクノロジの方が適切な場合があります)。
私たちがしなかったことの1つは、Webサイトの各コントローラクラスを個別の成果物に分割することでした。その理由の一部は、MVCがまだ始まったばかりで、当時のほとんどのMSショップと同様、まだWebサイトの WebForms に重点を置いていたからです。ただし、後で説明するように、Webサイトを分割しない理由は他にもあります。
マイクロサービスとセキュリティ
私が制定したポリシーの1つは、すべてのマイクロサービスが独自のActive Directoryドメインアカウントを取得するという厳しい規則でした。つまり、各アプリケーションの構成ファイルには、内部リソースのユーザ名とパスワードは含まれませんでした。(パートナ企業が運営するWebサイトやFTPサーバなどの外部リソースで必要になることがありました)。
以前は、すべてのアプリケーションで共有される単一のログインを使用するという悪い習慣がありました。そして、今日の多くの企業がそうであるように、その1回のログインが実質的にデータベースの管理者でした。これにより、デプロイメントが容易になりましたが、大惨事となるリスクも非常に高くなりました。
各マイクロサービスには、独自のネットワーク資格が必要です。
この変更を行った理由は、データベースをロックダウンできるということでした。各アプリケーションは、実際に実行する必要のあるアクションしか実行できませんでした。これにより、ハッカーが価格設定サーバの脆弱性を利用してユーザテーブルをダウンロードまたは破壊するなどの悪い結果を防ぐことができました。
後に、かなり堅牢なドキュメントシステムを誤って構築したことがわかりました。個別のネットワーク認証ポリシーを制定する前に、コードを徹底的に検索して、変更がアプリケーションにどのような影響を与えるかを確認する必要がありました。これで、データベースにクエリを実行するだけで、アクセスできるテーブル、ビュー、およびストアドプロシージャの完全なリストを取得できます。これにより、データベースを変更するときに必要な回帰テストの量を大幅に削減しました。
各サービスの命名と番号付け
各サービスに固有の名前を付けることは重要ですが、混乱を解消するには名前だけでは不十分です。特に、非常によく似た名前のアプリケーションが多数ある場合です。
この問題を解決する方法は、各サービスに独自の「アプリケーションキー」を割り当てることでした。これは、構成ファイルやデータベース監査列などで使用できる短い番号識別子です。
各アプリケーションキーが1つのテーブルに登録されていたため、使用できるすべてのアプリケーション、マイクロサービス、ツールなどの完全なインベントリも取得しました。そして、自分たちが何のソフトウェアを構築したのか、どこで実行されているのかが分からないという状況に陥ることはありませんでした。
何も心配していないように見えるかもしれませんが、私はコンサルタントとして、稼働中のWebサイトやマイクロサービスの数を文字通り私に教えてくれないクライアントと一緒に仕事をすることがよくあります。中央リポジトリがないため、データベースにアクセスしているものを把握するためだけに、すべての開発者、ネットワーク管理者、および部門にインタビューする必要があります。
アプリケーションフレームワーク
マイクロサービスの道を進む場合は、独自の会社固有のアプリケーション フレームワークを開発することが重要です。そして、すべてのマイクロサービスでこのアプリケーションフレームワークを独断的に使用する必要があります。そうしないと、メンテナンスについていくことができません。
.NETの世界では、アプリケーションフレームワークは、次を定義する単一のNuGetパッケージで構成できます:
- 構成
- ロギング
- 依存性注入
- 低レベルデータベースアクセス
- ヘルスモニタ
- セキュリティコンポネント
- 共通ライブラリ
これらすべてのカスタム実装を作成する必要があると言っているわけではありません。実際、これらの要素のすべてではないにしても、ほとんどを処理する事前定義パッケージを使用することを勧めします。ただし、すべてをつなぎ合わせ、各ライブラリのどのバージョンを使用するかを定義するアプリケーションフレームワークが必要です。
開発者が陥りやすい落とし穴は、技術の変化に対応できるという信念です。そしておそらく、モノリシックアーキテクチャを使用する場合は可能です。しかし、次に Heartbleed インシデントが発生したとき、数十または数百のマイクロサービス上のすべてのライブラリのバージョン番号を確認する時間はありません。代わりに、アプリケーション フレームワークをライブラリの安全なバージョンに更新し、そのアプリケーションフレームワークの更新をすべてのマイクロサービスにプッシュする必要があります。
この最後のポイントは、アプリケーションフレームワークが「スターターキット」と異なる点です。スターターキットはどこから始めるかを定義しますが、アプリケーションはすぐにそこから分岐します。そして、これは、両手よりも多いサービスを管理している場合には受け入れられません。この会社で働いていたとき、私は個人的に50を超えるサービスの管理を担当していました。それぞれの設定が少しずつ異なっていて、それに触れるたびに再学習しなければならなかった場合、それを行うことはできませんでした。
アプリケーションフレームワークは、非常に必要な抽象化レイヤも提供します。たとえば、SmtpClient が問題を引き起こしていることがわかり、MailKit に切り替えたいとします。アプリケーションフレームワークは、SmtpClient API を模倣するアダプタを提供できますが、実際には MailKit を介して電子メールを送信します。これにより、すべてのマイクロサービスを別の依存関係に移植するために必要な労力が大幅に削減されます。
自己レポートサービス
サービスは、ステータスを自己レポートできる必要があります。外部監視は依然として重要ですが、自己レポートにより、外部監視ツールではアクセスできないレベルのインサイトが得られます。
私たちの場合、各サービスはタイマーでハートビートを起動しました。データベースが最も堅牢なリソースであったため、これはデータベースに直接記録されました。(これにはマルチサイトフェールオーバクラスタがあり、実際に、それがダウンした場合、ほとんどのサービスが役に立たなくなります。) Azure Application Insights などの成熟した製品では、おそらく私たちがしたように独自に展開したくないでしょう。
私たちのハートビートには、次の情報が含まれていました:
- アプリケーションキー
- アプリケーション名
- アプリケーションバージョン
- アプリケーションフレームワークバージョン
- ホストサーバ (または、バーチャルマシン)
- 現在日時
- 起動日時 (サービスを最後に再起動した日時)
別のプロセスはハートビートテーブルを監視し、チェックインしていないサービスを監視します。また、誰かがプロダクション環境の設定でサービスを非プロダクションホストに誤ってインストールしたことを検出することもできます。
そこから、サービス自体の健全性に関するサービスのオピニオンなどの追加情報に基づいて構築できます。たとえば、パートナ企業からのストリーミングメッセージを監視するサービスは、最後のメッセージから時間がかかりすぎると感じたときに通知することができます。または、ファイルプロセッサは、最後にインポートしたレコードの数と、欠陥が見つかったレコードの割合を報告できます。
これは、最初は一般的な「ステータス」列を使用して安価に実行できます。その後、必要に応じて専用のレポートテーブルにさらに洗練されたメトリクスが追加されました。
バーチャルマシン? Docker? コンテナ? サーバレス?
サービスをどのようにホストするかについては、ここでは触れません。これは、開発者が考える必要のない実装の詳細です。あなたの唯一の重要な質問は、「このサービスの複数のコピーを同時に実行できますか?」です。他のすべての決定は延期できます。
私が始めたとき、マイクロサービスの半分はデスクトップアプリケーションとして実行されていました。文字通りサーバにログインして起動する必要がありました。私が最初に雇われた理由は、アプリケーションを Windows サービスに変換した経験があったからです。
しかし、それらは Windows サービスである必要はありませんでした。同じコードを取得して、IISによって管理されるWebサーバにドロップすることもできました。または、Dockerコンテナにパックすることもできました。それらのいくつかについては、サーバレスホストでも同様にうまく機能していたでしょう。サービスとそのホスト間の対話がアプリケーションフレームワークによって抽象化されている限り、ここで十分な柔軟性が得られます。
一元化された構成
何年にもわたって、構成に対処するためのさまざまな方法を試してきました。手動構成、ビルド スクリプト、さらには構成をソース管理に直接保存することも試みました。最終的に、すべての方法は同じ理由で失敗しました。それらはスケーラブルではありませんでした。マイクロサービスの数が増えると、共有設定の変更にかかる時間が直線的に増加し、間違いを犯す可能性が幾何学的に増加します。
最終的に有効だったソリューションは、一元化された構成でした。アプリケーションは「Environment」という名前の単一の構成値を使用して構築されました。これは、環境と一致するデータベースサーバー名のリストを含むファイル共有を指していました。例:「プロダクション:MainDB.company.net」。
構成データベースには、各アプリケーションに必要なすべての設定が保持されていました。しかし、それだけではありません。重要なアラートを送信するなどの一般的な設定が最初に定義されました。その後、各アプリケーションには、共通の設定を上書きするか、アプリケーション固有の設定を導入するオプションがありました。さらに、マイクロサービスの特定のインスタンスが設定をさらに上書きする可能性があります。
これは、何か一般的なものを変更する必要がある場合、1つのエントリを変更するだけで、すべてのサービスが新しい設定をロードすることを意味していました。(一部のマイクロサービスは長時間実行されるデータ処理ジョブで動作するため、これが発生したのはアプリケーション固有でした。)
これは完全な解決策ではありませんでした。マイクロサービスの非プロダクションインスタンスがプロダクション環境を指す可能性は依然としてありました。プロダクションサービスと非プロダクションサービスに別々のADアカウントが本当に必要でしたが、それは当時の私たちの組織能力を超えていました。
そして実際、環境/データベースサーバ名を保持するファイルサーバはばかげていました。データベースサーバを移動する必要がある場合、それだけで、データベース名を構成ファイルに直接入力し DNS に依存する必要がありました。
コマンドと制御
私が非常に誇りに思っている側面の1つは、コマンドと制御です。各マイクロサービスは、社内の監視ツールが接続できるWCFポートを公開しました。WCFを使用したのは、双方向メッセージングがサポートされており、ログデータのライブストリーミングが可能だったからです。
しかし、ログは重要な機能ではなく、多くのツールでそれを行うことができます。コマンドと制御ポートを使用すると、ログに報告されていない診断メトリクスを表示することもできました。たとえば、現在メモリに保持されている処理待ちのアイテムの数です。サービスによって実際に使用されている構成設定も確認できましたが、これらのサービスは数か月に1回しか再起動されないため、構成ファイルの設定とは異なる場合があります。
以前、マイクロサービスはタイマーまたはファイルシステムウォッチャによってトリガされることが多いと述べました。コマンドと制御ツールにより、サービスに「今すぐ実行」または「ファイル X を再処理」を伝えることができました。これは、実行頻度が低く、1時間に1回だけ、または1日に1回しか実行されないプロセスのトラブルシューティングを行う場合に特に重要でした。プロダクションデータの一部を修正した後、テストを再実行するのに50分もかかることはありません。そしてデータと言えば...
データベースアクセス
前述のように、各マイクロサービスには独自のネットワーク資格がありました。これらは、サービスにアクセスできるストアドプロシージャを決定するためにデータベースによって使用されました。「ストアドプロシージャ」という言葉を強調したいと思います。テーブルに直接アクセスできるサービスはありませんでした。
ORMの時代にはこれは奇妙に思えるかもしれませんが、NHibernateやEntity FrameworkなどのORMには結合に問題があります。それらは、データベースへの完全かつ自由なアクセス権を持ち、オブジェクトモデルをテーブルスキーマに緊密にバインドすることを期待しています。
これは、データベースにアクセスするアプリケーションが1つある場合には機能しますが、100どころか1ダースのアプリケーションがある場合にも機能しません。非常に多くのアプリケーションがあるため、データベースを過度に密結合から保護し、サービスを破壊的な変更から保護する必要がありました。
ストアドプロシージャは、カプセル化層を作成することでこれを行います。ストアドプロシージャ自体がAPIを形成し、テーブルはその非表示の実装の詳細にすぎません。テーブルへの変更が必要な場合、ストアドプロシージャは後方互換性のある方法で更新されるか、バージョン番号がインクリメントされ、古いバージョンと新しいバージョンを一時的に並べて実行できます。
また、各ストアドプロシージャには、アクセス許可を持つマイクロサービスのリストが付随していたため、ストアドプロシージャが変更されるたびに実行される回帰テストの既製のリストがありました。
これをシミュレートするには、実際のサービスとデータベースの間で一連のRESTサーバを配置します。ただし、これはシミュレーションに過ぎず、完全にカプセル化されたデータベースのパフォーマンスと分離のメリットをすべて享受することはできません。次のトピックに進みます。
Webサーバとマイクロサービス
以前、マイクロサービスアプローチをWebサーバに適用しなかったと述べました。その理由は、単純に、そこに利益がゼロで、コストがかかると考えたからです。使用したロジックのチェーンは次のとおりです:
容易なデプロイメント
マイクロサービスはデプロイを容易にするはずです。これは、デプロイメントが難しいことを前提としています。では、なぜデプロイメントが難しいのでしょうか?
ステートフルサービスである「プロセスアプリケーション」を安全にシャットダウンするには時間がかかるため、デプロイメントは困難です。従業員を混乱させないように適切なタイミングで行う必要があり、多くの場合、2つのジョブを処理する間の「アイドル」期間中に行う必要があります。
Webサーバはステートフルではありません。新しいバージョンをデプロイすると、IISは残りの要求を適切に処理し、新しい要求を新しくロードされたバージョンに転送します。つまり、デプロイメントはいつでも行うことができますが、追加のテストを可能にするために、業務時間後に行うことを好みます。
信頼性とスケーラビリティ
マイクロサービスは、信頼性とスケーラビリティを向上させることも意図しています。プロセス A が失敗したときにプロセス B から Z をクラッシュさせたくないため、これは非常に理にかなっています。また、プロセス C は単一のインスタンスに制限される場合がありますが、プロセス D と E はスケールアウトを許可する場合があります。したがって、これらの各プロセスを個別のサービスに分割することは非常に理にかなっています。
しかし、ステートレス Web サーバーは完全にスケーラブルです。十分なラックスペースがある限り、Webサーバを追加し続けることができます。最終的には、データベースやネットワークの容量などの他の制限が影響しますが、Webサーバ自体には制限がありません。
マイクロサービスのコスト
メリットがないとして、どんな害があるのでしょうか?
Webサーバが他のWebサーバを呼び出さなければならない場合、それは余分な遅延と、対処しなければならない障害点が増えることになります。チェーン内のリンク切れがプロセス全体を混乱させる可能性があるため、パフォーマンスと信頼性は低下せざるを得ません。
これは、マイクロWebサーバを試したときの最も痛かった問題ではありません。本当の問題は、依存関係の管理とデプロイメントでした。UI で何かを変更するたびに、常にバックエンドデータベースの呼び出しを一致させる必要がありました。ただし、UI とバックエンドが別々のサーバにあるため、両方を同時に変更してデプロイする必要がありました。UI Webサーバは、すべてのバックエンドWebサーバに強く結合されていました。
もう1つの問題は、構成、つまり複数のソースからのデータの結合でした。最終的にはすべてが同じデータベースから取得されましたが、顧客、クライアント、在庫、トランザクションなどのために別のバックエンドWebサーバを使用していたため、特定のデータを公開するWebサーバを決定するのは本当に悪夢でした。常に、私たちが選んだものはすべて「間違い」であり、どこかに移動するか、他のサービスからのデータと組み合わせる必要があります。
そのため、最終的に実験を中止し、Webサーバがデータベースに直接アクセスできるようにしました。
データのオーナシップと変更ログ
データのトピックに戻ると、マイクロサービスに関して最もよく寄せられる質問の1つは、「このレコードを変更したサービスはどれですか?」です。常に、これは、機能するシステムの複雑なWebを理解しようとする無数の開発者から、1日に何十回も尋ねられます。
「データベースの前面でマイクロサービスを使用しているので、そのマイクロサービスだけがそれを変更できたのではないか」と考えているかもしれません。マイクロサービスがそれ自体でデータを変更することを決定しなかった場合、または他のサービスからのリクエストに応答しているだけの場合、それは本当の答えではありません。
この問題を解決する方法は、「アプリケーションキー」の概念によるものです。データベースが更新されるたびに、実際に変更を行った、変更を開始したユーザと、Webサーバまたはマイクロサービスのアプリケーションキーの両方が記録されました。
もちろん、複数のマイクロサービスが同じデータに矛盾する変更を加える可能性は依然としてあります。したがって、データオーナシップの概念は依然として重要です。理論上は、どのマイクロサービスでも任意のフィールドを読み取ることができますが、可能な限り、1つのマイクロサービスのみが特定のフィールドに書き込むことができるようにシステムを設計しました。成功した場合、これにより、設計の変更についての推論がはるかに簡単になりました。失敗し、複数のサービスが同じフィールドに書き込みを行った場合、少なくともストアドプロシージャのリストを次のステップを計画するための開始点として使用できます。
疎結合とメッセージキュー
マイクロサービスで大いに宣伝されている利点は、システムの一部を「疎結合」にすることです。場合によっては、これが真実であることがわかりましたが、茶番劇だった場合もあります。その理由を理解するには、まず「疎結合」という用語を定義する必要があります。
私が使用する作業定義は、「B の障害が A の障害を引き起こさない場合、コンポーネント A はコンポーネント B に結合されない」です。これが唯一の有効な定義ではありませんが、システムの堅牢性を見積もるという点で、これが最も役立つと思います。
たとえば、パートナ企業からさまざまなフィードがありました。各フィードは独自のマイクロサービスであるため、1つのフィードが失敗しても、他のフィードが停止することはありません。各フィードは、見積もりサーバと価格サーバの2つのシステムにも書き込みを行いました。見積もりサーバは基本的に、フィードからの生データを保持するキャッシュでした。高速でしたが、使用が制限されました。オファリングサーバは、オファリングをデータベースに投稿する前に、必要な複雑な債券価格計算を通じて生のクオートを実行しました。データベースはWebサーバによって使用され、クオートサーバはほぼリアルタイムの端末でトレーダによって使用されます。
もともと、見積もりサーバまたはオファリングサーバに障害が発生すると、それを使用するすべてのフィードが停止していました。同様に、サーバを圧倒した1つのエラーフィードが、他のフィードを実質的に破壊する可能性があります。したがって、上記の定義により、各フィードはサーバに結合されているだけでなく、各フィードも他のすべてのフィードに結合されていました。
予想よりも頻繁に沼が発生したことに注意してください。各株式が独立している株式市場とは異なり、債券はしばしばお互いの価格または利回りでペッグされます。たとえば、米国債の価格が変動すると、数万の社債や地方債の価格も同様に変動する可能性があります。
見積もりサーバの問題を解決するために、メッセージを一方向に変更し、エラーが発生した場合、フィードはメッセージを単に破棄します。これは無謀に思えるかもしれませんが、クオートは急速に変化するため、1つが欠けていても、すぐに次のクオートに置き換えられるため、それほど大きな問題ではありません。さらに、見積もりサーバは非常に単純なソフトウェアであり、ハードウェア障害以外の何ものでもそれをダウンさせることはありませんでした。
はるかに複雑なサーバを提供するには、より堅牢なソリューションが必要でした。データを自分のペースで読み取る方法と、データを圧倒する可能性のあるフィードからの保護が必要でした。これを解決するために、MS Message Queues に目を向けました。現在、間違いなくより優れたメッセージキュー製品が利用可能ですが、当時、フィードと提供サーバ間の密結合を解消するには、MSMQ で十分ではありませんでした。
Webサーバの場合、この分離は非常に難しくなります。ユーザが情報を待っている場合、非同期メッセージをキューに送信して、不確定な時間に処理することはできません。場合によっては、要求された情報がなくてもWebサーバが処理を続行できます。たとえば、YouTube や Netflix がウォッチリストを表示しないことがあります。しかし、私たちの場合、「オプション」のデータはなく、マイクロサービスで障害が発生すると、Webサイトも同様に障害が発生します。
バージョン管理の考慮事項
大多数のマイクロサービスは互いに完全に独立しており、メッセージキューまたはデータベース自体にのみ依存しています。この場合、メッセージとストアドプロシージャの下位互換性を確保することで、バージョン管理の問題が軽減されました。安全でない方法で変更された場合、オリジナルを変更する代わりに、新しいメッセージキューまたはストアドプロシージャが作成されます。
相互に直接通信する少数のマイクロサービスについては、より包括的な計画が必要でした。各サービスは、バージョン番号と環境を提供するエンドポイントを公開しました。マイクロサービスが起動すると、依存している他のすべてのサービスのバージョン番号がチェックされます。チェックが失敗した場合、開始を拒否します。
結論
マイクロサービスは、簡単にスケーリングできるWebサーバではなく、ステートフルプロセスに最も適しています。コンポーネントを完全に、またはメッセージキューを介して分離できる場合に使用します。同期サービス呼び出しのチェーンを作成するだけの場合は、それらの使用を避けてください。そして最も重要なことは、マイクロサービスのインベントリをカタログ化する方法を設計して、マイクロサービスがあなたから遠ざけないようにすることです。
著者について
Jonathan Allen氏は、90年代後半にヘルスクリニックの MIS プロジェクトに取り組み始め、それらを Access と Excel からエンタープライズソリューションへと段階的に引き上げました。金融セクター向けの自動取引システムを5年間作成した後、ロボット倉庫の UI、がん研究ソフトウェアの中間層、大手不動産保険会社のビッグデータのニーズなど、さまざまなプロジェクトのコンサルタントになりました。余暇には、16世紀の武道の勉強と執筆を楽しんでいます。