ガトリング砲をぶっ放した話

## はじめに
* こんにちは。[CYBIRDエンジニア Advent Calender](http://qiita.com/advent-calendar/2016/cybird) 12日目を担当する[@Nate-River56](http://qiita.com/Nate-River56)です。
    * 新卒1年目, 今年度入社のエンジニアです。
* 昨日, 12日目は[cy-kuniyoshi-seto](http://qiita.com/cy-kuniyoshi-seto)さんの[デザイナーがADVアプリを無理やり作った話
](http://qiita.com/cy-kuniyoshi-seto/items/3b309d7f6b5f5dcb83be)でした。
  * アドベンチャーゲームとシナリオゲームの見せ方にここまで違いがあるとは思いもよりませんでした。
  * また[cy-kuniyoshi-seto](http://qiita.com/cy-kuniyoshi-seto)さんには社内のLTにもよくご登壇していただいているのですが, 発表の度になるほどアニメとか映画ではこのように演出しているのかと驚かされています。本当にすごいお方です。。

## Gatlingとは?
* [Gatling](https://github.com/gatling/gatling)はScalaをDSLとする負荷テストツールです。
* ab(apache-bench)より高機能で, Jmeterよりシナリオを書きやすい(らしい)のが特徴です
    * 実はJmeter, 使ったことがないです..
* ~~Gatlingという名前は田代砲になぞらえたのでしょうか~~

## Gatlingのいいところ
* シナリオがXMLではなく, Scalaで書ける
  * 普通のプログラムと同じ感覚で書くことが出来るので, シナリオの可読性を高くすることが出来る
  * また, プログラムなのでどうにでも工夫することができる
  * Scala入門としても最適(?)
* 出力結果がキレイ
  * Xではなく, HTMLで出力される
  * X環境(X.Org)を整備しておく必要がない, Javaの実行環境のみ整っていればいい
* 名前がカッコイイ

## 実行環境の準備

* 実行に際して`build.sbt`を準備し実行する方法もありますが, bundleされているものを使用する方法が最も簡単・便利なので以下の方法をおすすめいたします。

```bash
$ sudo yum -y install java-1.8.0-openjdk wget unzip # CentOSの場合
$ wget https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/2.2.3/gatling-charts-highcharts-bundle-2.2.3-bundle.zip -O gatling-2.2.3.zip
$ unzip gatling-2.2.3-bundle.zip
```

## シナリオの書き方について

* Google Chrome Developer Toolsの"Copy as cURL"を用いるとシナリオ作成に大変便利です。
* 以下の例を参考にしていただけるとよいかと思います。

```scala:TestSimulation.scala
package test //複数シナリオを書いておく場合, パッケージでまとめておくとわかりやすくていいです

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class TestSimulation extends Simulation {

  // シングルトンオブジェクトを生成する
  object uri {
    val fqdn = "example.com"
    val base = "https://" + fqdn
    val api(req:String): String = {
      "/api" + req
    }
  }

  // UserAgent
  object userAgent {
    val iPhone = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1"
    val android = "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; Nexus One Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"
  }

  // リソースごとに異なるヘッダー
  object headers {

    val top_referer = uri.base + "/index.html"

    // APIのリクエストヘッダ
    // フォーム送信時の形式はx-www-form-urlencodedを指定
    // curl でいうところの--dataにあたる
    val api = Map(
      "Accept"           -> "application/json,text/javascript,*/*;q=0.01",
      "Origin"           -> uri.base,
      "X-Requested-With" -> "XMLHttpRequest", //XHRからのリクエストであることを明示
      "Content-Type"     -> "application/x-www-form-urlencoded; charset=UTF-8",
      "Referer"          -> top_referer
    )

    // Javascriptのリクエストヘッダ
    val js  = Map(
      "Content-Type" -> "application/javascript; charset=UTF-8",
      "Referer"      -> top_referer
    )

    // HTMLのリクエストヘッダ
    val html = Map(
      "Content-Type"-> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
    )
  }

  // 全リソース共通の設定
  val httpConf = http.baseURL(uri.base)
     .acceptLanguageHeader("en-US,en;q=0.8")
     .acceptEncodingHeader("gzip,deflate,sdch,br")
     .userAgentHeader(userAgent.iPhone)
     .connectionHeader("keep-alive")
     .header("Cookie", "nickname=Nate-River56; place=Shibuya, Tokyo")
     .header("Host", uri.fqdn)
     .header("Pragma", "no-cache")
     .header("Cache-Control", "no-cache")
     .maxRedirects(5) //リダイレクトの最大回数

  object scn {

    val stayRange = (0 milliseconds, 100 milliseconds)
  
    val top = scenario("TopPage#1") //わかりやすい名前を付ける
     .exec(
       http("index.html")
         .get(uri.static("/index.html")) //リソースを指定
         .headers(headers.html) //HTML用のヘッダーを付加
         .check(status.is(200)) //HTTP 200であればOK, そうでなければNGでカウントされエラーとして表示される
         .check(regex("<center><h1>Hello</h1></center>").exists)  //指定した要素が含まれているか
     )
     //リソースの間の待ち時間を"Uniform random"の形式で指定
     //繰り返しシナリオを実行した際にリソースへのアクセスをバラけさせるため
     .pause(stayRange._1, stayRange._2)
     .exec(
       html("app.js")
       .get(uri.static("/scripts/app.js"))
       .queryParam("tm", "20161212") //クエリストリングを付加, ?tm=20161212と同じ
       .disableFollowRedirect //リダイレクトを無効に
       .headers(headers.js)
       .check(status.is(200))
     )
     .pause(stayRange._1, stayRange._2)
     .exec(
       http("status.json")
       .post(uri.api("status.json"))
       .headers(headers.api)
       .formParam("key1", "value1") //パラメータを指定
       .formParam("key2", "value2")
       .check(status.is(200))
     )
  }
  
  setUp(
    scn.top.inject(
      nothingFor(5 seconds), //間違えて実行したときのため, 5秒間何もしない
      atOnceUsers(3), //3回だけワッとシナリオを実行する
      rampUsersPerSec(1) to (30) during (120 seconds), //120秒かけてユーザー数を30まで増加, パラメータは適宜調整する
      constantUserPerSec(30) during (180seconds)  //180秒の間, ユーザー数30を維持する
    )
  ).protocols(httpConf)
}
```

## 実行方法
* user-files/simulation以下にDSLを配置し, 以下のように実行します。
  * ビルドが重く, 特に初回非常に重いので気長にお待ち下さい..

```bash
$ cd ~/gatling-charts-highcharts-bundle-2.2.3
$ ./bin/gatling.sh
GATLING_HOME is set to ~/gatling-charts-highcharts-bundle-2.2.3
14:48:23.772 [WARN ] i.g.c.ZincCompiler$ - Pruning sources from previous analysis, due to incompatible CompileSetup.
Choose a simulation number:
     [0] computerdatabase.BasicSimulation
     [1] computerdatabase.advanced.AdvancedSimulationStep01
     [2] computerdatabase.advanced.AdvancedSimulationStep02
     [3] computerdatabase.advanced.AdvancedSimulationStep03
     [4] computerdatabase.advanced.AdvancedSimulationStep04
     [5] computerdatabase.advanced.AdvancedSimulationStep05 //[0]-[5]はサンプルのシナリオ
     [6] test.TestSimulation

Select simulation id (default is 'basicsimulation'). Accepted characters are a-z, A-Z, 0-9, - and _
6

Select run description (optional)
これはテストです

Simulation computerdatabase.BasicSimulation started...

================================================================================
2016-12-12 14:50:13                                           5s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=2      KO=0     )
> request_1                                                (OK=1      KO=0     )
> request_1 Redirect 1                                     (OK=1      KO=0     )

---- Scenario Name -------------------------------------------------------------
[--------------------------------------------------------------------------]  0%
          waiting: 0      / active: 1      / done:0
================================================================================

--- 省略 ---

================================================================================
2016-12-12 14:50:36                                          28s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=13     KO=0     )
> request_1                                                (OK=1      KO=0     )
> request_1 Redirect 1                                     (OK=1      KO=0     )
> request_2                                                (OK=1      KO=0     )
> request_3                                                (OK=1      KO=0     )
> request_4                                                (OK=1      KO=0     )
> request_4 Redirect 1                                     (OK=1      KO=0     )
[> request_5                                               (OK=1      KO=0     )
> request_6                                                (OK=1      KO=0     )
> request_7                                                (OK=1      KO=0     )
> request_8                                                (OK=1      KO=0     )
> request_9                                                (OK=1      KO=0     )
> request_10                                               (OK=1      KO=0     )
> request_10 Redirect 1                                    (OK=1      KO=0     )

---- Scenario Name -------------------------------------------------------------
[##########################################################################]100%
          waiting: 0      / active: 0      / done:1
================================================================================![スクリーンショット 2016-12-12 15.12.22（2）.png](https://qiita-image-store.s3.amazonaws.com/0/61315/686adf04-442c-099b-52ca-cf8fe4715811.png)


Simulation computerdatabase.BasicSimulation completed in 26 seconds
Parsing log file(s)...
Parsing log file(s) done
Generating reports...

================================================================================
---- Global Information --------------------------------------------------------
> request count                                         13 (OK=13     KO=0     )
> min response time                                    257 (OK=257    KO=-     )
> max response time                                    752 (OK=752    KO=-     )
> mean response time                                   337 (OK=337    KO=-     )
> std deviation                                        150 (OK=150    KO=-     )
> response time 50th percentile                        263 (OK=263    KO=-     )
> response time 75th percentile                        265 (OK=265    KO=-     )
> response time 95th percentile                        612 (OK=612    KO=-     )
> response time 99th percentile                        724 (OK=724    KO=-     )
> mean requests/sec                                  0.481 (OK=0.481  KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                            13 (100%)
> 800 ms < t < 1200 ms                                   0 (  0%)
> t > 1200 ms                                            0 (  0%)
> failed                                                 0 (  0%)
================================================================================

Reports generated in 0s.
Please open the following file: ~/gatling-charts-highcharts-bundle-2.2.3/results/0-1481521808046/index.html
```

* 実行結果のフォルダ(0-1481521808046/)をローカルに移動しブラウザで確認すると負荷テストの結果をみることが出来ます

## 負荷テストの結果

* .check()に成功していれば, OKにカウントされそうでなければNGにカウントされます。
* また各リソースのResponse Time Number, レスポンス時間のパーセンタイルをみることが出来ます
![Gatling_global.png](https://qiita-image-store.s3.amazonaws.com/0/61315/896e5791-fd50-08dc-c5c0-2230af7c4563.png)

---
* 全体の1秒あたりのレスポンス数の推移を確認することができ, 想定通り捌けているかを確認することができます
![Gatling_rps.png](https://qiita-image-store.s3.amazonaws.com/0/61315/7ca6a22c-9a60-ab0e-1494-7356111d94df.png)

---
* 個別のリソースに関しても, Response Time Number, レスポンス時間の分布をみることができます
![Gatling_each.png](https://qiita-image-store.s3.amazonaws.com/0/61315/f3f2cdd8-8e72-cd47-5175-e6851250f9fb.png)

## 注意
* 負荷テストを自らが管理していないサーバーに対して行い過大な負荷をかけることはDoS攻撃にあたり, 電子計算機損壊等業務妨害罪等に問われることになります。
* Gatlingに限ったことではありませんが, 負荷テストツールをDoS攻撃に利用することは絶対にしないでください。

## 参考リンク

### 公式Document
<http://gatling.io/docs/2.2.3/>

### UserAgentについて
<http://qiita.com/nightyknite/items/b2590a69f2e0135756dc>

### 負荷のかけ方について
<http://qiita.com/noexpect/items/2f52ae16ec20a5ba14b4>

### Gatlingの使用例
<http://shanon-tech.blogspot.jp/2015/10/scala-gatling.html>

## まとめ
* JMeterのようにGatlingでmaster-slave構成の負荷テスト環境をつくることは難しいです。
* 比較的大きいサービスに対する負荷テストでなければscalaでシナリオを書くのも楽しいですし, ぜひ一度Gatling砲をぶっ放してみてはいかがでしょうか？

## 最後に 
* さて, 次回は一年上の先輩である[cy-kenta-takahashi](http://qiita.com/cy-kenta-takahashi)さんの"UIPageViewControllerを使って漫画ビューアの見開き表示に対応した話"です。非常に楽しみです！
  * 業務ではあまり関わることがありませんが, 社内イベント等で関わることの多い先輩です！
  * ~~今度スプラトゥーンでぜひ対戦したいです~~