精読「マイクロサービスアーキテクチャ 第2版」(第二部 実装 - 第13章 スケーリング)
マイクロサービスアーキテクチャ 第2版
マイクロサービスの設計、実装、運用に必要なベストプラクティスや最新技術を解説した、実践的なガイドブックです。これを読めば、マイクロサービスに関してそれっぽい会話もできますよ。
関連記事
スケーリングの4つの軸
システムをスケールさせる方法は一つではなく、状況や制約によって最適な方法が異なる。この章では、スケーリングの「4つの軸」を紹介している
- 垂直スケーリング: より高性能なマシンを導入する。
- 水平複製: 同じ処理を行うものを複数配置する。
- データのパーティション分割: 属性に基づきデータを分割(例: 顧客グループ)。
- 機能分解: 種類ごとに作業を分離(例: マイクロサービス化)。
これらの手法を組み合わせ、システムのパフォーマンスや堅牢性を向上させることが重要。
垂直スケーリング
垂直スケーリングでは、CPUやメモリの強化により処理能力を向上できますが、限界がある。特にFoodCoでは、書き込み競合の増加を背景にデータベースのアップグレードを繰り返したが、これ以上のスケーリングは長期的な解決にならず、コストやリードタイムも課題。
仮想化やクラウドの活用で迅速なスケーリングが可能になったものの、CPUのクロック速度の向上は限定的で、多くのソフトウェアがマルチコアに最適化されていない点が障壁。また、堅牢性向上やコスト効率の面では、垂直スケーリングより水平スケーリングが有利な場合がある。
水平複製
水平複製は、システムの負荷増加に対応するためのスケーリング方法で、システムの一部を複製して処理を分散させる。基本的にはロードバランサやキューなどを使用し、リクエストやジョブを複数のインスタンスに分配する仕組み。
この方法は比較的実装が容易で、垂直スケーリングが限界を迎えた際の次の選択肢として有効。ただし、モノリスの全体複製が必要になる場合や、スティッキーセッション負荷分散などの制約が追加される場合もある。負荷分散メカニズムの選択とその制限を理解し、コストや副作用を考慮することが重要。
データのパーティション分割
データのパーティション分割は、負荷分散のための重要なスケーリング手法。基本的にはデータの特性やキーを基に、異なるパーティション(シャード)にリクエストを振り分ける。例えば、顧客の姓や地域ごとにデータを分割することで、トランザクションの負荷を効率的に分散できる。
パーティション分割には利点と課題がある。利点として、スケーリングが容易になり、影響範囲を限定したメンテナンスや、地理的なデータ保存要件への対応が可能。一方で、均等な負荷分散の実現や、パーティション障害時のリクエスト失敗リスクへの対策が求められる。
適切な分割戦略の選定が鍵となり、単純なスキームでは負荷の偏りが生じる可能性がある。一意のIDを用いた分割は、均等な分散を実現しやすい選択肢といえる。
機能分解
機能を抽出し、独立してスケールできるようにする。既存にシステムから機能を抽出し、新しいマイクロサービスを作成することは、機能分解の標準的な例。但し、短期的な利点がほとんど見込めない。
モデルの組み合わせ
アプリケーションのスケーリングを複数の軸で考えることが重要。機能を分解し、サービスを複製して負荷を分散したり、地理的に分割してスケーリングすることで、柔軟かつ効率的にシステムを拡張できる。スケーリング方法は1つに絞らず、状況に応じた最適なアプローチを選ぶことが重要。
小さく始める
最適化の重要性とその実施タイミングについて、過度な最適化は問題の解決を遅らせ、システムを無駄に複雑にする可能性があるため、実際のニーズに基づいて最適化を行うべき。
また、システムのスケーリングや負荷対策を行う際、最初から過度に複雑なアプローチを取るのではなく、少量の作業で実験的に検証することが推奨されている。負荷テストなどを使用して、ボトルネックの特定と解決策の検証を行い、徐々に効果を確認しながら進めるべき。
CQRSとイベントソーシング
CQRS(コマンドクエリ責務分離)は、データの操作(書き込み)と取得(読み取り)を別々のモデルで処理するアーキテクチャパターン。これにより、読み取りと書き込みを独立してスケールさせることが可能になる。CQRSはイベントソーシングと組み合わせることが多く、イベントソーシングでは、エンティティの状態を現在のレコードではなく、そのエンティティに関連するイベントの履歴から投影する。
ただし、CQRSは実装が複雑であり、スケーリングの方法として採用するには注意が必要。まずはリードレプリカなど、より簡単なアプローチから試すべき。また、CQRSやイベントソーシングを使用する際は、開発者の認知的負荷が増加するため、これらのパターンを採用する価値があるかを慎重に判断する必要がある。
マイクロサービスアーキテクチャにおいては、CQRSやイベントソーシングを適用する際に、内部実装の詳細を外部のコンシューマから隠蔽することが重要。これにより、後で変更があっても柔軟に対応できるようになる。
キャッシュ
キャッシュは、パフォーマンス向上のために、結果を保存して再計算を避ける技術。
パフォーマンスのためのキャッシュ
キャッシュは、ネットワーク遅延や複数のマイクロサービスとの対話に伴うコストを削減するために有効。データをキャッシュから取得すれば、ネットワーク呼び出しを避けられ、下流サービスの負荷も軽減されます。例えば、ジャンルごとに人気商品一覧を取得する場合、結合クエリの結果をキャッシュし、キャッシュが無効になった時だけ再生成することで効率化が図れる。
スケールのためのキャッシュ
キャッシュを使うことで、システムのスケーリングが改善される。例えば、データベースのリードレプリカを活用することで、プライマリデータベースの負荷を軽減し、読み取りトラフィックを効率的に処理できる。このように、オリジンとクライアント間にキャッシュを配置することで、競合点を回避し、システムのスケーリングを実現できる。
堅牢性のためのキャッシュ
キャッシュを使用することで、オリジンが利用できない場合でもシステムの堅牢性が向上する可能性がある。しかし、キャッシュ無効化メカニズムが古いデータを自動的に削除しないようにする必要がある。そうしないと、オリジンがオフラインの際にキャッシュミスが発生し、データ取得に失敗することになる。このアプローチは、一貫性よりも可用性を優先することを意味する。例えば、Webサイトの静的バージョンをクローリングしておくことで、サイト停止時でも一定のバージョンを表示し続けることができる。
どこでキャッシュするか
マイクロサービスアーキテクチャでは、キャッシュをさまざまな場所で利用できる。最適なキャッシュの場所は、最適化の目的によって異なる。例えば、クライアント側キャッシュでは、特定のデータをローカルで保存し、再度リクエストする際に外部サービスとのやり取りを避けられますが、無効化が難しく、一貫性に問題が生じることがある。一方、サーバ側キャッシュでは、サービスがキャッシュを管理し、一貫性を保ちやすくなるが、遅延を最適化する効果は少ないことがある。また、リクエストキャッシュでは、特定のリクエスト結果をキャッシュし、後続のリクエストに対して効率的に応答する。キャッシュの使用方法は、システムの要求やトレードオフを考慮して選択する必要がある。
無効化
無効化はキャッシュからデータを削除するプロセスで、シンプルに思えるが、実装方法が多いため複雑。キャッシュの無効化方法にはいくつかの選択肢があり、最も簡単な方法はTTL(Time To Live)を使うこと。TTLでは、キャッシュデータの有効期限を設定し、その時間が経過するとデータが無効と見なされ、新しいデータが必要になる。
他の方法として、条件付きGETリクエストを使用して、リソースが変更された場合のみ新しいデータを取得する方法もある。また、通知ベースの無効化は、イベントを使ってキャッシュを更新する仕組み。これにより、古いデータが提供される可能性のある期間を短縮できるが、実装は複雑。
TTLや条件付きGETはシンプルで効果的だが、最も適切な方法はシステムや状況に応じて選ぶべき
キャッシュの黄金律
キャッシュを多くの場所で使用する際の問題点として、データの鮮度や無効化の判断が難しくなることが挙げられる。キャッシュを多く使用すればするほど、クライアントが見るデータが最新でない可能性が高くなるため、注意が必要。例えば、在庫管理システムのような場合、サーバ側キャッシュとクライアント側キャッシュがそれぞれ1分のTTL(有効期限)を持っていると、データが最大で2分古くなる可能性があるという問題が発生する。
これに対する対策として、タイムスタンプベースの有効期限を使用することで、キャッシュの鮮度を明確にすることが推奨されている。しかし、キャッシュの使用はシステムの複雑さを増すため、できる限り簡素化を目指すべきであり、最適化が必要な場合でもその複雑さを理解した上で実施すべきである。
鮮度と最適化
TTL(Time To Live)を使ったキャッシュの無効化に関して、データが変更されるタイミングとキャッシュの有効期限が重要なバランスを取る必要があることが示されている。例えば、TTLを5分に設定した場合、データが変更されてもその1秒後からTTLが切れるまでの5分間、キャッシュされた古いデータが提供されることになる。これを解決するためにTTLを短縮して古いデータの扱う期間を減らすことができるが、その分、オリジンサーバへのリクエストが増えるため、負荷や遅延が増加する。
このバランスを取るためには、エンドユーザーの要求やシステム全体の要件を理解することが求められる。たとえば、ユーザーは常に最新データを欲しがるものの、システムの過負荷を避けるためには、キャッシュの使用を適切に調整する必要がある。また、キャッシュが障害を起こした場合には、オリジンへの過負荷を避けるために機能を一時的に無効にすることも選択肢の一つ。
キャッシュの使用はシステムの理解を複雑にするため、可能な限りシンプルに保つことが推奨される。
キャッシュポイズニング:教訓
キャッシュの問題で最も悪い事態は、ただ古いデータを一定期間提供することではなく、永続的に誤ったデータが提供されること。例えば、広告企業「AdvertCorp」では、レガシーアプリケーションを新しいプラットフォームに移行する作業を行っていた。この際、キャッシュヘッダに関するバグが発生し、一部のページに適切なキャッシュヘッダが付与されないことがあった。
その結果、キャッシュが誤った情報を提供し続け、Squidを通してHTTPトラフィックをキャッシュしている環境で問題が悪化した。さらに、ユーザのブラウザのキャッシュが影響を与えることもあり、例えば「Expires: Never」と設定されたページが、キャッシュがいっぱいになるまで更新されず、最終的にページのURLを変更することで対処するしかなかったというケースが紹介されている。
このような問題を防ぐためには、キャッシュがどのように機能し、どの部分で問題が起こる可能性があるのかを理解することが重要。キャッシュの管理には、複雑さとリスクが伴うため、慎重に取り扱う必要がある。
オートスケーリング
オートスケーリングは、システムの負荷に応じてインスタンスを自動で追加・削除する手法。予測型スケーリングはトラフィック傾向に基づき事前にリソースを調整し、受動型スケーリングは負荷や障害に応じて対応する。AWSなどではインスタンス数を規定でき、障害時に自動的にインスタンスを起動できるが、注意が必要。オートスケーリングはコスト効率を向上させますが、データ収集と監視が重要。
再出発
システムが異なる規模の負荷に対応する必要が出てきた場合、初期のアーキテクチャでは不十分になることがある。垂直・水平スケーリングで対応できる範囲を超えた場合、システムの再設計が必要。例として、Giltはモノリシックなアプリケーションから再設計し、分割や新しいデータストアの導入、イベント駆動型への移行などを行った。最初から大規模なシステムを構築するのは危険で、早期に実験し、ユーザーのニーズを確認することが重要。システム変更は障害ではなく、成功の兆しと捉えるべき。
まとめ
マイクロサービスアーキテクチャにおけるスケーリングには、いくつかのアプローチがある。主な方法としては、
- 垂直スケーリング(より強力なマシンを導入)
- 水平複製(処理を複数のインスタンスで実行)
- データのパーティション分割(データ属性に基づいて作業を分ける)
- 機能分解(処理を異なるマイクロサービスで分割)
などがあり、それぞれの選択肢には適切なシナリオがある。スケーリングの目的や課題を明確にし、無理な最適化を避けることが重要。システムの複雑さが増すことを理解した上で、何を改善すべきかに絞り込むべき。
Discussion