🦁

マイクロサービスの再考: タダ飯なんてものはない

2022/09/18に公開

どうも、株式会社プラハCEO兼エンジニアの松原です。

先日かとじゅんさんがツイートで紹介していたマイクロサービスに関する論文を読むついでに、適度に意訳した内容を音声入力してみました。ついでに意訳レベルなので翻訳の質は保証できないのですが、もし内容を読んでみて少しでも興味を持てた場合は実際の論文にも目を通してみると良いかもしれません。

論文のリンク:

https://www.researchgate.net/publication/352397102_No_Free_Lunch_Microservice_Practices_Reconsidered_in_Industry

「これ日本語でなんて言うの?」って分からなかった部分も多々あったのでより適切な単語があったら教えてほしい...!

導入

マイクロサービスには様々なプラクティスや技術を用いて以下のメリットを目指す

  • 素早いデリバリー
  • 高いスケーラビリティ
  • 自律性

しかし実際にこの業界で実装されるマイクロサービスは採用するプラクティスや効果に大きな差があるため、オンラインサーベイ(51回答)と経験豊富なマイクロサービス実践者14名にインタビューを行った。

わかったこと

結果、以下のことがわかった

  • マイクロサービスには3つのレベルがある
  • レベル1: 独立した開発/デプロイが可能
  • レベル2: スケーラビリティと可用性を満たしている
  • レベル3: サービスのエコシステムになっている(?)

また組織がマイクロサービスを実践する上で生じる11の課題や制約条件を明らかにした

そもそもなぜマイクロサービスの実践状況には大きな幅があるのか?

マイクロサービスを取り入れる理由は市場や内部的な事情などビジネス上の要件から生じる。

例えば、あるサービスではアクセスが安定しているため、スケーラビリティには興味がないものの、独立してデプロイできたり、素早く新しい機能をデリバリーすることには興味があるかもしれない。すべてのプラクティスを取り入れるには多大な投資を必要とするため、各サービスが必要な要素を取捨選択した結果、多様な実践状況が生まれたと考えられる

マイクロサービスの特性

マイクロサービスには以下の約束事が求められる:

  • 素早いデリバリー
    • 個々のサービスを独自に開発してデプロイできる
    • 新しい要求やビジネス領域の拡張にうまく対応できる
  • 高いスケーラビリティと可用性
    • 環境や負荷に応じて自動的かつ柔軟に各サービスをスケールできる
    • 高い耐障害性と障害分離により高い可用性を確保できる
  • 自律性
    • それぞれのチームは異なる技術的な決断を独立して行えるため、チーム間のコミュニケーションを減らせる
    • それぞれのチームは自分たちのサービスにより適した言語やフレームワークなどを柔軟に選択できる

これらの約束事はベネフィット(利益、恩恵)として現れる。利益を生み出す度合いよってシステムの成熟度が測れる。

注意すべき点として、「利益」とは事業上の価値を満たすことを意味するものであって、対応する要素を単純に満たす事とは異なる。

例えばある組織で柔軟かつ自動的にスケールするシステムを実現できていなかったとしても、ユーザのアクセスが安定していればスケーラビリティの要求は満たしている。ビジネス的な要求がなければ、より低いレベルの成熟度のシステムのまま留まることを選択しても良い

利益の達成は組織のさまざまなcapability(能力)に依存する。

能力はissue(課題)によって制限される。

イシューが与える影響はシステムの成熟度によって異なる。

イシューの一部をプラクティスによって解決すればマイクロサービスが次のレベルに進めるかもしれないが、新たな課題に直面することもある。例えばデータベースを分割することでDBパフォーマンスに関する課題を改善したとしても、今度はデータ整合性に関する課題が現れるように。

結果
調査の結果、以下のことがわかった

サービスの分割について

サービスの分割はマイクロサービス開発における最も重要な部分だが、様々なシステムが異なる分割戦略をとっていた。

詳しくは表を参照。

  • 分割にあたっての戦略を問われ、51の回答のうち46が「開発者の経験」と答えた
  • ドメイン駆動設計に基づいて分割した回答は4つのみ

また、サービス分割により生じた問題がいくつか指摘された

  • coupling(結合)。サービス分割がうまく行われなかったため結合が残っていたり、モジュール同士があまりに頻繁に連携したり、互いに依存していた
  • consistency(一貫性)。サービス分割のデザインとコード実装の一貫性を担保することが難しかった
  • granularity(粒度)。分割の粒度があまりにも大きいか小さい。business isolation(ビジネス分離性?)に改善の必要があったり、管理コストが大きすぎるなどの問題が生じた

データベースの分割について

詳しくは表を参照。

共有dbが生まれた理由として最も多かったのが「ビジネスロジックの密結合」、次いで「データの同期とcascading queryの実行時間短縮」を挙げた。

db分割によって生じた問題がいくつか指摘された

  • redundancy(重複)。さまざまなテーブルでデータが重複した
  • granularity(粒度)。db分割の粒度が大きすぎるか小さすぎるため、バッチ性能の低下、データの分離に役立っていない、などなど...

デプロイについて

詳しくは表を参照

  • 仮想マシンとコンテナの組み合わせが最も多い
  • 大半のシステムがKubernetesを使う

サービス間通信について

詳しくは表を参照。

HTTP,AMQP(メッセージング),RPCが選択肢として挙げられた

  • 半分以上が混合
  • 単独利用ではhttpが最も多い、次いでrpc

APIゲートウェイについて

詳しくは表を参照。

サービスの登録と発見

クライアントが動的に必要なサービスを発見できるようになっているかどうか

詳しくは表を参照。

  • 大半のシステムはサービスのregistration(登録)/discovery(発見)の仕組みが用意されていた

ログとモニタリング

  • 大半のシステムはELKスタックを採用
  • トレーシングと可視化にはZipkin
  • 独自開発したAPMを使用するシステムも
  • モニタリングの解像度が不足していて、特化したミドルウェアが少ないことに不満を述べている人もいた

パフォーマンスと可用性に関する問題

サービス間のリモートコールが多いため、特定のリクエストのエラーが連鎖的な不具合を引き起こしたり、リモートコール自体が遅延を生むため、システム全体の性能に影響し得る。

  • ほぼ全てのシステムは何らかの仕組みによりパフォーマンスや可用性に関する課題に対処している。タイムアウト、レート制限、リトライ、サーキットブレーカーなど
  • 水平スケーリングの方が垂直スケーリングより好まれる
  • ただし自動スケーリングに対応しているシステムは少なく、大半は手動で行われている

テスト

詳しくは表を参照。

  • ほぼ全てのシステムで単体テストが実装されている
  • 次いで統合テスト、負荷テスト、E2Eテスト、コンポーネントテスト、CDCテストが行われていた
  • ロジックが要求を満たすこと、次いでパフォーマンスに関するテストが優先されていた
  • カオスエンジニアリングを用いてシステムの品質を保証するチームも見受けられた

不具合の特定

  • 大半はログを使ってトラブルシューティングを行う
  • 2/5はモニタリングツールを用いて原因を特定する
  • 残りはテスト、リモート/ローカルデバッグを用いる

不具合の特定に関しては以下の点が特に難しいそうだ

  • ログとモニタリングの弱さ。ログなどの重要な情報にトレース情報を付与しておくことが大切
  • 自動化不足。人力でログやモニタリング情報を頼りに不具合を特定する必要があり、自動化によるサポートが欲しい
  • 不具合の再現。本番不具合は開発環境で非常に再現しづらい

サービスの進化(?)

マイクロサービスの進化において品質管理は重要な役割を占める。

大半のシステムは2つのメトリックスを使用してシステムの品質を評価していた(システムメトリクスとサービスメトリクス)。

一部の回答者はCPUやメモリやネットワーク遅延などハードウェアやOSに関するメトリクスを述べた一方、一部の回答者はQPSやTPSやエラーや例外などサービスに関するメトリクスをより注視していた

インタビューを経てわかったこと

サーベイとインタビューの結果マイクロサービスの約束事や利益は大半のサービスで満たされていることがわかった。一方でその満たし度合いに関しては大きな開きがあった。 どれだけ利益を達成している日に応じてシステムを3つのレベルに分類した

  • レベル1: Independend Development and Deployment。それぞれのサービスは独自に開発してデプロイできる
  • レベル2: High scalability and availability。システムは柔軟にスケール可能で、耐障害性と不具合の分離により、高い可用性を実現している
  • レベル3: Service ecosystem. システムはエコシステムと呼ばれるまでに進化していて、新しい要求に応じてシステムを拡張することのみならずビジネスドメインの拡張にも対応できる

レベル1のマイクロサービス

レベル1のサービスはいくつかのマイクロサービスを抱えている。それぞれのサービスは物理的に分離されていて、独自のプロセスを実行し、軽量な仕組みを用いて相互通信を行っている。コンテナを使うこともあるがコンテナオーケストレーションを使うわけではない。システムをデプロイや管理するためにはオペレーターが主導でリソースを追加したり、実行環境を整える必要が生じている。

このようなシステムはmutable infrastructure(ミュータブルインフラ)と呼ばれ、サービスの目的を果たすために継続的にインフラを更新したり調整したりしなければいけない。

こうしたインフラは様々な問題を抱えている。例えばスケーリングが難しいこと、不具合から復旧したりロールバックするのが難しいことなど。

モノリスなシステムレベル1のマイクロサービスシステムに進化させるためにはサービスを分割し、CI/CDパイプラインを作成する必要がある。このような大規模なリファクタリングをしつつシステムの挙動が変わらないことを保証するのは非常に難しい

レベル2のマイクロサービス

数百に至るまでのサービスを抱えている。すべてのシステムは軽量コンテナ(Dockerなど)を使用していて、いくつかは仮想マシンとコンテナを組み合わせている。

レベル2のサービスに共通するのは「immutable infrastructure」を達成していることで、サービスのインスタンスは変えるのではなく置き換えられる(個別にSSHしてインスタンスを設定するような作業から解放されていることを意味していると思われる)。

レベル1からレベル2に進化するためには、開発組織はコンテナやイメージの管理の仕組み、コンテナオーケストレーターを導入する必要がある。

レベル3のマイクロサービス

ときには数百に至るサービスを抱えている。異なるビジネスドメインのサービスが共通のインフラにより相互につながり支援されている。サービスのインフラは技術的なサポート(リソースアロケーションやスケーリングなど)のみならずビジネス的なサポート(ユーザの認証や認可)も提供している。こうしたサービスは優れたクラウドコンピューティング技術(サービスメッシュやサーバレス技術)を使用している。

レベル2からレベル3に移行するためには、ビジネスサービスを提供する技術インフラを改善し、新たな技術をインフラに採用し、サービスエコシステムの管理の仕組みを導入する必要がある。

レベルが上がるごとにマイクロサービスの利益をより強く享受できる:

  • 並行開発。これはレベル1のマイクロサービスでも十分に達成できる。かつマイクロサービス実践者の大半(88%) は並行開発の成果に満足している。レベル2と3のマイクロサービスはさらに頻繁に小さなサービスを更新することに対応できるため、より強く利益を享受できる
  • 以下略(正直あまりレベル1〜3の分類に納得感がないので翻訳する気が起きなかった)

どういう課題があるのか

マイクロサービスを開発する上ではいくつかの課題が登場する。

サービスの分割について

イシュ-1: 誤った分割によるツケ

サービスは物理的に分離されているため、誤った分割を行うとパフォーマンスやスケーラビリティーに深刻な影響を及ぼす。

また物理的に分離されたサービスの内部に外からアクセスすることができないため、新たな実装要求が全く実現できなくなる可能性もある。一部の回答者は複数のサービスを統合し直す必要が生じたことを述べている。またモノリスのリファクタリングよりもマイクロサービスのリファクタリングの方が難易度が高かったと話している。

プラクティス

ドメイン駆動設計。ドメイン駆動設計はマイクロサービス固有の概念ではないが、より効果的にサービスを分割するために使われることが多い。ほとんどの回答者はドメイン駆動設計に基づきたいと話していたが、実際に適用できたのはわずかだった

チャレンジ

ドメインモデルと成果物のマッピングが最も難しい。どのようにドメインを分析して良いと良いモデルを作るのか、既存のレガシーシステムからどのようにartifact(成果物)(コードやテストケースなど)を抽出して新しい成果物に落とし込むのか、ドメインモデルと成果物の一貫性をどのように監視して維持するのか

イシュー2: 品質評価が難しい

モノリスシステムであれば静的解析に頼れるか、マイクロサービスでそれを行うためにはIPアドレスやサービスの名前とコードのレポジトリをマッピングしなければいけない。このマッピングは複雑でよくエラーを引き起こす。異なるレポジトリでサービスが開発されるようになると、サービス間が徐々に結合していくことを見つけるのがより難しくなってくる。

プラクティス

サービスの呼び出しをトレースして、サービス間の依存性を明らかにする

チャレンジ

高いコストと低いカバレッジ。実行時のトレースはモニタリングインフラに依存するため、もしそのようなインフラがまだ確立されていない場合は特に高い実装コストがかかる。かつ依存性のカバレッジが低い(のでROIが見合わない)

イシュー3: レガシーシステムをマイグレーションしづらい

半数近くのサービスはレガシーシステムから移行したものだった。複雑な依存性のためモノリスシステムをマイクロサービスに移行するのは難しい

プラクティス

ストラングラーパターンと腐敗防止層。

チャレンジ

レガシーシステムと新しいサービスの境界を見つけるのはしばしば難しい。不適切な境界を選ぶと新しいサービスの開発が難しくなったり、腐敗防止草の実装が困難になったりする。

マイグレーション中にもチャレンジがある。簡単な部分がサービスに置き換えられた時点でマイグレーションプロセスはしばしば巨大なモノリスレガシーサブシステムを残したまま終わることがある。残りのサブシステムはマイグレーションするにはあまりにも困難で、かつ新しいサービスの方でも重複コードが大量に実装されているなど、dead codeとして様々な問題を引き起こすことがある。

DBの分割について

イシュー4: 複数サービス間でデータ結合が起きる

データ要素(フィールドやテーブルなど)が複数のサービスから利用されている際にデータ結合が起きる。物理的な訓練によりcascading queryや従来のトランザクション管理はマイクロサービスで用いることができない。51のシステムのうち37がサービス間でデータベースを共有しているのもうなずける

プラクティス

複数サービスの呼び出しを結合することでcascading queryを、分散トランザクションフレームワーク(Seataなど)を用いることで分散トランザクションとデータ整合性を担保できる

チャレンジ

データベースが複数に分割されると依存するコードをファクタリングしなければいけない。またサービス呼び出しを結合すると深刻なネットワーク遅延を引き起こす可能性がある

デプロイについて

イシュー5: サービスの設定が複雑

例えばJVMとDockerのメモリ制限の不一致などにより実行時例外が起き得る。この問題は主にレベル2~3のサービスに影響する。彼らの実行環境が動的なため、一般的な設定値を推奨することが難しい。更に設定ミスによる問題は、特定も解決も難しい。

プラクティス(とチャレンジ)

なし

パフォーマンスと可用性について

イシュー6: ステートフルなサービス間の矛盾

ステートフルなサービスをスケーリングすることでインスタンスのステートが矛盾した状態になり得る。ステートフルなサービスは推奨できないが、モノリスからのマイグレーション中は存在し得る。レベル2~3システムに特に影響する。

プラクティス

ステートは外部ストレージにまとめる。Redisなど。

チャレンジ

リファクタリングにかかる高いコスト、ネットワーク遅延、システムボトルネック。ステートの排除にはリファクタリングを要する。また、外部システムがシステム上のボトルネックになり得る

イシュー7: 予想外/制御不可なオートスケーリング設定

オートスケーリングの設定はテストしづらく、設定値による影響は予想しづらい。かつ影響が制御不可かもしれない。マイクロサービスがDoS攻撃に晒された時、サービスインスタンスの急激な増加により大量のリソースが消費され、サービスが提供できなくなる可能性がある。マイクロサービス実践者が最も不満を感じていたのが「柔軟で自動的なスケーリング」だったのもこれが理由かもしれない。レベル2~3システムに特に影響する。

プラクティス

半自動スケーリング。問題が起きたかもしれない時は管理者にアラートを飛ばすものの、スケーリングの判断自体は管理者に委ねる。スケーリングの作業自体は自動的に行われる。

チャレンジ

実践できていた人はいなかったものの、完全自動化されたスケーリングが望ましい。予想可能で信頼できるスケーリングは、スケーリングを賢く判断する仕組みと、実行時の挙動に関する信頼に足る品質管理が必要

ログとモニタリングについて

イシュー8: 複雑で非同期なサービスの呼び出しチェーン

不具合の原因を特定したり、パフォーマンスの問題を特定するためには分散トレーシングが大体必要。レベル1から3に近づくにつれて影響が増えていく。

プラクティス

分散トレーシングには大きく二つの分類がある。invasive(侵襲)とnon-invasive(無侵襲)。前者はトレース用のプローブをサービスに埋め込む。後者はネットワークリクエストをプロキシするサイドカーを用いて計測する。

チャレンジ

invasive tracingは

  • 実装するコストが高い。特に複数言語でサービスが実装されている時は
  • 壊れやすい。誤ったトレースidを渡す程度のミスでもトレースチェーンが崩壊する

non-invasive tracingは

  • サービスメッシュなどのインフラが必要になること
  • サイドカープロキシによるネットワーク遅延

イシュー9: インシデントを検知しづらい

その動的で細かく分割された特性上、マイクロサービスにおきた不具合を検知するのは難しい

プラクティス

ダッシュボードと閾値。GrafanaやPrometheusといったモニタリングシステムが提供するメトリクスを使って異常を検知する

チャレンジ

オペレータが24時間ダッシュボードに貼り付く訳にはいかないので、anomaly(異常。不具合ではなく)を自動的に検知する仕組みや、複数のモニタリングツールを横断して必要な情報を得る手段が必要。また閾値の適切(正確でタイムリー)な設定には経験が必要で、システムの変更に追従して調整し続けなければいけない

不具合の特定について

イシュー10: サービス間の複雑かつ動的なやり取り

マイクロサービスの不具合は複雑で動的な分散環境におけるサービス間のやり取りから生じる。例えばサービスインスタンスは動的に生成/破壊されるため、リクエストの実行プロセスに関する情報は様々なインスタンスに分散している。そのため開発環境で再現するのが難しい。レベル2~3のサービスはより動的に環境が変化するため、強く影響される

プラクティス

ローカルデバッグ、モック、リモートデバッグ、トラフィックルーティング。

レベル1のシステムはそこまでサービス数が多くないので、ローカルデバッグを用いて不具合を特定できる。レベル2~3ではローカル環境で全てのサービスをホストできないこともあるため現実的ではない。リモートサーバーに接続して(VSCodeのRemote Debuggerなどを用いて)デバッグする必要がある。

他の手段としてはトラフィックルーティングがある。リモート環境からのトラフィックをローカル環境にルーティングしてデバッグする

チャレンジ

デバッグのパフォーマンス、インフラのスペック、インテリジェンスの欠如。リモートデバッグはそこまでスムーズな作業ではない。トラフィックルーティングは様々なネットワークインフラ対応を要する。現時点で用意されている不具合特定の手法はインテリジェンスに欠けている(?)ため機械学習を用いた不具合特定の手法が推奨される。

サービスの進化について

イシュー11: 互換性

マイクロサービスは多数のサービスを抱えることがあり、1つのサービスをアップグレードすることが上流サービスに問題を引き起こすことがある。

プラクティス

後方互換性と更新期限。複数バージョンのAPIを稼働させておくことで後方互換性を担保しつつ、サービスAPIの各バージョンの利用頻度をモニタリングして、使われていないバージョンは破棄する。またAPIの更新期限を設けることも有効。期限日を超えたら特定のサービスAPIバージョンは使えなくする

チャレンジ

メンテナンスコスト。サービスの更新による影響は予想しづらいことが多いため、他のサービスが壊れるかどうか開発者にはわからないことが多い。複数のバージョンを稼働させておくことで対策できるが、それによりメンテナンスコストが増加する

まとめ

レベル1のマイクロサービスの注力テーマは独自開発とデプロイができること。

新しく開発されるシステムに関しては大体のチャレンジはサービスの分割とサービス間の結合を避けることに関連している。ドメイン駆動設計や分散トランザクションといった設計手法は広く採用されているが、不適切なサービス分割やデータ結合はシステムのパフォーマンスや拡張性や独立性を阻害する。ドメイン駆動設計は素晴らしい手法だが効果的なテクニックやツールに欠けている。ドメインモデルと成果物のマッピング作成には知識に依存した(サービス固有の知識ってことかな)テクニックやツールが必要になる。

これから移行していくサービスのチャレンジはストラングラーパターンや腐敗防止層を用いて徐々に開発していく必要があるが、レガシーシステムの巨大なサブシステムが手付かずで残り、他のマイクロサービスとやり取りをしている状態になることが多い。これはサブシステムのリファクタリングにかかるコストがあまりにも高く、コスパが合わないために起きる。

移行途中に起きるこの状態はモニタリングやトレーシングに新たな困難をもたらす。モノリスシステムはシステム全体の観測容易性を影響するし、古いテクノロジーで作られていることが最新のトレーシングフレームワークの適用を阻害することもある。

レベル2のマイクロサービスの注力テーマは高いスケーラビリティと可用性。チャレンジは主に不具合の検知と特定に関するもの。AIOpsを導入する組織も。この規模のサービスは代替分散トレーシングを導入している。これには大量のデータ処理を必要とするため、データマイニング、機械学習、可視化などのテクニックが大切。

それでもなおログ、メトリクス、トレースの組み合わせを解析するのは非常に困難。ログは各サービスのローカルな状態を表し、メトリクスはインフラリソースやサービスの品質を、トレースは呼び出しチェーンの情報など、それぞれが異なるレベルの情報を記録している。これらを統合したデータ表現の手段が必要。

レベル3のマイクロサービスの注力テーマはビジネスドメインの継続的な拡大をサポートする技術的なインフラを確立すること。チャレンジは不確実な未来を抽象に落とし込むこと。複数の関連ドメインを横断する数多のサービスを管理すること。config管理、認可、バージョニング、evolution management(進化マネジメント?)に関する包括的な仕組みを作ること??

思ったこと

  • レベル3のシステムをどこまで自前で作っているのか気になった。AkkaとかElixirとかAxonあたりを使ってるんだろか
  • 段階的に移行しようとしてもリファクタリングの難易度が高すぎるレガシーサブシステムが残りがち問題→めっちゃ起きそう
  • DoS、インシデント検知、(マルチクライアント対応時の)後方互換性に関してはモノリスでも同じことが言えそうだと思ったけど、どの要素がマイクロサービス固有なのだろう?
  • 分散トランザクションってDDDと同列に並べる設計手法だっけ?
  • DDDの問題について指摘している中で"Knowledge-based techniques and tools are required to map between the domain model and artifacts, and maintain their consistency" って書いてあるけど、どういう意味だろう?開発者の経験や知識に大きく品質が依存してるってことなのかな
  • レベル3の定義が掴みきれなかった
アガルートテクノロジーズ/PrAha

Discussion