AWSにおけるマイクロサービスのオートスケーリング -Part1
FiNC の SREチームでインターンをしている久保田です。
本記事は、弊社SREエンジニアの Son Nguyen による英語記事の和訳となっております。
1. イントロダクション
FiNCでは、機能追加、バグ修正、技術負債の解決などの開発活動の大幅な増加に対処するため、マイクロサービスアーキテクチャを採用しています。
FiNC のほとんどのサービスでは Ruby on Rails を使用しており、Amazon ECS で、コンテナ化したアプリとしてデプロイしています。
モノシリックアプリケーションと同様に、マイクロサービスも、ピーク時にはスケールアウトし、そうでない時にはスケールインする必要があります。(言うまでもなく、スケールアウトはより多くのリクエストに対応することであり、スケールインはコンピューティングリソースを解放し、コストを節約することです。)
この記事では、私たちがFiNCで採用している方法について説明します。
2. インフラアーキテクチャ
インフラアーキテクチャは大凡以下のようになっています。
リアルタイムサービスと、非同期ワーカーの両方を、別々のECSクラスタにデプロイしています。
リアルタイムサービスは、迅速なレスポンスが必要なリクエスト(主にユーザーからの要求)を処理し、非同期ワーカーは、メッセージキュー(RedisまたはAmazon SQS)にエンキューされた遅延ジョブを処理します。
3. マイクロサービスのオートスケーリング
図1に示すように、マイクロサービスは、①ECS、②メッセージキュー、③RDSの3つのコンポーネントを使用します。
したがって、マイクロサービスをオートスケーリングさせるには、これらの3つのコンポーネントをオートスケーリングさせる必要があります。
ここでは、①ECSのオートスケーリングを設定する方法についてのみ取り上げます。
次のパートでは、他の2つのコンポーネントのオートスケーリングについて説明します。
Amazon ECSでは、クラスタはEC2インスタンスの集合を含む、オートスケーリンググループによってサポートされています。
これらのEC2インスタンスとは、コンテナ化されたアプリケーションのコンピューティングリソースです。
3.1 リアルタイムサービス
同じECSクラスタ内でマイクロサービスを個別に調整するために、サービスごとに個別のECSサービスを作成します。
各ECSサービスは指定された数のタスクを実行し、各タスクには複数のアプリケーションインスタンスが含まれます。
FiNCでは、ほとんどのマイクロサービスは Ruby on Rails で構築され、Unicorn(Rack HTTPサーバ)はRailsアプリケーションがリクエストを並行処理できるようにするために使用されます。
同時ユーザー数(CCU)が大幅に増加すると、マイクロサービスへのトラフィックもそれに応じて増加し、アプリケーションインスタンスの数がすべての同時リクエストの処理に十分でない場合があります。
するとその結果、容量が不足することがあります。
このような状況に適応するためには、マイクロサービスキャパシティを監視し、適切なタイミングでスムーズに、アプリケーションインスタンスをスケールアウトする必要があります。
つまり、ECSサービスに対し、適切なタイミングでタスクの数を増やすようにリクエストする必要があるのです。
しかし、実行するタスクの数が増えると、より多くのコンピューティングリソース(CPUとメモリ)が消費されます。
これにより、どこかでコンピューティングリソースが不足することになるでしょう。
したがって、コンピューティングリソースの利用状況を監視し、EC2のインスタンスも適切なタイミングで、スムーズにスケールアウトする必要があります。
逆にCCUが減少すると、マイクロサービスへのトラフィックもそれに応じて減少します。
したがって、一部のアプリケーションインスタンスが冗長になります。その時点で、コンピューティングリソースを解放するために、アプリケーションインスタンスをスケールインする必要があります。
つまり、実行中のタスクがグレースフルに停止するように、ECSサービスにリクエストするということです。
コンピューティングリソースの使用率が一定の閾値まで低下した後、EC2インスタンスの幾つかをグレースフルに終了させて、コストを節約することができます。
以下の図2において、番号①の左側のボックスは、リアルタイムサービスクラスタをサポートする、EC2インスタンスのオートスケーリングワークフローを構成するコンポーネントを示しています。
一方、番号②の右側のボックスは、ECSサービスのオートスケーリングワークフローを構成するコンポーネントを示しています。
(1)EC2インスタンスのオートスケーリング
上述のように、リアルタイムサービスクラスタをサポートするEC2インスタンスは、スケーリングポリシーをサポートするオートスケーリンググループに属します。
オートスケーリンググループでEC2インスタンスを自動的にスケールアウトするには、ScaleOutという名前のシンプルスケーリングポリシーを作成します。このポリシーが実行されると、2つの新しいEC2インスタンスが、オートスケーリンググループに追加されます。
このポリシーの実行には、CloudWatchアラームがトリガーとなる必要があります。
そのCloudWatchアラーム(スケールアウトアラーム)を作成するには、コンピューティングリソースの使用率を適切に表すメトリックが必要です。
しかし、そのような指標で使用可能なものはありません。
そこで私たちは、 ECSクラスタによって公開されたメトリックを探し、ECSクラスタが2つの関連するメトリック、①CPUUtilizationと②MemoryUtilizationを公開していることを発見しました。
いずれのメトリックも単一のコンピューティングリソースの使用率を表すだけなので、スケールアウトアラームを作成することには使えません。
CPUUtilizationがMemoryUtilizationよりずっと高いこともあれば、その逆もあります。
したがって、これらの2つのメトリックを組み合わせて、より関連性の高いメトリックの作成を考えました。
そのために、Lambda Function(図2のλ1)を設定します。
組み合わせのロジックは次のとおりです。
cpuはCPUUtilizationを表し、memはMemoryUtilizationを表します。
この組み合わせロジックによって、CPUUtilizationとMemoryUtilizationの両方が均等に考慮されます。
Lambda Function は、戻り値をCloudWatchにCPUMemoryUtilizationNormalizedメトリックとしてパブリッシュします。
CloudWatchでこのメトリックが利用できるようになると、そこからスケールアウトアラームを作成し、ScaleOutポリシーを以下のように作成することができます。
スケールアウトとは対照的に、EC2インスタンスのスケールインにシンプルスケーリングポリシーを使用することはできません。
これは、削除されたEC2インスタンス上で実行されているタスクは、乱暴な方法でキルされてしまうからです。
これは、ECSサービスを不安定な状態にしてしまう可能性があります。この問題に対処するために、λ2とλ3の2つの Lambda Function を設定します(図2)。
λ2は定期的にCloudWatchに問い合わせて、CPUMemoryUtilizationNormalizedメトリックをチェックします。
その値が特定の閾値を下回っている場合、λ2はAmazon ECSに、ECSクラスタのオートスケーリンググループ内の最も古いEC2インスタンスに対応するコンテナインスタンスを”DRAIN”させるように要求します。
そのコンテナインスタンスのステータスは、次のようにDRAININGになります。
λ3は定期的にAmazon ECSに問い合わせて(API経由)、ECSクラスタ内のコンテナインスタンスのステータスをチェックします。
ステータスがDRAININGで、実行中のタスク数が0のコンテナインスタンスが存在する場合、λ3はオートスケーリンググループ内の対応するEC2インスタンスを終了し、必要な容量も減らします。
(2)ECSサービスのオートスケーリング
ECSでは、ECSサービスのオートスケーリングを個別に設定することができます。
たとえば、プロダクション環境でのECSサービスのオートスケーリングの構成は次の通りです。
①ScaleOutと②ScaleInの2つのステップスケーリングポリシーを作成しました。
各ポリシーには、その実行のトリガーのために、専用のCloudWatchアラームが1つ必要です。
ScaleOutポリシーがスケールアウトアラームによってトリガーされると、指定された数の新しいタスクがECSサービスに追加されます。
一方、ScaleInポリシーがスケールインアラームによってトリガーされると、指定された数の実行中のタスクがECSサービスからグレースフルに削除されます。
図6に示すように、AppInstanceBusyPercentという同じメトリックからCloudWatchアラームを作成しました。
AppInstanceBusyPercent は何を表しているでしょうか。
FiNCでは、New Relic APMを使用してアプリケーションのパフォーマンスを監視しているため、キャパシティ分析レポートでAppインスタンスビジーチャートをチェックすることで、アプリケーションがどの程度「ビジー」かを把握できます。
たとえば、次の図は、本番環境のRuby on Railsアプリケーションで使用中のAppインスタンスを示しています。
ここでは、1つのアプリケーションインスタンスが1つのUnicornワーカープロセスに相当し、Appインスタンスのビジーチャートには、Unicornワーカープロセスがリクエストを処理する時間の割合が示されています。
Unicornワーカープロセスがリクエスト処理に費やす時間が長くなればなるほど、同時に処理できるリクエストは少なくなります。
当然、サービスを迅速かつ安定した状態に保つためには、Appインスタンスビジー率を特定の閾値以下に保つ必要があります。
そのため、このメトリックを使用してScaleOutおよびScaleInポリシーのCloudWatchアラームを作成します。
これを実行するために、Lambda Function λ4(図2)を設定して、New RelicからAppインスタンスビジー率を取得し、それをAppInstanceBusyPercentメトリックとしてCloudWatchに公開します。
以下のPythonのコードスニペットは、APIエンドポイントにリクエストし、New Relicからアプリケーションインスタンスビジー率の平均値を取得する方法を示しています。
下の図8は、プロダクション環境のリアルタイムサービスクラスタ上でオートスケーリング構成がどの程度うまく機能するかを示しています。
最初のグラフは、アプリケーションロードバランサーのRequestCountメトリックの変化を示しています。(アプリケーションロードバランサーは、図2に示すように、対応するECSサービスによって使用されます。RequestCountメトリックは、ECSサービスによって正常に処理された要求の数を表します。)
2番目のグラフは、アプリケーションロードバランサーのRequestCountメトリックの変化に応じた、ECSサービスの実行中のタスク数の変化を示しています。
最後に、3番目のグラフは、ECSサービスの実行中のタスク数の変化に応じた、リアルタイムサービスクラスターをサポートするオートスケーリンググループ内の正常なインスタンス数の変化を示しています。
これらの変化は、人間が干渉することなく、自動的に起こっています。
3.2 非同期ワーカー
前述のとおり、ECSクラスタに非同期ワーカーもデプロイしています。
そのため、リアルタイムサービスクラスタと同様に、非同期ワーカクラスタをサポートするEC2インスタンスにも、オートスケーリングを設定する必要があります。
非同期ワーカーの場合、それらを孤立したタスクとして実行します。
これらの非同期ワーカータスクを維持するためにECSサービスは使用しません。
その理由は、ECSサービスが新しいタスク定義のリビジョンで更新された場合、ECSスケジューラは、SIGTERMおよびSIGKILL信号を使用して、現在の実行中のタスクを乱暴に終了させてしまうためです。
非同期ワーカーは、SIGUSR1を使ってグレースフルにキルされる必要があるのです。
図9に示すように、ECSクラスタで動作する非同期ワーカータスクを維持するためのツール、daemon_watcherを開発しました。
Jenkinsジョブは定期的にdaemon_watcherを実行します。
これは、Amazon Parameter Storeにクエリして、非同期ワーカーのオートスケーリングに関連する設定(タスク数、最小タスク数、最大タスク数、スケールアウト/スケールインの閾値、増分タスクなどの設定)を取得し、CloudWatchにクエリしてキューのサイズを取得します。
取得したキューのサイズに応じて、daemon_watcherは、増分タスクを実行することによって非同期ワーカーをスケールアウトするか、指定した数の実行中タスクをグレースフルに終了させることでスケールインします。
現在、RedisとAmazon SQSの両方をメッセージキューとして使用しています。
SQSは、キューサイズをApproximateNumberOfMessagesVisibleメトリックとしてCloudWatchに自動で公開していますが、Redisについては、キューサイズを計算してCloudWatchに公開する Lambda Function を作成する必要があります。
4. リスクとその対策
まず、リアルタイムサービスのオートスケーリングは、New Relicに完全に依存しています。
New Relic APIサーバーは常にダウンする可能性があるため、これは設計上のリスクです。(実際、New Relic APIサーバーは数回ダウンしました。)
その時点で、Lambda Function λ4は、New RelicからAppインスタンスビジー率を取得することができないため、CloudWatchメトリックのAppInstanceBusyPercentのデータが欠落してしまいます。
これに対しては、2つの対策があります。
1つ目の対策は、欠落したデータポイントを「不正」として処理するように、スケールアウトアラームを設定することです。
これにより、ECSサービスが通常通りスケールアウトすることになります。
2つ目の対策は、RequestCount、ActiveConnectionCountなどのApplication Load Balancerメトリックを調べ、サービスのビジー状態を確認することです。
これらのメトリックの値に応じて、手動でスケールアウトし、サービスを安定に保つことができます。
リアルタイムサービスと非同期の両方のオートスケーリングでは、接続数が多すぎたり、クエリのスループットが高すぎたりするため、MySQLのサービス拒否(DoS)を引き起こす可能性があります。
このようなリスクに対処するためには、事前に適切な対策を準備する必要があります。
この興味深い話題については、次のパートで議論しましょう。
5. 結論
ECSコンポーネントのオートスケールは、以下のような大きな利益をもたらします。
・ マイクロサービスは、トラフィックの増加にシームレスに適応可能
・ SREメンバーを退屈な作業(手動スケールアウト/イン)から解放
・ ピーク外(特に深夜)に仮想マシンの数を減らし、AWSコストを削減
EC2インスタンスのオートスケーリングを設定した時点では、AWS Fargateは誕生していませんでした。
しかし、今ではFargateは使用可能となっています。
AWS Fargateを使用すると、コンテナを実行するために仮想マシンのクラスタをプロビジョニング、設定、およびスケールする必要がなくなります。
これにより、サーバーの種類を選択したり、クラスタをいつスケールするかを決定したり、クラスタのパッキングを最適化する必要がなくなります。
AWS Fargateでは、サーバーやクラスタとやり取りしたり、考えたりする必要がなくなります。
Fargateでは、アプリケーションを実行するインフラストラクチャの管理ではなく、アプリケーションの設計と構築に専念できます。
この便利な機能を使うため、私たちはECSクラスターをEC2起動タイプから、Fargate起動タイプに移行する予定です。
うまくいけば、AWS Fargateでは更にコストを節約できます。
オートスケーリングは、AWS上のマイクロサービスを管理するSREメンバーにとっては非常に難しい課題です。
次のパートでは、残りの2つのコンポーネント(メッセージキューとRDS)のオートスケーリングについて説明します。
乞うご期待。
現在FiNCでは、成長中のSREチームにジョインするエンジニアを求めています。
是非Wantedlyをチェックしてみてください。