「ハイトラフィック運用 〜スループットの向上を目指して〜」という話をしました #devio2020

2020.06.17

こんにちは(U・ω・U)
AWS事業部の深澤です。

さて弊社では昨日よりDevelopersIO2020 〜connect〜というイベントが開かれています。

その中で僕は「ハイトラフィック運用 〜スループットの向上を目指して〜」という話でセッションさせていただいたので本ブログではその発表した内容を資料と共にご紹介します!

動画

資料

セッション内容

今回扱うこと、扱わないこと

  • 今回は観点をメインに紹介します。
    • それぞれのエンジン等による細かい違いやチューニング方法には触れません。
      • 例:MySQLとPostgras等
  • メトリクスはCloudWatchのものをご紹介します。
  • 今回はあくまで概要と基本です。
    • 障害原因やチューニング方法に鋼の弾丸はありません。
    • ご紹介した以外にも有益なメトリクスはありますので、是非今回を機に他のメトリクスも確認していただけたら嬉しいです。

最高のエクスペリエンスを届けるためには

せっかく良いコンテンツを提供しているのに、レスポンス速度が足枷になってしまってはもったいないです。現代のwebシステムではそういったスループットや応答速度も重視されるようになってきました。

ここでは一般的なwebシステムの構成をご紹介し、この先この中からどうやってボトルネックを特定したりチューニングしたりしていけば良いのかというお話をしていきます。

目標を定めよう

まずは目標から決めましょうという話をしました。まず目標を定めるにはユーザの目線に立つことが大切です。ユーザから見て今どんな問題が起きているのかです。

こういったことを決めるのに外形監視が非常に役に立ちます。先日AWSのCloudWatchでは「Amazon CloudWatch Synthetics」というサービスをリリースしました。

この結果を受けてレスポンス速度が徐々に低下してきているのであればチューニングが必要になりますし、SLOを下回っているとか5XXエラーが帰ってきてしまうとかであればこれは障害対応になると思います。

CloudFrontの確認

まずはユーザに一番近いところから見ていきます。

この時、まずはシステムにアクセスが到達しているか確認しましょう。DNSで障害が発生した場合にはそもそもCloudFrontまでアクセスが届きません。

CloudFrontと併用してlambda@edgeを採用している方もいらっしゃるかと思いますが、lambda@edgeでボトルネックになりそうな箇所をまとめてみました。以下のような点注意して下さい。

  • タイムアウトは発生していないか?
    • 外部APIの遅延、リクエストbodyの読み込み等でタイムアウトが発生
  • レスポンスサイズが制限にかかっていないか?
    • リクエストによって動的にサイズが変わる場合は注意
  • 同時実行数の制限にかかっていないか?

この他にもいろいろと制限がありますので、改めてご確認をお勧めします。

CloudFront ディストリビューションメトリクス

CloudFrontでは通常のメトリクスに加えて追加料金を支払うことでディストリビューションメトリクスというものが見れるようになります。

これを追加することによってHTTP ステータスコード別のエラー率が取得できるようになるので今のシステムで起きている問題を把握したり切り分けたりするのに非常に役立ちます。発生しているHTTP ステータスコードが判明したら以下のページを参考にトラブルシュートしてみましょう。

最後にボトルネックはCloudFrontなのか、それよりも下なのかを切り分けていきます。

ロードバランサレイヤーの確認

続いてロードバランサレイヤーの確認に移っていきます。

ここで確認すべきは、「問題が発生しているのはロードバランサか?その下か?」です。この時、以下のメトリクスが上がっていればその可能性があるという指標になるでしょう。

  • HTTPCode_ELB_4XX_Count
  • HTTPCode_ELB_5XX_Count

ロードバランサも裏側はインスタンス(サーバ)ですので、急なアクセスには耐えられない可能性があります。その場合にはサポートを通して暖機申請を行うことを検討しましょう。

ちなみにNLBの場合は暖機申請が不要となっています。

Q. なぜ NLB だけ暖気不要なのでしょうか?
A. AWS Hyperplaneと呼ばれる特殊なAWS独自の負荷分散技術を用いているためです。
AWS Hyperplaneに関しては、こちらの資料のP17 – P21をご確認ください。

とはいえ個人的な経験上、ロードバランサがボトルネックになることはあまりありません。HTTPCode_ELB_4XX_CountとHTTPCode_ELB_5XX_Countを確認した後には以下のトラブルシューティングの内容も確認しましょう。

またこちらのブログは実際にロードバランサで502が発生していても、実際の原因はロードバランサではなかったというケースを分かりやすく解説しています。併せて参考にしてみて下さい。

最後にロードバランサ下のホストが有効になっているか確認しましょう。ALBを利用されている場合にはターゲットグループのヘルスステータスがhealthyになっているかどうかで判断できますし、CloudWatchのメトリクスであればUnHealthyHostCountが上昇しているかどうかで確認が行えます。
非常に稀ではありますが、ロードバランサが原因でヘルスチェックが失敗することもあります。ヘルスチェックには理由コードというものがあるのですが、こちらがElb.InternalErrorになっているケースです。詳しくはターゲットグループのヘルスチェックをご参照下さい。

アプリケーションレイヤーの確認

ロードバランサがボトルネックではなかった場合、アプリケーションレイヤーの確認へと移っていきます。

EC2, ECS

アプリケーションレイヤーで確認すべきことは、問題が起きているのがサーバの中なのか外なのかを確認することです。まずはCPU使用率の確認を行いましょう。EC2ならCPUUtilization、コンテナならContainer Insightsを用いることでコンテナやタスク別の監視が行えて非常に便利です。Container Insightsについては以下のブログが非常に参考になります。

CPU使用率を確認し、ほぼほぼ100%で張り付いている場合にはサーバの中がボトルネックになっている可能性が高いです。実際にスケールさせてみるとそれがはっきりします。

インスタンスのスケールを行うことで、スループットが向上しインスタンスのCPU使用率も下がったなら、それはインスタンスがボトルネックなのでスケールアップもしくはスケールアップを検討して良い状況です。一方でインスタンスのCPU以外がボトルネックになっている可能性もあります。この場合インスタンスのCPU使用率は100%にはならないのですが、スケールさせてみるとスループットは向上するでしょう。こういった事象が確認されたらインスタンスの中に原因がある可能性が高いので、インスタンスに搭載されているアプリケーションが利用しているミドルウェアのチューニングを検討して下さい(スレッド数を増やす、マルチプロセスを利用する等)。

AWS Lambda

さて、最近ではアプリケーションを搭載するサーバにAWS Lambda(以降、lambda)を用いられている方も非常に多いかと思います。lambdaの場合はEC2のような常時稼働するインスタンスではないので、チューニングの勘所が少し変わってくるのですがまずはシンプルにエラーやスロットリングが発生していないかを確認しましょう。

最近、Provisioned Concurrencyと呼ばれるものがリリースしました。これを用いることでlambdaのコールドスタートがボトルネックになっていた場合、それを回避することができます。

lambdaはコンテナ実行時に、デプロイパッケージのダウンロードや解凍、関数を実行するためのランタイム環境を整備するといった処理(初期化処理)が走りlambda実行コンテナが用意されるのですが、これをコールドスタートと呼んでいます。一定時間lambdaを呼び出さないとlambdaのコンテナは破棄されて、また呼び出された際にlambdaの初期化処理が行われてやや処理が遅れるといった事象が発生していました。

しかし、このProvisioned Concurrencyと呼ばれるサービスを利用することで実行に必要な分のlambdaを一定数プールしておくことが可能になったのです。追加料金が発生するものの、lambdaのスループットを向上させる上で非常に重要な機能になるでしょう。詳しくは以下のブログが参考になります。

リソースの割り当てを強化するのも有効です。lambdaではリソースの調整はメモリで行うのですが、このメモリを増やす事でCPUやネットワークの帯域が強化されます。ですが調節可能なのはメモリのみのため、どのくらい割り当てるのが最適なのかというのを見つけるのはなかなか困難です。最近ではこのメモリ割り当てを自動で最適化するためのツールも出てますので良かったら試してみて下さい。

パッケージのサイズを小さくすることも有効です。先ほどもご説明した通り、lambdaの初期化処理ではデプロイパッケージのダウンロードや解凍が走るためパッケージのサイズによってコールドスタートの時間が変わってきます。このことについてはAWSの公式ドキュメントでも、パッケージサイズを可能な限り小さくすることが推奨されています。

次は少しマニアックなチューニング方法にはなるのですが、handler関数の外に再利用可能なアセットを置く事で処理時間が短くなることを期待できます。詳しくは弊社藤井のブログが大変参考になります。

簡単にご説明させていただきますね。まずは以下のコードを参照下さい(上記ブログから引用したものです)。

import boto3

# この処理と変数の値は使いまわしても問題なし
dynamodb = boto3.resource('dynamodb')

def lambda_handler(event, context):
    table = dynamodb.Table('use-table-name')
    
    # 以下略

lambdaでは実行する関数を一つ指定しその関数から処理を始めますが(上記コードではlambda_handler関数がそれに当たります)、それ以外の処理はコールドスタートの時にしか実行されず、再実行の際には再利用が行われます。この挙動を利用し、再利用可能なアセットをhandler関数外に置く事で再実行時に速度面での最適化が期待できるということになります。

データストアレイヤーの確認

アプリケーションレイヤーでボトルネックが確認できなかった場合はデータストアレイヤーの確認へと移っていきます。

RDS

まずはDBからみていきます。まずはデッドロック等が発生していないかのご確認をしましょう。Auroraの場合には以下のメトリクスでトランザクションがスムーズに流れているか確認が行えます。

  • BlockedTransactions
  • Deadlocks

もしDeadlockが発生していた場合には手動で解除し、その後、原因調査しましょう。例えばMysqlであればSHOW ENGINE INNODB STATUSを見れば、どのようなDeadlockが発生していたのか後追いで確認できるはずです。

続いてはレイテンシの確認です。各種レイテンシを確認して何となくどのオペレーションが影響を与えていそうか判断しましょう。Auroraの場合以下のメトリクスを確認可能です。

  • DeleteLatency
  • InsertLatency
  • UpdateLatency
  • SelectLatency
  • CommitLatency
  • DDLLatency
  • DMLLatency

またRDSにはパフォーマンスインサイトと呼ばれるモニタリングサービスがあります。

こちらを有効にしていただくことでCPUやIOがボトルネックになった場合やスロークエリの検出がしやすくなります。以下の点をご確認いただき、問題がないようであれば導入を前向きに検討して良いでしょう。(情報は2020/06/17執筆現在のものです)

  • パフォーマンスインサイトの有効化/無効化はインスタンスの再起動を伴う
  • T系インスタンスを利用するAurora MySQL、Amazon RDS for MariaDB及びMySQLの場合へのパフォーマンススキーマ有効はサポートされていない
  • 並列クエリが有効になっている Aurora MySQL DB クラスターではサポートされていない
  • エンジンが対応しているかどうか
    • MySQL と互換性がある Amazon Aurora バージョン 2.04.2 以降の 2.x バージョン (MySQL 5.7 互換)
    • MySQL と互換性がある Amazon Aurora バージョン 1.17.3 以降の 1.x バージョン (MySQL 5.6 互換)
    • PostgreSQL との互換性がある Amazon Aurora
    • Amazon RDS for MariaDB バージョン 10.4.8 以降の 10.4 バージョン、バージョン 10.3.13 以降の 10.3 バージョン、バージョン 10.2.21 以降の 10.2 バージョン
    • Amazon RDS for MySQL バージョン 8.0.17 以降の 8.0 バージョン、バージョン 5.7.22 以降の 5.7 バージョン、バージョン 5.6.41 以降の 5.6 バージョン
    • Amazon RDS for Microsoft SQL Server (SQL Server 2008 を除くすべてのバージョン)
    • Amazon RDS for PostgreSQL バージョン 10、11、および 12
    • Amazon RDS for Oracle (すべてのバージョン)

スロークエリのチューニングには、実際にインスタンスに入り実行計画(EXPLAIN)を用いるとより詳細な情報がわかるかと思います。このスロークエリを含めログを転送しCloudWatch Logs等に出力しておくと非常に障害や問題の切り分けがしやすいのでオススメです。

リソース面は先ほどのパフォーマンスインサイトでも確認が行えますが、拡張モニタリングと呼ばれるものを有効にするとより詳しい情報が見れます。

これを用いることで例えば、

  • cpuUtilization
  • diskIO
  • memory
  • network

のより詳しいメトリクスが得られます。通常のCloudWatchのメトリクスでもこれらを確認することもできますが拡張モニタリングはインスタンスの中で実行したエージェントから情報を収集するためより詳しい情報を得ることができます。CloudWatchで収集しているメトリクスはインスタンスを実行しているハイパーバイザから見ているメトリクスなので若干のオーバヘッドが含まれた値となりますしCPU使用率の場合にはインスタンス上のさまざまなプロセスやスレッドがどのようにCPUを使用しているかを確認することができます。パフォーマンスインサイトとは異なり、有効時に再起動はかかりません。対応しているデータベースエンジンは次の通りですので、追加料金が発生することをご検討の上問題なさそうであれば導入を推奨します。

  • MariaDB
  • Microsoft SQL Server
  • MySQL (バージョン 5.5 以降)
  • Oracle
  • PostgreSQL

これらを確認いただき、コネクション数が張り付いていたりCPU使用率が高い状態が慢性的に続く、もしくはボトルネックになっている場合はスケールアップやスケールアウト(レプリケーションを行いReadとWriteを分けるなど)の対応が必要になります。
またスロークエリが出た場合やDeadlockが定期的に発生している場合には実行計画を確認しながらのSQLの改善が必要になってきます。

DynamoDB

DynamoDBは他のデータベースレイヤーのサービスとは毛色が異なりインスタンスがユーザに見えないフルマネージドなサービスになっています。まずはDynamoDBがどのように負荷を分散しているか基本をおさらいしましょう。

DynamoDBではテーブルを作成する際にパーティションキーが必須になります。このパーティションキーを用いてDynamoDBでは負荷を分散しているのですが、パーティションキーのハッシュ値を算出しそのハッシュ値によって保存するディスク(パーティション)を分けているのです。なので同じパーティションキーでデータを保存し続けた場合、ハッシュ値の計算も同じになるので同じディスク(パーティション)に保存され続けていくことになります。
この場合の対処方法としては、

  • そもそも偏りが出そうなキーをパーティションキーに設定しない
    • ユーザIDなど一意になる物が望ましい
  • キーにランダムな値を付けて算出されるハッシュ値をバラけさせる

のが良いでしょう。

続いてDynamoDBの料金体系別に発生し得るボトルネックを考えていきます。DynamoDBには以下2つの料金体系があります。

  • オンデマンドキャパシティーモード
  • プロビジョニング済みキャパシティーモード

オンデマンドキャパシティモードは使った分だけの料金がかかるプランです。特に事前のキャパシティプランニングが不要なところが最大のメリットですが、30分以内にピークの2倍を超えるようなアクセスがあった場合に対応できないため、こちらがボトルネックになる可能性があります。

ただし、30 分以内に前のピークの 2 倍を超えた場合、スロットリングが発生する可能性があります。

続いてプロビジョニング済みキャパシティーモードは事前に発生するキャパシティを予測し使う分を定額で払うプランになります。こちらは事前に割り当てた以上のトラフィックが発生した場合にボトルネックが発生します。この際以下のメトリクスで異常を検知することが可能です。GSI(グローバルセカンダリインデックス)にもキャパシティユニットを設定している場合は同様に注意しましょう。

  • ThrottledRequests
    • プロビジョニング済みのスループットキャパシティを超過すると増加するメトリクス
  • ReadThrottleEvents
    • 読み込みでスループットキャパシティを超過した場合、増加
  • WriteThrottleEvents
    • 書き込みでスループットキャパシティを超過した場合、増加

最後にDAXを用いることでDynamoDBの負荷を下げてチューニングする方法を提案します。

Amazon DynamoDB Acceleratorが正式な名称です。こちらはDynamoDBの前段に置くキャッシュサーバになりますので、読み込みの際にキャッシュが効けばシステムのスループットが向上することが期待できます。書き込みが行われて、その後キャッシュサーバ(DAX)に反映されるというその仕組み上、結果整合性のある読み込みでしか利用できないという点にご注意下さい。強力な整合性のある読み込みの場合は書いたデータがすぐに読み込める必要があるためキャッシュサーバはスルーして直接DynamoDB参照となります。また、VPC内からしか利用できないのでオンプレ環境からDynamoDBのみを使っているという場合にもDAXは利用できませんのでご注意下さい。

ElastiCache

キャッシュサーバとしてはこちらのElastiCacheを用いているという方が多いかもしれません。キャッシュの観点ですが、保存したキャッシュが正しく効いているかを確認しましょう。キャッシュサーバにアクセスが届いているものの、キャッシュが全然拾えていないという場合にはキャッシュサーバに確認しにいっている分、タイムロスとなりますのでレスポンスの遅延に繋がります。これらは次のメトリクスをご確認いただくことでみることができます。

  • CacheHits
    • メインディクショナリで読み取り専用のキー検索に成功した数。Redis INFO の keyspace_hits 統計から算出されます。
  • CacheMisses
    • メインディクショナリで読み取り専用のキー検索に失敗した数。Redis INFO の keyspace_misses から算出されます。

  • CasHits
    • キャッシュが受信し、リクエストされたキーが見つかって Cas 値が一致した Cas リクエストの数。
  • CasMisses
    • キャッシュが受信したが、リクエストされたキーが見つからない Cas リクエストの数。

オンメモリDBなのでインスタンス(以降、ノード)のメモリには十分なゆとりがあることを確認して下さい。Redisの場合には予約メモリと呼ばれるものがあり、特にこちらの数値を変更していない場合には最大搭載メモリの75%程度までしか使えない点にご注意下さい。予約メモリとはスナップショットを取得するなど、本来の使い方(キャッシュの出し入れ)とは別にシステム側で処理が必要になった際、使われるメモリスペースです。

ノードのメモリがどのくらい使われているかはBytesUsedForCacheというメトリクスでも確認可能です。もしノードのメモリが足りなくなった場合にはEvictionという事象が発生する可能性があります。

メモリ溢れで発生し得る事象についてはこちらのブログにまとめてあります。

また、稀ですがCPU使用率がボトルネックになることもあります。Redisの場合はシングルスレッドで動作するという特性上、CPUUtilizationメトリクスをコア数で割った分しか見れないという点にご注意下さい。例えばCPUのコア数が4つのノードを用いている場合にはCPUがボトルネックになったとしてもCPUUtilizationメトリクスは25%程度までしか上がらないということです。このように多くのコアを搭載したノードを監視する場合にはEngineCPUUtilizationの方が負荷を確認しやすいのでこちらを確認しましょう。
これらをご確認いただき、以下の場合にはスケールアップ、もしくはスケールアウトを検討して良いでしょう。

  • キャッシュヒットに問題なし
    • メモリ使用率が逼迫している
    • CPU使用率が逼迫している
    • Evictionが発生している

これに該当しない場合、TTLの調整やキャッシュ利用の見直しを行いましょう。

最後に

セッションの最後には次のようなことを紹介させていただきました。

せっかくAWSというクラウドを利用しているのでどんどんボトルネックは取り除いて行ってスケールで解決できる環境を用意しましょう!皆様の大人気サイトが益々繁栄することを祈ってます。最後までお読みいただきありがとうございました!

以上、深澤(@shun_quartet)でした!