設計

マイクロサービス (同期|非同期) x (直列|並列) 設計パターンとプロトコルについて

この記事では、マイクロサービスを設計する際に考慮すべき内容をまとめています。

Software Design 2020年1月号に記載された記事を元に、個人的に参考になった箇所を抜粋しました。
(同期 or 非同期)x(直列 or 並列)の設計パターンや、マイクロサービス間の通信プロトコルに何を選べば良いかなど、
これからまさにマイクロサービスの設計をするという方がご覧になれば、必ず参考になる内容が書かれていると思います。

Software Designにはここに記載したもの以外にも、マイクロサービス設計に関する有用な内容が多数記載されていますので、機会があればぜひ書籍を購入してみて下さい。(記事の最後にもリンクがあります)

Software Design 2020年1月号

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

マイクロサービスアーキテクチャは、一言で言うと「バラバラ」にする技術です。

モノリシックな(一枚岩の)アプリケーションとして開発して運用するのではなく、1つのアプリケーションが独立して動作する、多数の小さなサービス群に分割し、ゆるやかに連動させることを目指します。

何をバラバラにするのか?

プログラムを分割するという点では、マイクロサービスの分割も、従来のモジュール分割の考え方と同様です。
しかし、マイクロサービス分割ではプログラムのモジュール分割だけではなく、様々な側面でバラバラに分散させます

最も大きな特徴は、それぞれの実行環境をバラバラに分散させることです。
個々のマイクロサービスは、独立した実行環境に対して、任意のタイミングでデプロイを可能にします。

実行環境とデプロイのタイミングを分けられるようにするために、データ(DB等)も分散して管理します。
処理のタイミングもサービス間の結合度合いを小さくして、できるだけ異なるタイミングで実行できるように設計します。

なぜバラバラにするのか?

一言で言うなら、変更要求への対応スピードを上げ、変更に対応するコストを下げるためです。

モノリシックな大きなアプリケーション構造では、変更のための調査、検討、コードの変更、確認のためのテスト、本番環境への再配置などに時間とコストが掛かります。

マイクロサービスの分割とは、このような作業を小さな単位に分けることで、変更や調査に掛かる時間を短くし、作業を単純にするための工夫となります。

マイクロサービスの設計パターンとは?

マイクロサービスの個々のサービスはそれぞれの実行環境が分かれているため、なんらかの通信手段を使って連携することになります。

サービス間連携の基本的な選択肢は、(同期 or 非同期 の通信方式) と (直列 or 並列 の処理形態) の組み合わせになります。

マイクロサービスでアプリケーションを設計する場合、この(同期 or 非同期)x(直列 or 並列)を適切に組み合わせることが基本的な考え方になります。

マイクロサービス設計パターン
  • 同期・直列
  • 同期・並列
  • 非同期・直列
  • 非同期・並列

2つの直列パターン

同期・直列パターン

「同期・直列パターン」は、モノリシックなアプリケーションにおけるサブルーチンの階層的な呼び出しと基本的には同じ形式です。
動作は、他のサービスに処理を依頼し、その結果を待ちます。

同期・直列パターン

非同期・直列パターン

「非同期・直列パターン」は、先行するサービスについては (他サービスに)メッセージを送信するだけで完了します。
送信先がメッセージを受け取ったかどうかの確認や、結果の待ち合わせをしません。

非同期パターンの方が、結果を待ち合わせないので、サービス間の依存は小さくなります。

非同期・直列パターン

2つの並列パターン

同期・並列パターン

「同期・並列パターン」は、モノリシックなアプリケーションで、サブルーチンを順番に呼び出していくパターンと基本的には同じです。

一つのサービスの責任が大きくなる可能性があるため、注意が必要です。

同期・並列パターン

非同期・並列パターン

「非同期・並列パターン」は、発信元のサービスはメッセージを出版(publish)するだけです。(Apache Kafkaなどを使って実装するイメージ)
そのメッセージを誰が受け取り、どう処理するかには発信元では関与しません。

同期方式に比べ、サービス間の依存性を極端に小さくすることができます。

非同期・並列パターン

集約パターン

非同期・並列・集約パターン

「非同期・並列→集約パターン」は、それぞれのサービスの結果を集約して1つの処理を行いたい場合に使います。

上記、「非同期・並列パターン」の応用になります。

非同期・並列→集約パターン

どのパターンを選ぶか?

紹介したパターンは、それぞれ一長一短があるので、メリット・デメリットを加味して選択します。

同期方式のメリット・デメリット

メリット

  • モノリシックなサブルーチン呼び出しと同じ考え方で設計できるので、多くのエンジニアにとって取り組みやすい

デメリット

  • 依存関係が強く、処理のタイミングが同じなので、分割したサービス間が密結合になり、変更の影響が他のサービスに波及しやすい

非同期方式のメリット・デメリット

メリット

  • キューやトピックと呼ばれるメッセージ送信のしくみ(チャネル)を間に挟むことで、サービス間の関係が疎になる

デメリット

  • 同期方式と異なる設計ノウハウが必要になる

マイクロサービスの構成パターン

通信方式構造処理パターン説明
同期直列サービス呼び出し上位のサービスが買いのサービスを呼び出す
同期並列サービスマネージャ上位のサービスが買いのサービス群を束ねる
非同期直列パイプラインサービスフロー
非同期並列出版・購読独立したサービス群の並列処理
非同期並列→集約分配・集約並行処理結果の集約
マイクロサービスの構成パターン組み合わせ表

これらのパターンは目的に応じて使い分けることになります。

一般的に現実世界のビジネス活動は、非同期・並列パターンで行われていることが多いです。
そのようなビジネス活動を支えるしくみとして、マイクロサービスによるアプリケーションで「非同期・並列パターン」を採用することは、有力な選択肢となりえます。(オススメパターン)

サービスはどういう単位で分けるか?

マイクロサービスに分割するメリットを活かすためには、観点として次の内容が押さえられているか確認すると良いです。

  • それぞれのサービスはお互いに独立性が高いか?
    • 隠蔽性:お互いのサービス内部の詳細を知らずに利用出来る
    • 交換可能性:他のサービスの変更なしに入れ替え(交換)が出来る
  • サービスが切り分けられていることで、技術的な選択肢を広げることが出来るか?
    • 低コストかつ高パフォーマンスを発揮出来る
    • 競争優位性を得ることが出来る
  • サービスが切り分けられていることで、開発効率を高められるか?

要約すると

  • 一度に考えなければいけない範囲を減らすことが出来る
  • 適材適所で考えることが出来る
  • 効率よく開発出来る

ドメインの境界で分ける

サービスが互いに独立するように分ける一つの方法は、サービスが扱う「ドメイン」の境界で分けることです。

ドメインとは、扱う問題領域のことを指します。
問題領域の境界は、ドメイン駆動設計の世界では「境界づけられたコンテキスト」と呼ばれています。

再利用性の高いものを分ける

色々なサービスから共通して利用されるような再利用性の高いコンポーネントは、独立性が高い方が変更を加えた時にそれぞれの利用者に影響が波及しにくくなります。

例えば、ユーザーアカウントの管理機能をマイクロサービスとして切り出しておくと、複数のサービスから共通して利用することが出来ます。
1つのGoogleアカウントを持っていれば、GmailもYouTubeも利用できるといった具合です。

プログラミング言語や採用技術で分ける

ある程度大きなサービスであれば、複数の異なる技術を駆使して作り上げられていることが多いと思います。
そして個々の技術領域には、それぞれに適したフレームワークや実装言語が存在します。

サービスを技術領域ごとに別々のサービスに分割することが出来れば、それぞれに適したフレームワークや実装言語を選択することが出来ます

開発サイクルやプロセスの違いで分ける

開発効率を上げるためにマイクロサービス化するという観点では、開発サイクルやプロセスの違いに着目するのも分割の助けになるかも知れません。

例えば、頻繁に細かな修正があるサービスと、長期的に大きな開発をしなければならないサービスが混然一体になっていると、長期開発側に細かな変更を都度取り込んでいかなければ、後でお互いの変更が衝突してしまうかもしれません。
これらが別々のサービスに分離されていれば、お互いの開発状況に過度に気を使わなくて済むようになります。

頻繁に変更のあるフロントエンド寄りの部分は BFF (Backends For Frontends) にして、アプリケーションの核となる部分とは切り離しておく、というやり方もあり得ます。

組織構造に合わせて分ける

1つのサービスに対して複数のチームが手を入れると開発効率が低下します。一般的に利害関係者が増えると調整のコストが増すからです。

なるべく、それぞれのチームが扱っている範囲に沿って分割したり、外部要因の影響を受けやすい部分をマイクロサービスとして切り出したりしておくと、他のサービスやチームからの影響が少なくなります。

サービス間のプロトコルはどれを選ぶ?

サービス間のプロトコルを決めるうえで、考慮すべき観点は以下の通りです。

  • 開発しやすいか?
  • 運用しやすいか?
  • パフォーマンスは十分か?
  • 安定した技術か?
  • マイクロサービスの利点を損なわないか?

開発する上では、手軽に試せて上手く行かない場合にどう対処したらよいか分かりやすいものが良いと思います。
高いパフォーマンスが求めれられるシステムであれば、プロトコル要因のオーバーヘッドも少なく抑える必要があります。
また、いくら良さそうな技術でも、数年で廃れてしまったら、後で苦い思いをするかも知れません。
そして、マイクロサービスにした利点を損なってしまっては本末転倒です。

こういった観点で、いくつかのプロトコルについてメリット・デメリットを以下にまとめます。

HTTP (REST)

[オススメ度 ]

Webサービスのために培われてきた様々なエコシステムをそのまま利用することが出来ます。

例えば、プロキシ、アクセスログ、キャッシュ、キープアライブ、暗号化(HTTPS)、データ圧縮といった従来の機能です。
クライアント/サーバどちらの場合でも、また間違いなくどのような言語でも、ライブラリが提供されていて、将来に渡ってサポートされ続ける可能性が高いと予想されます。
また、テキストベースのプロトコルのため、動作の確認も容易です。

他に大きな理由がないなら、HTTP(HTTPS) で REST API を使うことが出来るか、まず最初に検討しましょう。

HTTP (RPC)

[オススメ度 ]

HTTP上のRPC(リモートプロシージャコール)には、XMLベースのSOAPやXML-RPC、JSONベースのJSON-RPCなどがあります。
あらかじめサーバとクライアント間でインターフェースの合意を取り、クライアントからはあたかも通常のメソッド呼び出しかのように遠隔サービスを利用できるものです。

マイクロサービスにおいては、RPCはやや不向きな面があります。
各サービスが独立性を維持するためには、外部からはなるべく内部の詳細には踏み込まないのが理想です。しかしRPCでは、あたかもローカルなメソッド呼び出しかのように他のマイクロサービスを利用出来てしまい、(クライアントにとって)それが自然であればあるほど、コンテキストを外部と共有してしまっていることになるからです。

HTTP (GraphQL)

[オススメ度 ]

インターフェースを過度にクライアント側の都合に合わせず、それでいて柔軟に使えるようにするために考案されたのがGraphQLです

GraphQLでもまずインターフェース定義を書くのはRPCと同様ですが、任意のメソッドを定義するのではなくRESTのように特定のリソースに対する操作を定義します。
更に、あるリソースから別のリソースへの解決方法を提供することができ、リソース間の関係が複雑な場合でもクライアント側からはリソースグラフの中から欲しい部分を簡単に取り出すことが出来ます。

GraphQLが役立つ典型例はフロントエンドから利用する場合です。
フロントエンドでどういう要素を表示するかを試行錯誤しながら素早く変えていきたい時に、バックエンドの実装がそれに引きずられにくくなるからです。
BFFに対するインターフェースの場合でも同じことが言えるかも知れません。

逆にそれ以外の場面では、注意深くマイクロサービスを切り分けている限り、GraphQLの出番は無いかも知れません。

Apache Thrift

[オススメ度 ]

バイナリプロトコルのRPCです。

HTTPベースのRPCの場合と同様に、マイクロサービスには不向きな側面もあります。
それに加えて、最近はThriftの話自体を聞く機会が減少しています。

今後もエコシステムが安定しているかは不安です

Protocol Buffers / gRPC

[オススメ度 ]

バイナリプロトコル(通信プロトコルとしてはHTTP/2)のRPCです。

RPCとしての欠点はありますが、速度が求められる場合には他に有力な選択肢が少ないため、マイクロサービス間のインターフェースとしても採用されることが多い印象です。

Akka

[オススメ度 ]

非同期な呼び出しが必要で、ある程度複雑な分散システムを構築したい場合に選択されるプロトコルです。

クラスタ化の機能が強力で、Akka Streams を使うことで非同期なイベントモデルにも対応することが出来ます。

高度な分散システムを実現できる一方で、その分システムは複雑になり、利用できるプログラミング言語もある程度限定されます
そのため、Akkaに関するエキスパートが居て、かつ組織文化にもフィットする場合に採用するようにします。

AMQP

[オススメ度 ]

RabbitMQなどが採用しているメッセージングプロトコルです。

オープンなプロトコルで他にも実装があるため、プロトコルそのものに対する安心感があります。
例えば、Alpakka は Akka Streams ベースのサービスの AMQP での接続をサポートしています。

非同期的なイベントモデルで分散システムを構築すること自体が難しいため、運用ノウハウがかなり要求される プロトコルと言われています。

クラウドサービス独自プロトコル

[オススメ度 ]

クラウドサービスで非同期的なイベントモデル(ストリーム)を扱うものとしては、Amazon Kinesis Data Streams や Google Cloud Dataflow、もしくは Cloud Pub/Sub などがあります。

マネージドなサービスのため、大規模な分散システムを構築・運用する上ではだいぶ手間が省ける可能性があります。

参考文献

書籍

著者

この記事は役に立ちましたか?

  • この記事を書いた人
アバター画像

ゴイチ

ソフトウェアエンジニア歴20年。 C/C++、Java、C#、Kotlinが得意で、組込系からAndroidアプリ、大規模なWebサービスなど幅広いプログラミング経験があります。 現在は某SNSの会社でWebエンジニアをしています。

-設計
-,