ModernJavaEEDesignPatterns / Microservices Best Practices


マイクロサービス ベストプラクティス

要約

以下に示す原則は開発時のベストプラクティスとして登場し、マイクロサービスベースのアーキテクチャと連携している。また初期評価や開発前のチェックリストとしても役立つ。

自動化設計

継続的デリバリ(CD)は、いつでも確実にリリースできるよう短いサイクルで利用可能なソフトウェアを生成するソフトウェアエンジニアリングアプローチである。ソフトウェア開発において、ソフトウェアの配信プロセスを自動化および改善するために利用される。CDは数段落では済ませられないボリュームを持つ複雑で包括的なトピックであるが、背後にある考えはマイクロサービスベースのアプリケーションの革新サイクルがうまく回る仕組みを提供することである。ここで最も関係する継続的デリバリの原則は、早くプロダクションにデプロイしアイデアとフィードバックのサイクルを短くすることである。

素早いデプロイを実現するには、インフラの自動化やビルドの自動化、デプロイやロールバックの自動化、データ移行の自動化、そしてもちろんテストの自動化を含む多くの継続的デリバリのテクニックを必要とする。これらのテクニックはいずれも新機能を早く開発したり、新しいシステムを早くテストしたり、アプリケーションを安全に早くプロダクトションにデプロイしたり、システムが期待通りに動かなかったり機能があまり良くないアイデアだと分かった時に安全に早くロールバックしたりするのに必要である。

失敗のためのデザイン

高可用性の高水準はファイブナインと呼ばれ、99.999%の稼働率保証を意味する。これは、年間を通じてダウンタイムがわずか5分半を意味する。昔は「信頼性」や「失敗防止」という言葉を使っていたが、クラウドベースのマイクロサービスアーキテクチャは完全に異なる。

多数の個別サービスで構成されたアプリケーションにおいて、可用性を測定し失敗のためにデザインするためにアプリケーションの関連する全ての部分に接する、指数関数的に増える複雑さに対処しなければならない。また、相互依存性が多くなると複雑さはさらに増すことになる。最も重要なのは、完全なアプリケーションが動くまでユーザの振る舞いによって需要を分類することができないことだ。

耐障害性に関するデザインのゴールは人間の介入を最小限にすることである。障害ルーチンの自動化を実装することは発生している全サービスコールの一部である必要がある。使いやすさの指標と応答時間を振り返ることで、意外に早く非常に有益になる。しかし、失敗したサービスでできることはなんだろう?来たリクエストに対してどうやって意味ある結果を生成するのか?

サービスの負荷分散と自動スケーリング

最初の防衛線となるのはサービス品質保証(SLA)に基づいた負荷分散である。マイクロサービスはいずれも利用率や平均応答時間についての詳細情報を探せるメタデータのセットを必要とする。閾値に応じて、サービスは水平(物理マシンを増やす)または垂直(1マシンに実行するソフトウェアインスタンスを増やす)に自動的にスケールすべきである。

執筆時点において、これはアプリケーションに対して最もよく知られたクラウドプラットフォームの特徴である。マイクロサービスのための個々のSLAとメトリクスに基づいたスケーリングはKubernetesのようなオーケストレーションレイヤにてすぐに実装されるだろう。それまでは、メタ情報とスケーリング自動化の独自セットを構築する必要がある。

最も簡単なのは、早く失敗して早くその失敗を検出することだ。失敗しているサービスをマークするには、呼び出し番号を追跡しサービスインスタンスを完全に退けることを決定するまで妥当な回数分リトライをする方法を考える必要がある。望むべき振る舞いを行うサービスを実装するのに役立つ方法が4パターンある。

失敗時のリトライ
このパターンはサービスに接続しようとして一時的に失敗したのを対処できるようにするもので、失敗の原因が一時的であることを期待して、失敗前の操作を透過的にリトライする。リトライのパターンは動的またはサービスメタデータに基づいた固定値分の回数を設定できる。リトライは同期、ブロッキング、非同期ノンブロッキングとして実装でき、実装に役立つライブラリが多数ある。

メッセージやメッセージングシステムを利用することで失敗時のリトライを少し楽にできる。サービスに関するメタデータはキューまたはイベントバスによって解釈され、それに従って反応する。永続的な失敗の場合、メッセージはCompensating Service(補償サービス)またはDead-Letter Endpointに終わる。いずれにせよ、メッセージングやイベントバス駆動ソリューションは、殆どのエンタープライズ環境において統合や処理を簡単にするだろう。
サーキットブレーカー
サーキットブレーカーはリモートサービスに接続するのに時間がかかる不具合に対応し、失敗するリスクがある操作のプロキシとして動作する。プロキシは直近の失敗数を監視し、その情報を利用して操作を続行するか単に例外を返すかを決定する。これはMichal Nygard氏による「Release It!」によって最初に広まり、Martin Fowler氏による記事「CircuitBreaker」にて概要を得られる。
隔壁(かくへき)
隔壁という名前は、実際の船の沈没から防ぐことからシステムを分割し失敗に耐えられることを意味する。もし正しく行われた場合、システム全体を停止するのとは対照的に一つの領域にエラーを閉じ込められる。パーティションによりハードウェアの冗長性や特定のCPUにバインドされたプロセス、異なるサーバクラスタへの専用機能の分割にいたるまで完全に別のものとすることができる。
タイムアウト
リクエストのリソースを永遠に待つことなく、専用のタイムアウトが失敗を早期に見つけることにつながる。これはリトライとサーキットブレーカーの非常にシンプルな形態であり、より低レベルのサービスとの通信時に利用できる。

データ分離のためのデザイン

単一のリレーショナルデータベースにデータを格納する昔ながらのモノリシックアプリケーションを考えてみよう。アプリケーションのすべての部分は同じドメインオブジェクトにアクセスし、トランザクションやデータ分離周りの問題は通常発生しない。データ分離はマイクロサービスとは異なる。2つ以上のサービスが同じデータストアを操作すると一貫性の問題に突き当たる。この(トランザクション)周りの潜在的な方法はあるが、一般的にアンチパターンとみなされている。よって、最初のアプローチとしては全てのシステムを独立させることだ。サービスを疎結合にできるので、マイクロサービスとともに一般的なアプローチである。しかし、根底にあるデータが一貫になるコードを実装しなければならない。これは競合状態や失敗、各サービスの色んなデータストアの一貫性補償などを含んでいる。ドメインサービスを見ている間は容易だが、他サービスとの依存関係が増えると難しく複雑になっていくので、整合性の明示的なデザインが必要となる。


整合性のためのデザイン

各サービスのデータが完全に別々に格納されている間、サービスはCompensating Transactionと一貫した状態を保てる。経験則から、一つのサービスは一つのトランザクションにきっちり関連させるべきである。これは、データを永続する全てのサービスが起動して利用可能になっている場合において唯一の現実的なソリューションである。もしそうでない場合でも、サービスカスケードの呼び出しを完全に失敗させ、早期にロールバックをCompensating Transactionとともに呼ぶことができるが、最終的に得られる結果整合性は何の保証もない。よってエンタープライズシステムにおいて十分でないかもしれない。以下のサブセクションではこの問題を解決するために利用できる幾つかの異なるアプローチについて議論する。

トランザクションの利用

よくある誤解に、マイクロサービスベースのアーキテクチャはトランザクションを全く持てないまたは使えない、というのがある。モダンなソフトウェアスタックの一部とみなす異なる技術を用いてアトミックまたは拡張したトランザクションを利用する方法はたくさんある。例として、サーバがサポートするトランザクションマネージャからOMGによるAdditional Structuring Mechanisms for OTSやOASISのWS-Transaction、REST-ATのようなベンダー固有のソリューションまで色々ある。自身のインフラやサービスにて同等の機能(例: 任意の障害や不明瞭なサービス回復、モジュール構造メカニズム、異なる通信パターンにまたがった状況下での一貫性)を実装するのは非常に慎重に検討したほうが良い。

リードとライトの分離

もし最初にトランザクションを調べたくないのであれば、リードオンリーのサービスとライトオンリーのサービスに分離することで複雑さを軽減できるかもしれない。もしサービスのかなりの部分がドメインオブジェクトを読み取るのみになれば、サービスを分離しやすくなり必要な補償アクションの数を減らせるだろう。

イベント駆動設計

トランザクションの別アプローチはサービスのイベント駆動設計である。これは、全サービスの全ての書き込みを一連のイベントとして記録するロジックが必要となる。このイベントを登録して消費(consume)することで、複数のサービスは順序付けられたイベントのストリームに反応し何からの有用な動作を行う。消費サービスは自身の速度および可用性でイベントを読み取ることができなければならない。これは特定のサービスがダウンした後に消費(consumption)を再開できるようイベントの追跡も含まれる。イベントデータベースとして完全な書き込み履歴を持つことで、後の段階で新しいサービスを追加したり、独自の有用なビジネスロジックを追加するために全ての記録されたイベントを介してを動作させたりすることもできる。

トランザクションIDの利用

もう一つの変形は、サービス呼び出しをトランザクションIDと組み合わせることである。トランザクションIDをペイロードに追加することで、その後のサービス呼び出しは長時間実行のトランザクションリクエストを識別できる。全てのサービスが全てのトランザクションを渡すまで、データ変更はフラグのみ立てられ、二番目の(非同期)サービス呼び出しは、全ての貢献(contributing)サービスが成功結果を知るために必要とされる。システム内でリクエスト数が急激に上昇した場合に、完全な一貫性を必要とする非常にレアかつ複雑なケースにおいて唯一のソリューションだが、大半のサービスは必要ない。

注: 上記の全てのソリューションは異なるレベルの一貫性につながり、2フェーズコミット/XAトランザクション(例: 相関IDまたはReactiveシステム設計)を使って動作する方法はいくつもあるが、これら全てはシステムの重要な部分すなわち全体のパフォーマンスに影響を与える。

パフォーマンス設計

パフォーマンスは全てのエンタープライズアプリケーションの最も重要な部分である。最も仕様が曖昧で非機能要件であっても、最も不満となりうる。

マイクロサービスベースのアーキテクチャは両面において大幅にパフォーマンスに影響を与える。まず第一に、サービスの粒度が荒ければより多くのサービス呼び出しにつながる。ビジネスロジックとサービスのサイズによって、この影響は、単一のサービス呼び出しが6から10のバックエンドサービス呼び出しへと広がり、同期サービスの場合は同量のネットワーク遅延が発生する。この問題を制御する戦略はたくさんあり、多くの要因によって異なる。

早期に負荷テスト、頻繁に負荷テスト

パフォーマンステストは分散アプリケーションの重要な部分であり、新しいアーキテクチャにおいてより重要である。完全なシステムのパフォーマンスが積極的にテストされ、個々のサービスは開発段階で既にテストされていることを確認する必要がある。

実際の実行時監視も同様に重要である。しかし最大の違いは、負荷テストは個々のサービスまたはサービス群の初期のメタ情報を検証する事前対策なのである。また、初期のSLAを確認および定義する方法でもある。マイクロサービスに関するほとんどの記事や書籍がこのことについて強調していないが、負荷テストは新しい種類のアプリケーションアーキテクチャと運用モデルに必要なマインドシフトを助けるための、エンタープライズにおいて特に重要なものである。

ジョブに適した技術の利用

通常のアプローチはRESTfulな呼び出しの全てのエンドポイントにもとづく。実際のところ、これは要件に対する唯一の実現可能なソリューションというわけではないかもしれない。時折言われているのは、HTTPベースのRESTfulサービスやマイクロサービス間の1対1の関係は確固たるものではない。エンドポイント技術、インターフェースアーキテクチャ、プロトコルにk擦る全てはエンタープライズ環境にてテスト可能である。

一部のサービスは同期または非同期メッセージングを介して通信したほうがよいが、他は理想を言えばHTTP上で通信するRESTfulエンドポイントを使って実装したほうがよい。古いリモート処理技術に基づいた低レベルサービスインターフェースを利用する必要があるインスタンスもまれにあるかもしれない。バズワード互換になるためだけに全体のパフォーマンスを犠牲にすべきではない。さらに、異なるシナリオや最適なパフォーマンスのインターフェース技術スタックをテストするのに役立つ。

APIゲートウェイとロードバランサーの利用

もう一つの重要な側面はAPIのバージョニングおよび管理である。我々はこれ以上完全にモノリシックなデプロイを管理する必要がなく、サービスAPIやエンドポイント上の明示的なバージョニングを利用することで、より魅力的になる。様々なAPI管理ソリューションがあり、シンプルなフレームワークからプロダクトの一部としてデプロイしなければならないものまで色々ある。

RESTfulサービスを使おうとする場合、最低でもAPIゲートウェイは使う必要があり、インターフェースの様々な側面を追跡するのに役立つ。もっとも重要なのはサービスのバージョンに基づいてディスパッチしてくれることであり、これらのほとんどは負荷分散の機能も提供している。

監視やバージョニング、負荷分散の他に、サービスやバージョン毎の個々のコール数を追跡することも重要である。これは完全なSLAの概要を得たり、サービス利用やボトルネックの問題を追跡したりするための最初のステップである。パフォーマンス関連のトピックは別として、APIゲートウェイや管理ソリューションはガバナンスやセキュリティを含む幅広い追加機能を提供する。

適切なレイヤでのキャッシュの利用

キャッシングはマイクロサービスアーキテクチャの最も重要でパフォーマンスに関連する部分である。アプリケーション内において基本的に二種類のデータが存在する。ヘヴィにキャッシュされるものとキャッシュされることがないものだ。後者は絶えず更新するデータストリーム(例: 株式情報)、またはセキュアでパーソナライズな重要情報(例: クレジットカードや医療データ)などがある。他の全てが異なるレベルでヘヴィにキャッシュできる。

マイクロサービスのUI面は実際、エッジキャッシュやCDN、シンプルなHTTPプロキシのような既存の高パフォーマンスWeb技術を利用できる。これらのソリューションは全てサーバとクライアント間でネゴシエートされたキャッシュ有効期限設定に依存している。バックエンドにて異なるレイヤのキャッシュ技術が機能する。最も簡単なケースはJPAプロバイダを使ったセカンドレベルキャッシュ、あるいはドメインエンティティのキャッシュレイヤとして専用のインメモリデータストアを使うことである。最大の問題は、キャッシュレプリカ間やキャッシュとバックエンドデータストア間の整合性を保つことである。ここでのベストなアプローチは、JBOSS Infinispanのような既存実装を利用することである。

担当者のつぶやき

  • 最後にしれっとステマ。

みんなの突っ込み