お疲れ様です。MatsuribaTechのアドベントカレンダー7日目です。
久しぶりの記事投稿となります。
気づけばもう12月...月日の流れを早く感じてます(20歳を過ぎてからが特に早い...)
はじめに
最近、マイクロサービスアーキテクチャについて色々とキャッチアップしているので、備忘録としてここに書き残しておきます。
私が理解している範囲で、導入メリデメやサービスの切り出し区分などについて軽くまとめる程度にします。
前提知識:APIやコンテナ、BFFについて知っている
到達レベル:マイクロサービスアーキテクチャについてかじれた気分になる
マイクロサービス・アーキテクチャとは
画像引用元:https://aws.amazon.com/jp/microservices/
マイクロサービス・アーキテクチャ(以下、マイクロサービス)とは、複数の独立した小規模なサービス群から構成される開発手法を指します。
従来のアプリケーション開発やシステム開発において主流だったモノリシック・アーキテクチャは、全ての機能をモノリス(一枚岩)としてまとめて開発していました。一方で、マイクロサービスは、機能ごとに小さなサービスを作り、それらを組み合わせて一つのアプリケーションを開発する手法です。
構成されている個々の小規模なサービス(コンポーネント)もまたマイクロサービスと呼ばれています。マイクロサービス間の通信は、APIやメッセージブローカーと呼ばれる中間ソフトウェアなどを介して行われます。
マイクロサービスの最大の特徴は、それぞれのマイクロサービスが互いに独立しており、それぞれの依存性を低く保ちやすい(凝集性・疎結合性を保ちやすい)点です。これにより、システムやアプリを構成する上で多くのメリットを生み出しています。
詳細は、マイクロサービスが広く認知されるきっかけとなった以下のブログを参照してください。
マイクロサービスのメリットとデメリット
マイクロサービスアーキテクチャは、その特性から多くのメリットを提供しますが、同時にいくつかのデメリットも存在します。
メリット
-
異なる技術の採用:
各マイクロサービスは独立しているため、それぞれのサービスで最適な技術スタックを選択することが可能です。例えば、機械学習やビッグデータ分析を扱うようなサービスではPythonを採用して、webフロントエンドではTypeScriptを、高パフォーマンスが求められるサービスではGoやRustを採用するなど、各サービスの要件に合わせて最適な技術を選択できます。これにより、技術選択の自由度が高まり、各サービスの品質向上や開発効率の向上が図れます。 -
柔軟性・拡張性:
各サービスはgRPCなどの軽量なメッセージングにより疎結合で連携することが推奨されています。そのため、一部の機能に変更を加えたい場合でも、その影響は該当部分のみに留まります。これにより、システムが大きくなっても機能の追加や変更が容易です。 -
負荷の分散:
ある機能に負荷がかかった場合でも、特定のマイクロサービスのみをスケールすることで対応可能です。これは、マイクロサービスがそれぞれ独立しているため、一部のサービスに負荷が集中しても他のサービスに影響を与えず、特定のサービスだけをスケールアップまたはスケールダウンすることが可能です。これにより、全体のパフォーマンスを維持しつつ、リソースの効率的な利用が可能となります。 -
開発体験の向上:
すべての機能がまとまっているモノリシックアーキテクチャの場合、改修を繰り返すうちにコードは必然的に複雑になっていきます。次第に全体像の把握が難しくなり、新サービスの開発や改修に掛かる時間が長くなってしまうというデメリットが挙げられます。一方で、マイクロサービスはそれぞれのサービスごとに独立して開発・実装を行えるため、全体像の把握が容易です。その結果、サービスの開発/改修のサイクルが回しやすいメリットがあります。また、マイクロサービスでは各サービスでデータを管理する分散型データ管理が好ましいとされているため、データーベースの変更にも比較的に柔軟に対応しやすいです。さらに、障害時の影響範囲も最小限に抑えることができるため、復旧作業やメンテナンス、CI/CDパイプラインも柔軟に設計することができます。
デメリット
-
導入の敷居が高い:
細分化された各サービスを連携させるのは、デメリットとしても捉えられます。マイクロサービス間を連携させることで、構成が複雑になるリスクがあります。そもそも、サービスをどのように切り出すのかを慎重に検討する必要があります。サービスを個々に切り分けていくため、サービスごとの管理業務やビジネス機能ごとにサービスを実装します。そのため、マイクロサービスでは各サービスを開発するチームもビジネス単位に分けることが望ましいとされています。小規模で人数が少ない企業でマイクロサービスを導入した場合、複数の一貫性がないシステムを少人数で管理する必要があるため、導入の敷居が高いケースも存在します。 -
データの一貫性を担保しづらい:
メリットにも挙げた、分散型データ管理を推奨しているため、マイクロサービスでは、各サービスが独自のデータベースを持ちます。その場合、トランザクションの整合性を維持するのが難しくなります。このため、DDD(ドメイン駆動設計)などを利用し、どのようにサービスを分割すれば良いかを注意深く設計する必要があります。 -
デバッグや統合テストの難易度が高い:
マイクロサービスでは、個々の機能単位は独立しているものの、エンドユーザーへ提供する際は、これらが連携して一つのシステムやアプリケーションとして動作します。そこで、エンドユーザーへ完成したサービスとして提供するに当たり、デバッグや統合テストが必要となりますが、エラーが出た場合に、その原因追求の作業が難化します。これらの問題を解決するためには、各サービスのログを一元的に管理し、エラー追跡を容易にするシステムが必要となります。また、各サービスのテストケースを十分に準備し、統合テストを行うことで、システム全体としての品質を確保する必要が考えられます。
マイクロサービスの個人的な実装例
マイクロサービスを実装するにあたって、数々のベストプラクティスが模索されてきましたが、主に以下の二つが挙げられます。
- マイクロサービスの構造を業務構造と関連付ける
- マイクロサービスの構造をシステム階層構造と関連付ける
上記の詳細については、ここでは割愛するので以下の記事を参照してください。
上記で述べた通り、マイクロサービスは大規模かつ業務ドメインに則したデータ管理をしているケースに導入した際に最大限の効果を発揮します。ただ、その場合、マイクロサービスに触れる機会がかなり限定されてしまい、キャッチアップの難化や学習コストがかなり高いです。そのため、今回は個人開発の範疇でマイクロサービスを導入するとどうなるのかについて探っていきたいと思います。
TL;DR
上記は、自分がコロナ禍にチーム開発していた「団体向けの体温管理サービス」にマイクロサービスを導入してみた例です。
主にバックエンド中心にマイクロサービスを組み込んでみました。体温計の画像から自動的に体温の値を抽出し、システムに反映できるようにしているので、その際にOCRサービスを利用する想定です。
サービスの分割については、機能そのものを基準としてサービスを切り出してみました。今回は、あくまでキャッチアップとして個人開発規模のサービスにマイクロサービスを導入してみたので、機能の一覧をそのままマイクロサービスの一覧として扱うことで単純明快な設計を心がけました。また、団体向けのシステムであるため、ユーザー管理や権限管理を担当するサービスを別途設け、それぞれのマイクロサービスがこのサービスを参照する形にしました。これにより、各マイクロサービスが独立して機能を提供しつつ、ユーザーの権限に基づいた適切なサービス提供が可能となっています。
認証サービスの切り出しについて
マイクロサービスの認証に関しては様々なアプローチがありますが、今回は上記のような設計にしています。
バックエンドのマイクロサービスがBFFからリクエストを受け取ったとき、認証済みかどうか検証する必要があります。そこで、認証サービスを切り出すことで、IDトークンを発行し、そのトークンをAuthorization
ヘッダーに含めてBearer認証でリクエストする設計にします。これにより、バックエンドのサービスは、そのトークンを検証することで認証済みのリクエストかどうか確認することができます。
BFFにGraphQLサーバを設置
団体向けのシステムなので、一般ユーザや管理者ユーザなどによりアクションを実行できる権限の区別が必要になります。認証のPaaSなどを利用して、IDトークン生成時にロールのIDも追加することで、権限サービスに認証情報としてロールIDを含めることができます。これにより、各マイクロサービスはユーザーのロールIDを参照し、ユーザーが実行可能なアクションを判断することができます。
例えば、一般ユーザーは自分の体温データのみを閲覧・更新でき、管理者ユーザーは全ユーザーの体温データを閲覧・更新できるようにします。また、異常体温者の通知は管理者ユーザーのみに送信されます。
さらに、BFFにGraphQLを採用することで、一回のリクエストでユーザ情報とそのロールの情報を取得可能にしています。
query getUser {
getUser(id: 1) {
id
name
role_id
role {
id
policy
}
}
}
GraphQLサーバ側でrole
フィールドに権限サービスのresolverを設定しておくことにより、権限管理周りは権限サービスが単一責務を維持しながら、一度のアクセスでロール情報も取得になります。
権限情報のキャッシュ
また、疎結合性を維持するために、各サービスごとで権限情報のキャッシュを保持することも考えられます。そうなると、キャッシュデータの更新の問題も浮上しますが、権限データで更新があるたびにイベント駆動で更新をかけることで、サービス間の影響を極力抑えることができます。今回は、システムの複雑化を避けるために省きましたが、ここでメッセージブローカーを挟んでもいいかもしれません。
メッセージブローカーの導入
体調不良者の有無を管理者にリアルタイムで確実に通知する必要があったため、体温データ管理サービスと通知サービスの間にメッセージブローカーを仲介しています。メッセージブローカーは、異なるマイクロサービス間でのメッセージのやり取りを効率的に行うためのミドルウェアです。この例では、体温データ管理サービスが異常体温者のデータを検出した際に、その情報をメッセージブローカーに送信します。メッセージブローカーはそのメッセージを受け取り、通知サービスにリアルタイムで転送します。通知サービスはメッセージを受け取ると、管理者に対して通知を行います。
このように、メッセージブローカーを導入することで、異なるマイクロサービス間の通信を効率的に行うことができ、リアルタイムの通知などの要件を満たすことが可能となります。また、メッセージブローカーはメッセージの配信保証や順序保証などの機能を提供するため、信頼性の高いシステムを構築することができます。
まとめ
マイクロサービスは、その柔軟性と拡張性から多くのメリットを提供しますが、同時に設計や運用の複雑さからくるデメリットも存在します。そのため、マイクロサービスを採用する際には、その特性を理解し、適切な設計と運用計画を立てることが重要だと思います。また、モノリシックからマイクロサービスへの移行は、段階的に行うことでリスクを管理しつつ、効率的に進めることが可能になると思います。今回は、学生の身分ながら個人開発に無理やり組み込んでみましたが、大規模開発の現場では、より複雑化していると思われるのでぜひ経験してみたいです。
参考文献