SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

負荷テストを定期実施するために必要な9つのポイント【前編】

f:id:ug_23:20190208152336j:plain

こんにちは。好きなテストフレームワークはやっぱりRSpec、スマートキャンプの今川(@ug23_)です。

みなさんは負荷テスト、定期的にやっていますか? リリースごと、マイルストーンごと、など単位はさまざまでしょうが、定期的にやる仕組みは重要だと感じています。 今回は社内で負荷テストを定期的に行う仕組みを整えたときについてまとめました。

この記事は 前編 です。前半の5つのポイントについて紹介していきます。

負荷テストの背景

スマートキャンプではいくつかのWebサービスを運営しております。 ほとんどのサービスは公開しているサービスなので、ユーザの挙動によって負荷が大きく変わります。 例えば、テレビに取り上げられるなどしてバズったりすることもあるでしょう。

スケールアップするにしても、スケールアウトするにしても いまの構成でどんだけ耐えられるの?がわからないと対策が的外れなことになりかねません。

公開サービスでユーザが訪問してくる(クローズドな社内サービスではない)場合の負荷テストについて述べていきます。

【ポイント1】正しい負荷テストについて学ぼう

私は正しい負荷テストを体系的に学んだことがなかったので以下の書籍で勉強しました。 対象サービスがAWSで構築されていたことからこちらの本を選びましたが、ネットワーク構成や注意すべき考え方について載っているのでおすすめです。

また、過去行われた負荷テストはどのように行ったか、何が課題で何がうまく行ったのか、その時に実施した社員に聞いてみました。

【ポイント2】負荷テストの終了条件を決めよう

あてどなく負荷テストをかけて「毎分○リクエストで死ぬわ!!」とやっても仕方がないのでゴールを決めました。 ソフトウェアテストには終了条件を定めないと、過剰なテストによるムダやテスト不足によるムラが起きてしまいます。

今回とりあげる負荷テストでは以下の3つを満たしたら終了とすることにしました。

  • ボトルネックとなる要因を特定できていること
  • スケールアップ時またはスケールアウト時の特性がわかっていること
  • 目標負荷を達成するための改修または調査の見通しが立っていること

数値を測定して終わり、でもいいんですが結局それを直すのにどれぐらいの期間と影響範囲があって、どれぐらいの効果が見込めるのかを調査しておかないとムリな開発をする羽目になります。 「これをやったら許容できる負荷を2倍にできるんでやらせてください!」と進言するのにも使えます。

終了条件を決めることはムリ・ムダ・ムラをなくすことにつながると思います。

【ポイント3】対象とするメトリクスとその目標数値を決める

試験の終了条件は決めました。次は実際にどこを目標とするか、です。 エンジニアだけで決めても、事業影響が出たら意味がありません。そのシステムで事業を動かしているメンバーと話し合いました。 今回の試験では以下のように決めています。

メトリクス

  • 事業メンバーはGoogle Analytics上のリアルタイム集計のアクティブユーザ数で現状の来訪者数をウォッチしている
  • アクティブユーザ数とアプリケーションサーバ上で計測される毎分リクエスト数はおおよそ似た遷移かつだいたい近似できそう

⇒ 目標のメトリクスはアプリケーションサーバ上の毎分リクエスト数とする

目標数値

  • 過去にサービスダウンしたことがあったのでその時の負荷には十分耐えたい
    • その時の倍は耐えるように
  • 今後のサービスの伸び幅も考えると1.5倍ぐらいは多めに見込んでおきたい
    • そのさらに1.5倍でも耐えるように

⇒ 目標数値は以前落ちた際の負荷の約3倍とする

【ポイント4】想定する負荷や環境や社内のスキルに合わせて負荷テスト環境を作ろう

負荷テストツール

以前の負荷テストの記録ではjmeterを使っていましたが、Gatlingを採用しました。理由は以下です。

  • 私がScalaに慣れていた
  • 社内にScalaを使える人間がもうひとりいた
  • 自動生成されるレポートがきれいな
  • 自分が一番使いたかった

gatling.io

Gatling、かっこいいですね!

実際には以下のリポジトリを参考に用意しました。

github.com

公式のリポジトリでも問題はないのですが、開発時のやりやすさを考えSBTで実行できる形でのプロジェクト作成を選びました。 もちろんScalaなのでIntelliJIDEAやENSIMEなどを用いれば型推論をフル活用して開発することができます。

公式のチュートリアルは丁寧にかかれているのでそれに沿ってシナリオをかけば簡単に負荷テストをセットアップできますし、 公式が「Javaがあればこれで動くぜ!」というプロジェクトを作ったzipを配布しているのでそれでも十分だと思います。 非Scalaエンジニアにも(関数型な書き方を多用しなければ)DSLで読みやすいでしょう。

環境作成

動かすサーバは以下のように準備しました。

  • AWSのCentOS7イメージでEC2を作成
  • SDKMAN を利用し、 jdk scala sbt をインストール
  • インスタンス上に自作のシナリオを配置、 sbt test testOnly XxxxSpec のように実行して爆撃開始
  • httpd をインストール、ルートディレクトリとGatlingのレポートをrsyncで同期させてブラウザから閲覧できるようにした

【ポイント5】攻撃に合わせてシナリオを組み立てよう

作ったシナリオ

今回は下記の4種類のシナリオを用意しました。

  1. ヘルスチェックに使っているエンドポイントへのストレステスト
  2. ヘルスチェックに使っているエンドポイントへのスパイクテスト
  3. 1記事を指定してのストレステスト
  4. 1記事を指定してのスパイクテスト

  5. ストレステスト: 秒間20リクエストから10分で250リクエストまで増やして限界値をさぐる

  6. スパイクテスト: インターバルをあけつつ同時に大量のリクエストを送って限界値をさぐる

※一般的な定義についてはこちらを参照ください。

sites.google.com

ヘルスチェックに使っているエンドポイントに対して負荷をかけているのはDBやRedisに依存しないWebアプリケーションとしての限界値を探るために測っています。 また、このタイミングでインスタンスタイプを変えたり、台数を変えたりしてスケールアップ・スケールアウト時の特性を測っています。

実施しなかったシナリオ

他にも負荷テストの手法はありますが、下記の理由から実施しませんでした。

  • ロングランテスト: 週に1回は必ずリリースしているので長時間稼働は重要視していない
  • ピークロードテスト: 対象となるシステムではピーク値が続くようなケースはなかった

実際のシナリオ

Gatlingでは、プロキシを利用してアクセスしたページへのリクエストのシナリオを生成するが用意されていますし、 チートシートが充実しているため、単純なものから複雑なものまでシナリオを作ることができます。

gatling.io

実際につかったのを少し変えたものが以下のシナリオになります。リクエスト数やエンドポイントは例です。

package smartcamp

import io.gatling.core.Predef._
import io.gatling.http.Predef._

import scala.concurrent.duration._

class ApplicationStress extends Simulation {

// base URLやヘッダーを定義
  val httpProtocol = http
    .baseUrl("http://target.example.com")
    .disableCaching
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
    .acceptEncodingHeader("gzip, deflate, br")
    .acceptLanguageHeader("ja,en-US;q=0.9,en;q=0.8")
    .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
    .upgradeInsecureRequestsHeader("1")

// ここにシナリオ名と攻撃先を書く
  val scn = scenario("一覧APIアクセス")
    .exec(http("全件取得")
    .get("/hoge/XXXXXX"))

// シナリオの内容を書く
  setUp(
    scn.inject(
      // 1000リクエスト/秒から30000リクエスト/秒を20分間で遷移させる
      rampUsersPerSec(1000) to 30000 during (20 minutes)
    ).protocols(httpProtocol)
  )
}

scn.inject(...)の中に、リクエストの内容を書いていくことでシナリオを変えることができます。

また、参考までにスパイクテストは以下のようになっています。injectには Iterable[InjectionProfileFactory] 型を渡せるので以下のようにわたすことができます。

※リクエスト数は例です。

setUp(
  scn.inject(
    Range(0, 5).map(Math.pow(2, _)).toList.flatMap { x =>
      List(heavisideUsers(x.toInt * 10000) during (30 seconds),
        nothingFor(30 seconds))}
  ).protocols(httpProtocol)
)

スパイクを起こさせるのに heavisideUsers を使っています。指定した時間でヘヴィサイドの階段関数を近似するようにリクエストを生成してくれます。
30秒は少し長いように思われるかもしれませんが、実際のユーザ来訪をシミュレートするのでそこまで急激なスパイクよりかは一気にアクセス数が伸びる、というようなシナリオを想定しているためにこうしています。

前編のまとめ

  • Gatlingを使った負荷テストとそれを定期実施するために必要なことをまとめました
  • 正しい負荷テストについて学びました
  • 負荷テストの終了条件を決めました
  • 対象とするメトリクスとその目標数値を決めました
  • 想定する負荷や環境や社内のスキルに合わせて負荷テスト環境を作りました
  • 攻撃に合わせてシナリオを組み立てました

後編では残り4つのポイントを紹介しつつ紹介していく予定です。

お読みいただき、ありがとうございました!