Webアプリ負荷試験ガイド

Webアプリ負荷試験ガイド

目次

前置き

Webアプリの負荷試験について書きます、それ以外の事は取り扱っていません。

一般的な Web/RDB/KVS なアプリを前提として書いています。

数万DAU程度でサーバ数十台な規模感を想定しています。

各個別の項目についてはざっくり書きますが掘り下げてはいません。

ざっくり試験の流れや注目点を書いた手引書的なものを目指して書いています。

新規プロジェクトでリリース前のものを想定して書いていますが、まともに負荷試験をしていないプロジェクトや稼働中のプロジェクトで高負荷に陥ってる場合にも参考になると思います。

時間がない人向け要約

  • スケジュールは十分に確保
  • サーバ1台に負荷掛けて限界点を調べた上で複数台に負荷掛ける

about me

六本木の会社で日々Webアプリのトラブル対応や負荷試験の監修とかしてます。

何故負荷試験を行うのか

状況によって異なりますが、凡そ次のような目的で実施するはずです

  • 大前提として目標rpsを出すため
  • アプリケーションのパフォーマンス計測、ボトルネックを調査するために行う。
  • 複数台構成で並べた的に適切にスケールするかの確認。
  • 許容されるレスポンスタイムで目標のrpsをさばくために何台のサーバが必要となるかを試算・計測する。

負荷試験ツール

負荷試験を行うにあたって知っていると便利なアプリ・サービス

負荷掛けるツール

Apache Benchは複雑なシナリオには不向きですが、単純な参照系APIの簡単な試験には使ったりします。*1

Jemeterは定番で負荷試験のシナリオは大半Jmeterで出てきます。

私の最近のおすすめはLocustです、シナリオが書きやすく簡単に分散環境で実行できます。

数万DAU規模の負荷を掛けるのであれば最初から分散実行を想定して下さい。

負荷計測

  • sar/top/iostat/etc..
  • Grafana/Prometheus
  • Datadog

cliはお手軽ですが記録出来てあとから見直せるものが良いです。 手に馴染んでるものなら何でもいいと思います。

負荷の可視化

Newrelic APMがお勧めです、特にphp環境では特別な改修も不要で導入するだけで非常に詳細な情報を取ることが出来ます。

負荷試験の流れ

よくある駄目な流れとして、作成・動作確認程度しか行っていないシナリオで一度に複数台のサーバを並べて1度だけ(短いスケジュールで)流そうとする人たちが居ますがお勧めできません。 *2

負荷試験は基本的に次のような流れで行ってください

  • シナリオ作成 参照
  • サーバ1台に対して掛けて1台辺りのrps/response timeを計測 参照
  • サーバを数台並べてresponse timeが悪化することなくrpsがスケールすることを確認
  • サーバを数十台並べてDBやKVS等のボトルネックを計測する 参照

これらの工程の合間合間にシナリオやプログラムの修正作業が随時入るはずです。

最初から複数台並べて大量の負荷を掛けようとすると、適切なrps/response timeの算出が大変ですし、複数台故の問題なのかの切り分けも大変になります。

まず1台で問題がないことを確認出来たものを複数に対して掛けてください。

負荷試験スケジュールについて

上でも触れていますが、1度流しただけで完了できるスケジュールが多いです。

一度で問題なくテストが終わり、目標のrpsを達成出来る前提となります。

2度目以降の試験であれば問題ないのですが、1度目であればそのようなスケジュールを取るべきではありません。

1度目の試験ではシナリオを正常に流せることが希です、多くの場合シナリオ改修に時間を取られます。

シナリオ自体が正常に動作したとしてもその状態で満足な負荷を掛けられることも希です、サーバ設定やプログラム改修を行う必要があります。

改修を行えば計測をやり直す必要があります、個別のコントローラへの改修であれば工数は少ないかもしれませんが共通部分に手を入れた場合は最初から全て計測の必要が出てくる事もあります。

そして改修計測は繰り返し行われます。

負荷試験の流れで解説した各項目1w確保すべきです、シナリオ改修を繰り返すことはめったにありませんが、改修・計測フェイズは何度繰り返すかはアプリに依存します。

ここまでの流れを読めば解るように、負荷試験をリリース直前に配置しないで下さい。もし最後に持ってくるのであれば十分な期間を設けて下さい。

大半のプロジェクトではプロジェクト大詰めの最後に負荷試験を配置します、それも1wのようなスケジュールで用意されることが多いですが、今までの流れを読めば解るようにそのようなスケジュールは最初から破綻していることが多いです。

注目すべきポイント

シナリオ作成

シナリオは個別の案件に依存することが多くざっくり述べるのは難しいのですが、殆どのシナリオで注意できる点をいくつか述べます。

本項目と併せて アプリの正常性の確認 も確認して下さい。

アカウント情報は自動生成出来るようにする

csv等で一定のユーザを与えたりするケースが見受けられますが、アカウント情報がスケールしないことが多いです。

100人データを作って1万人を想定する場合はそれらを使いまわしたりしますが、同じレコードに対してLockを取って失敗したり、同一の情報を参照するためキャッシュが想定より効いてしまったりします。

例えばプレゼントBox肥大化の検証等で同一のユーザで繰り返す必要があるシナリオでない限りはユーザデータは自動生成出来たほうが良いです。

つまりアカウント情報については リストで受け取る or 自動生成する の二通りで実行できることが望ましいです。

UID発行=>チュートリアル実行=>初回ボーナス付与、のように正常なフローで生成出来る場合は良いのですが、デバッグツール(API)等を使って前記のシーケンスを一度に実行したりする場合は負荷に気をつけてください。 そのような場合はsetup(pre-warmup)等で計測とは無縁の場所で発行すべきです。

DB分割を行ってる場合はDB分割を意識したシナリオを用意する。

前項目と若干被るのですが、シャードに対して適切に負荷を掛けられない場合があります。

例えばUserDBをN分割している場合に十分に散っていないユーザIDのリストを与えて試験を行った場合、特定のDBにしか負荷がかかりません。

上記のような場合は前項目のように自動生成出来れば自動生成ロジックが間違ってない限りは分散するはずです

全ての項目について適切に分散してデータを生成出来ればよいのですが、どうしても自動生成しにくいデータもあるはずです。例えばギルドはどうしても固定で生成しなくてはいけない(難しい)場合にはギルドIDで分散してるのであれば適切に散るようにデータを意識して下さい。

負荷試験

jmeterやlocustなりの負荷試験元の監視を忘れず行って下さい。

数千DAU程度の負荷なら良いのですが、万を超えるオーダーの場合よく負荷掛ける元が詰まってる事があります。

memory不足は負荷ツールがエラーを吐いて停止することが多いですが、CPUやTCP接続数不足によるスローダウンはエラーにならないことが多く見落としがちです。

http or https

開発時でもhttpsを推奨しますが、負荷試験時は可能であればhttpの利用をお勧めします。

負荷掛ける側のCPUに影響したり、tlsがLB終端だとしても接続がstickyされてしまって特定のLBに偏ったり等が起きたりします。

理想はhttp/httpsどちらでも負荷を掛けることです。

サーバ1台

必ず本項目の前に前の2項目に目を通して下さい。

1台ではrdb/kvsに問題が起きない前提で書いています。

ここではLB経由しない状態で掛けるべきです、LB経由したときに問題が起きた場合の問題の切り分けに役立ちます。

サーバ単体での負荷

Webサーバは基本的にcpu usage/load averageを見ましょう。

ざっくり cpu usageは100%未満でload averageはコア数と同程度までとすればよいはずです。

cpu usage/load averageが上記まで到達しない状態でresponse time/rpsが頭打ちするときは次を確認してください。

  • アプリの実行数が上限に達していないか

phpならapache/php-fpmだったり、rubyならunicorn等それぞれの言語のworkerのprocess/thread数です。

laやcpuが余ってる状態なら増やしましょう。

rpsが非常に出てたり、バックエンドへの接続が非常に多いようなアプリの場合TCP数が上限に達することがあります。

port rangeの拡張や net.ipv4.tcp_tw_reuse=1 にして対応しましょう。

都度名前解決してしまって問題になる事が多いです。

適切にDNSキャッシュを導入したり . 終わりにして無駄な検索を減らしたりすることを推奨します。

パブリッククラウドで顕著に現れますが、オンプレでも起きうる問題です。

最近ではコンテナが流行りで小さめのインスタンスを大量に並べるパターンも多く、以前より問題となりやすいです。

アプリの正常性の確認

試験用データが間違っていたり、開発環境と違い複数のユーザで同時実行した際の不整合等でアプリが正常に動作してない事が多々あります。

1件1件シナリオ作成時は画面を見ながら作るので良いのですが、負荷試験はheadlessで実行するため異常に気付けず試験を続けてしまってることが多いです。

これらを回避するために次の点に気をつけて下さい。

  • response statusが問題ないか(まれにstatus:200でも中身はerrorだったりします)。
  • レンスポンスサイズが一定ではないはずの応答結果(例えばmypage)が常に同じcontent-lengthで返っている。
  • シナリオで戻り値をちゃんと検証する

戻り値の検証は割と漏れています、特にjmeterではサンプラーエラー後のアクションのデフォルトが続行となっているため見落としやすいです。

サーバ複数台

ここに来るまでにWebに関しては既にある程度確認できてる状態のはずなので、KVS/RDBの様子を見ます。

基本的にKVS/RDBは可能であれば持続的接続を利用するようにして下さい、高負荷の場合では接続数/cpu usageが問題になることが多く大幅に負荷を削減することが可能です。

持続的接続を導入した場合、KVSであれば大きく問題になることはありませんがRDBの場合はトランザクション周りで問題が出ることがあるため、トランザクションを利用している場合は導入に注意が必要です。

それでも導入は検討すべきで、持続的接続に関する資料としてはphpになりますが次の資料がお勧めです

RDBへの持続的接続の導入に関しては上記のように懸念事項も多く、オンプレ時代はmayぐらいで出来ればば好ましいぐらいの感触でしたが、パブリッククラウド全盛の今ではshouldぐらいの感触です、費用で泣きたくなければmustでも。

専用のミドルウェア(mysql routerやtwemproxy)でもいいですが、多くの場合各言語に実装が用意されているはずです。

特に最近ではパブリッククラウドを利用することが多くネットワーク品質がオンプレに比べて低いです、持続的接続を利用すれば大幅なパフォーマンス改善が見込めます。

KVS

memcached/redis を使い分ける前提で書いています。

Memcached

サーバプールを増やすことでキャッシュ容量やコマンド実行性能はスケールはできるはずなのですが、往々にして利用方法で問題があり増やせないことが多いです。

接続数やコマンドが正常に分散しているかを確認してください。

特定Keyの利用が偏っていて特定サーバに負荷がかかるような利用をしている場合、1ノード辺りのスペック上限が頭打ちになります。

良くあるキーの偏りとしては次のような物が上げられます

  • マスタデータ
  • メンテナンス情報
  • 最新イベントの情報
  • ユーザ情報をキャッシュさせていてランキング上位ユーザの情報

これらは適切に散らせるようにするか、可能であればlocal cacheを利用すべきです。

偏りの調査は tcpdump でkeyの集計や、memcached側でcmd rateや転送量を注視して下さい。

Redis

Redisはマスタに関してはスケールアウトが出来ません、Redisに格納するようなデータはデータ分割にも不向きなことが多く基本的にアップ戦略になってしまい上限が比較的早く訪れます。

ほんとうにそのデータはRedisに格納する必要があるのか?Memcached等他のストレージで済まないか検討すべきです。

良くあるパターンとしてランキングでRedisを利用しているのでそのままキャッシュストレージとしてRedisを使ってしまうことがありますがお勧め出来ません。

多くの場合メモリサイズよりCPUの頭打ちが訪れるはずです。

メモリサイズで問題になる場合は実データを丸々置かないようにし、、基本的に数値や格納先のid以外は保存しないようにすることによって削減出来るはずです。

接続数やmultiコマンドやluaによってCPUの頭打ちが訪れることが多いと思います。

非常に良いまとめだと思うので参考リンクを貼っておきます

RDB

数万DAUを想定しているためmaster複数slave構成前提で書いています。

Masterに対する負荷は水平分割出来ない限りスケールアップでしか対応できません、数千DAU程度であれば垂直分割で対応可能ですが、万を超えるDAUを想定する場合は水平分割を必ず導入して下さい。

RDBで問題点をざっくり分けると cpu usage/接続数/メモリ/ストレージ容量/ストレージ性能 となります、そして Master/Slave 、Write/Read負荷となります、それぞれで問題を分けて考えるべきです。

多くの場合まず最初に問題になるのは接続数/cpu usageです、slaveに関しては並べて対応可能です。

上記を解決した場合次に問題となるのはwrite性能です、write性能に関しては次のような方法で対応できます

  • bulk insert
  • DB分割
  • 非同期書き込み*3

bulk insert/DB分割は必ず導入すべきです。

非同期書き込みはアプリの作りにも左右されることが多く、製造初期から検討しないと前2個に比べて導入が難しいです。

次に問題となるのはメモリ及びストレージです、基本的には水平分割で対応して下さい。

問題になりやすいDB
  • マスタ系DB

参照負荷が問題になります。

必ずキャッシュすべきです。

slaveを追加での対応も可能ですが、キャッシュを適切に利用して対応すべきです。

  • ユーザ系DB

数万DAUを想定する場合は必ず水平分割を導入すべきです。

  • ログ・調査系DB

ユーザ系DBは分割しているのにログや調査用DBが水平分割されていなくて問題になるパターンをよく見ます。

そもそもRDBへの保存はお勧めできません、これらは可能であればログファイルに書き出してtd-agent等で送信出来るようにするのがベターです。

エンジニアリソースや慣れたものを使いたいなどの理由があってRDBを使いたい場合はユーザデータと同様に分割をすべきです。

キャッシュの話

キャッシュするレイヤーは大まかに分けると次のように考えられます。

  • CDNやproxyレベルのHTTP層
  • アプリ層
    • Appサーバローカル内での格納
    • Appサーバ外での格納
  • RDB

大前提

キャッシュは可能な限りして下さい、10秒や5秒最悪1秒でも良いです。

1秒でもキャッシュ出来れば数千リクエストされたとしてもストレージへのアクセスは1で済みます。

キャッシュの削除(管理)をし始めると難易度が跳ね上がります、削除ではなくTTLを短くして(更新したら削除ではなく、前記の秒単位キャッシュを検討する)対応できたほうが難易度は低くお勧めです。

注意すべき点

キャッシュすべきデータは基本的に整形済みデータを保存して下さい。

多くの場合dbのデータをそのまま保存して取り出す度に整形していることが見受けられますが、整形のコストも馬鹿にはならないため整形済みデータを保存して下さい。

CDNやProxyレベル

数万DAUを想定する場合CDNは必ず導入しましょう。

共通で使われるデータは必ずキャッシュすべきです、マスタデータやメンテナンスフラグで用いられるjsonだったりhtmlで返される更新情報等です。

頻繁に更新されない静的なリソース類、もしくは動的なものでもリアルタイム情報でなく、ユーザ情報を必要とせず生成できるのであればCDN/Proxyでのキャッシュを検討しましょう。

この層でキャッシュができればapp側にリクエストがほぼ行かなくなります。

local cache or remote cache

マスタデータ等の頻繁に参照しデータサイズの大きなものはローカルキャッシュに格納すべきです。

マスタデータをリモートに格納するとkeyの偏りやネットワーク転送量が問題なることが多くおすすめ出来ません、そしてローカルに格納することでそれらの問題が発生しにくなり難易度が下がります。

キャッシュヒット率を考慮し保存先を決めて下さい。

一般的なLBでRoundRobinで振り分け構成の場合、各ユーザに属するデータはローカルに保存するとヒット率が悪いためリモートサーバに保存すべきです

local cache or memory cache(in app cache)

ローカルに保存出来てればおおよそ問題は無いのですが、問い合わせ回数が数十回を超えて多い場合はアプリ内部に保持したほうがいいです。

ローカルに保存できていたとしても場合によっては 10-20msec程度掛かってる事をよく見かけますが(集計関数ではなく単純なget等で時間がかかっている場合)同じ実装でもメモリ内キャッシュであれば数msecで済むことが多いです。

references

更新情報

  • 2020/11/09 初版公開
  • 2020/11/11
    • Web負荷試験にタイトル変更。
    • スケジュールに関する注意点を追記 diff
    • シナリオに関する項目を追記 diff

*1:LBがスケールするほどの負荷を出すのは単体実行では難しいため、あくまで簡易なテスト向けです。

*2:9割はそのような予定で建ててきます

*3:ログに書き出しあとから書き込こんだり、job workerに渡して書き込み数を制限したり