最も"攻撃力"が高い負荷試験ツールは...?

この記事は決して DoS 攻撃を推奨・肯定するものではありません。DoS 攻撃は攻撃対象へ金銭的・物損的被害が発生する可能性がある非常に危険な行為です。本記事に起因するいかなるトラブル・損失・損害に対しても、著者は責任を負いません。

こんにちは k-jun です。先日 GitHub の海を泳いでいると以下のようなリポジトリを発見しました。

https://github.com/ajax-lives/NoRussian

Volunteer DoS tool via HTML + JS. Made to be user friendly and simple to use - as to maximize the amount of people able to help. Running the HTML locally on your device will run a DoS attack on Russian and Belarussian websites (gov, mil, banks, propaganda sites), through complete volunteering.

HTML と JavaScript を利用して、特定のサイトへボランティア DoS 攻撃を行うためのツールのようです。

https://ja.wikipedia.org/wiki/2022%E5%B9%B4%E3%83%AD%E3%82%B7%E3%82%A2%E3%81%AE%E3%82%A6%E3%82%AF%E3%83%A9%E3%82%A4%E3%83%8A%E4%BE%B5%E6%94%BB

中身のコードを見てみると、リストアップした URL へ JavaScript より大量のリクエストを発行。アクセスするだけで誰でも Dos 攻撃に参加できる、まさに Volunteer DoS tool というわけです。

さてここで、ある疑問が自分の中に湧いてきました。JMeter などに代表される負荷試験ツールならもっと効率的に負荷を与えられるのではと。Volunteer DoS Tool はブラウザから大量のリクエストを発行しているが、負荷試験ツールならより少ないコンピューターリソースで高い負荷を生み出せるのではと。ということで、休日の時間を利用してこれを調べて実験してみることにしました。

目的

今回の目的は、同じコンピューターリソースを利用して、対象に最も高い負荷を与え続けられる負荷試験ツールの特定 です。考えの発端は Volunteer DoS Tool ですが、要するに単純で大量のリクエストを送信し続ける場合、最も効率の良い負荷試験ツールは何かというお話です 笑。

手法

星の数ほどある負荷試験ツールですが、今回は以下の7種類のものを調査の対象としました。選定基準はそれなりに有名であること、簡単に試せることの2つです。これだけ言語がバラバラで7種類もあればそれなりの結果が得られるでしょう。僕の時間も無限ではないのです...。

name url language stars
wrk https://github.com/wg/wrk C 31.5k
hey https://github.com/rakyll/hey Go 13k
ab https://github.com/CloudFundoo/ApacheBench-ab C 89
siege https://github.com/JoeDog/siege C 4.9k
vegeta https://github.com/chidakiyo/vegeta Go 19.2k
locust https://github.com/locustio/locust Python 18.4k
gatling https://github.com/gatling/gatling Scala 5.5k

次に、負荷をかける環境を AWS 上に構築していきます。構築と言ってもターゲットとランチャーだけのシンプルなものです。

f:id:K-jun1221:20220313002124p:plain

ランチャー上では上記7種の負荷試験ツールのインストールを、ターゲットでは負荷を受ける役割の NGINX を起動させておきます。なお、VPC は ap-northeast-1 、Subnet は ap-northeast-1a に作成しました。その他インスタンスタイプ、OS などは以下のとおりです。

name OS instance type
target Ubuntu v18.04 c5.large
launcher Ubuntu v18.04 c5.large

また socket: too many open files を回避するため、ランチャーとターゲットのファイルディスクリプタは予め最大値まで上げておきます。ターゲット上の NGINX にも同様に反映しておきます。

# in target & launcher
$ sudo tail -n 4 /etc/security/limits.conf
* soft     nproc          780246
* hard     nproc          780246
* soft     nofile         780246
* hard     nofile         780246
# in target
$ sudo cat /etc/nginx/nginx.conf | grep worker
worker_processes 16;
worker_rlimit_nofile  780246;
        worker_connections 780246;

最後に実験手順です。ランチャーとターゲットへ ssh でログインし、以下の手順で実験を行いました。(RAM に関しては特段目立った変化が見られたなかったので省略しました。)

  1. ランチャー & ターゲット: vmstat を実行。
  2. ランチャー: 調査対象の負荷試験ツールを30秒間実行。
  3. ランチャー: 負荷試験ツールから出力された RPS を記録。
  4. ランチャー & ターゲット: vmstat から出力された CPU と RAM の変化量を記録。

負荷試験ツールは 実行時間は30秒、RPS は最大となるように設定を弄っています。例として wrk を例に取った実際のコマンドを以下に示します。

# ubuntu@target
$ vmstat 1 -nt | awk '{printf("%s,%s,%s\n", $19,$15,$4)}'
# ubuntu@launcher
$ vmstat 1 -nt | awk '{printf("%s,%s,%s\n", $19,$15,$4)}'
$ ./wrk/wrk -d 30 -t 4 -c 100 http://10.1.101.10

結果

name rps 1回目 rps 2回目 rps 3回目 command
wrk 77491.89 78012.51 76856 ./wrk/wrk -d 30 -t 4 -c 100 http://10.1.101.10
hey 45224.8694 45779.8114 46258.3372 ./hey -z 30s http://10.1.101.10
ab 39328.21 39147.51 38708.19 ab -n 1000000 -c 100 http://10.1.101.10/
siege 32088.11 31622.09 31692.97 siege -t 30S http://10.1.101.10
vegeta 14999.48 15000.44 14999.59 echo "GET http://10.1.101.10" | vegeta attack -rate 15000 -duration 30s | vegeta report
locust 3287 3306 3313 python3 -m locust --master (※詳細は別途参照)
gatling 833.333 833.333 833.333 bin/gatling.sh (※詳細は別途参照)

※ locustfile.py

from locust import HttpUser, task

class HelloWorldUser(HttpUser):
    @task
    def hello_world(self):
        self.client.get("/")

※ BasicSimulation.java

package computerdatabase;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
import java.time.Duration;

public class BasicSimulation extends Simulation {

  HttpProtocolBuilder httpProtocol =
      http
          .baseUrl("http://10.1.101.10")
          .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
          .doNotTrackHeader("1")
          .acceptLanguageHeader("en-US,en;q=0.5")
          .acceptEncodingHeader("gzip, deflate")
          .userAgentHeader(
              "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0");

  ScenarioBuilder scn =
      scenario("simplest")
          .exec(http("request_1").get("/")
          .check(status().is(200)));
  {
    setUp(scn.injectOpen(stressPeakUsers(10000).during(30)).protocols(httpProtocol));
  }
}

wrk

f:id:K-jun1221:20220313012443p:plain

hey

f:id:K-jun1221:20220313011939p:plain

ab

f:id:K-jun1221:20220313011958p:plain

siege

f:id:K-jun1221:20220313012007p:plain

vegeta

f:id:K-jun1221:20220313012018p:plain

locust

f:id:K-jun1221:20220313012030p:plain

gatling

f:id:K-jun1221:20220313012037p:plain

考察

wrk

最も目を引くのは wrk の 78,012 RPS ですね。他を寄せ付けないほど圧倒的な RPS であり、ターゲット側の CPU をほとんど 0% まで使い切らせています。ランチャー側の CPU が 60% 程度で安定しているところを見るに、まだ余力がありそうです。ターゲット側のスペックをより積めばより高い PRS を出すでしょう。

hey, siege

hey, siege はそれぞれ 46,000 RPS, 32,000 RPS と高い数値を記録しています。ランチャー側の CPU を使い切っていることから wrk よりリソースの使用効率が悪いものの、負荷試験ツールとしては十分な RPS を出せています。特に hey はインストール・設定共に簡単であり、良い負荷試験ツールと感じました。

ab

ab は 39,000 RPS とかなり高い値を出しているにも関わらず、ランチャー・ターゲット共に CPU に余裕が見られます。この余剰リソースを使い切るため試行錯誤してみましたが、今回の実験ではこれ以上の PRS を実現することは無理でした...。実験終盤に CPU 使用量が回復しているのは、秒数ではなく 1,000,000 リクエストを打ち切った後に終了するという設定で ab を実行しているためです。

vegeta

vegeta は 14999.59, 15000.44, 14999.59 と全ての PRS がほとんど同じ値になっています。コマンドを見てもらえばわかりますが、vegeta -rate 15000 で RPS を予め指定しているため、このような均一的な結果になっています。どうやら実行時に address の食い合いが起きるらしく、15,000 以上の RPS を指定すると以下のエラーが頻繁に見られました。

Get "http://10.1.101.10": dial tcp 0.0.0.0:0->10.1.101.10:80: bind: address already in use

名前と GitHub の画像は面白いですが、同じ Go 言語の hey と比べてみると微妙かなぁというのが正直な印象です。

locust, gatling

locust, gatling の2種類は他と比較して RPS がかなり低い値に抑えられています。この2つは他と異なり言語によるシナリオが記述可能であり、locust は GUI。gatling は JVM負荷試験の箇所以外でのリソース消費要因も備えています。

コネクションに関しても懸念が残ります。locust, gatling はそれぞれユーザーという設定項目により並列度を指定します。このため、他ツールとはことなり、リクエストごとにコネクションを都度都度切っているのかも...? という予想をしています。

総じてこれら2つは単純なリクエストを送信する以上の複雑な機構を備えた高級な負荷試験ツールであり、純粋に今回の目的には適していなかったのかもしれません。

結論

今回の実験の結果 同じコンピューターリソースを利用して、対象に最も高い負荷を与え続けられる負荷試験ツール は wrk ということになりました。wrk 側の CPU を余らしつつ NGINX 側の CPU を使い切っており、最も負荷を与えるという点ではまず確実に wrk が結論になるでしょう。要するに手っ取り早く大量の負荷を掛けたいときには wrk を脳死でぶっ放せばよさそうということです 笑。

おまけとしてですが、gatling や hey などのツールに触れられたこと。siege・wrk の設定を一通り眺めて知らなかったオプションを発見・利用出来たこと。実験・比較・調査し自分の中に自分なりの回答を持つということが出来たので良い経験となりました。今後もこのようなことをどんどんやっていければと思います。それでは今回はこの辺で!