Goose(Rust)の使い方|負荷テストの導入手順・コマンド引数・メトリクスの読み方
Gooseは、負荷テストのシナリオをRustのコードとして書く負荷テストツールです。設定ファイルやGUIではなく通常のRustプログラムとしてビルドするため、既存のcrateをそのまま呼び出せる一方、導入手順は一般的なCLIツールとは異なります。この記事では公式ドキュメントの記述に加えて、実際にGoose 0.18.1をビルドして負荷テストを走らせた結果をもとに、インストールから最初のシナリオ作成、コマンド引数、メトリクスの読み方までを整理します。
なお、同じ「goose」という名前でもBlock社が公開しているAIコーディングエージェントや、MongoDB向けのODMであるMongooseは別物です。ここで扱うのは、crates.ioで配布されているRust製の負荷テストライブラリのGooseです。
まとめ
- Gooseはライブラリcrateなので、
cargo install gooseは「no binaries」で失敗します。cargo newでプロジェクトを作り依存に追加するのが正しい手順です。 - 最新の安定版は0.18.1(2025年8月14日公開)で、ビルドにはrustc 1.70.0以上が必要です。
- 同時ユーザー数の既定値はCPUコア数、ユーザーの起動レートは毎秒1です。
--hostは未指定だと起動時にエラーで停止します。 - レポートは
--report-fileの拡張子で形式が決まり、HTML・Markdown・JSONを出力できます。 - 複数サーバーへの分散実行(Gaggle)は0.17.0で一時削除されたまま0.18.1でも復帰していません。分散負荷が要件なら0.18系は選べません。
Scenario::set_shared_dataやGooseUser::goose_sendのような実在しないAPIを使ったコード例が古い記事に残っています。0.18.1ではコンパイルが通りません。
Rust製負荷テストツールGooseの位置づけ
Locustとの関係と設計思想
公式ドキュメント(The Goose Book)は、GooseをPython製のLocustに着想を得たツールであると明記しています。シナリオはRustの標準的なコードとして記述し、HTTPリクエストはReqwestクレートが処理します。つまり負荷テスト自体が、Gooseライブラリに依存した1つのRustアプリケーションになります。
性能について公式は「CPUコアあたりLocustの11倍以上のトラフィックを生成する」と説明しています。これはツール提供元自身の主張であり、第三者による検証値ではありません。ただし設計上の違いは明確で、Locustが1台のサーバーで複数コアを使うために分散構成を必要とするのに対し、Gooseは単一プロセスで利用可能な全コアを使います。
分散実行(Gaggle)が使えない現状
複数サーバーに負荷生成を分散するGaggleという機能について、公式ドキュメントのGaggleの章には「Gaggleのサポートは Goose 0.17.0 の時点で一時的に削除された。この章で説明する機能が必要な場合は Goose 0.16.4 を使うこと」という注記が冒頭に置かれています。0.18.1のCargo.tomlにはgaggleというfeature名こそ残っていますが中身は空で、Rustソース中の言及もコメント4行だけでした。
したがって0.18系では、負荷生成を1台のサーバー内で完結させることになります。1台の帯域やCPUでは足りない規模を測る要件があるなら、0.16.4に留めるか、分散実行を備えたk6やLocustを選ぶ判断になります。ツールの比較軸そのものは負荷テストツール比較でも整理しています。
Gooseのインストール手順
cargo install goose が失敗する理由
Gooseの導入をcargo install gooseと説明している資料がありますが、0.18.1に対してこのコマンドは成功しません。実行すると次のように停止します。
$ cargo install goose
Updating crates.io index
Downloading crates ...
Downloaded goose v0.18.1
error: there is nothing to install in `goose v0.18.1`, because it has no binaries
`cargo install` is only for installing programs, and can't be used with libraries.
To use a library crate, add it as a dependency to a Cargo project with `cargo add`.
Gooseが提供しているのはライブラリだけで、実行可能ファイルを含んでいないためです。crateのCargo.tomlに[[bin]]の定義は存在しません。負荷テストの実行ファイルは、利用者側が自分のプロジェクトとしてビルドします。
プロジェクトの作成と依存の追加
正しい手順は、Cargoで新しいアプリケーションを作り、そこにGooseと非同期ランタイムのTokioを追加することです。この記事の後半で扱うセッション管理にはserde、レスポンス検証には補助crateのgoose-eggsを使うため、あわせて追加しておきます。
$ cargo new loadtest
$ cd loadtest
$ cargo add goose tokio
$ cargo add serde --features derive
$ cargo add goose-eggs
結果としてCargo.tomlの依存は次のようになります。実際にcargo addを実行するとその時点の具体的なバージョンが書き込まれるため、表示は多少異なります。
[dependencies]
goose = "^0.18"
tokio = "^1"
serde = { version = "1", features = ["derive"] }
goose-eggs = "0.6.0"
ビルドにはrustc 1.70.0以上が必要です。Goose自体が非同期ライブラリのため、Tokioの追加は省略できません。なお公式ドキュメントはgoose-eggsを"0.4"と案内していますが、crates.io上の最新は0.6.0(2025年2月28日公開)で、ドキュメントの記載のほうが古くなっています。
最初の負荷テストの作成と実行
最小構成のシナリオコード
もっとも単純な負荷テストは、対象サイトのトップページを繰り返し取得するものです。以下はsrc/main.rsの全体で、Goose 0.18.1に対してコンパイルが通ることを確認しています。
use goose::prelude::*;
async fn loadtest_index(user: &mut GooseUser) -> TransactionResult {
let _goose = user.get("").await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), GooseError> {
GooseAttack::initialize()?
.register_scenario(
scenario!("LoadtestTransactions").register_transaction(transaction!(loadtest_index)),
)
.execute()
.await?;
Ok(())
}
負荷をかける処理はasyncな関数として書き、引数にGooseUserへの可変参照を受け取ります。user.get("")の空文字列は、実行時に--hostで与えるホストからの相対パスです。ホストをコードに埋め込まないことで、同じバイナリを検証環境と本番相当環境に使い回せます。戻り値のTransactionResultは成功時にOk(())を返します。
main側ではGooseAttack::initialize()でテストを初期化し、scenario!マクロで作ったシナリオにtransaction!マクロで処理を登録します。このScenarioとTransactionという名前は0.16.0(2022年5月1日公開)の改称後のもので、それ以前はGooseTaskSetとGooseTask、マクロもtask!でした。古い記事のコードがそのままでは動かない原因の1つがこれです。
実行コマンドと実測したメトリクス
実行はcargo run --releaseで行い、Gooseへの引数は--の後ろに続けます。ホストを指定せずに起動すると、負荷をかける前の段階で停止します。
$ cargo run --release
21:14:18 [INFO] Output verbosity level: INFO
21:14:18 [INFO] Logfile verbosity level: WARN
21:14:18 [INFO] users defaulted to number of CPUs = 16
21:14:18 [INFO] iterations = 0
Error: InvalidOption { option: "--host", value: "", detail: "A host must be defined via the --host option, the GooseAttack.set_default() function, or the Scenario.set_host() function (no host defined for LoadtestTransactions)." }
3行目のとおり、ユーザー数を指定しなければ実行マシンのCPU数がそのまま使われます。この環境はIntel Core i9-9880H(8コア16スレッド)のため16ユーザーになりました。
ここではローカルにPythonの簡易HTTPサーバーを立て、5ユーザー・毎秒5ユーザー起動・実行時間10秒という条件で実際に走らせました。--no-reset-metricsは、ユーザー起動中に取得した計測値を破棄せず残すための指定です。
$ cargo run --release -- --host http://127.0.0.1:8099 -u5 -r5 -t10s --no-reset-metrics
得られた出力の主要部分は次のとおりです。
=== PER SCENARIO METRICS ===
------------------------------------------------------------------------------
Name | # users | # times run | scenarios/s | iterations
------------------------------------------------------------------------------
1: LoadtestTransactions | 5 | 16414 | 1492 | 3283
=== PER TRANSACTION METRICS ===
------------------------------------------------------------------------------
Name | # times run | # fails | trans/s | fail/s
------------------------------------------------------------------------------
1: LoadtestTransactions
1: | 16,419 | 0 (0%) | 1493 | 0.00
=== PER REQUEST METRICS ===
------------------------------------------------------------------------------
Name | # reqs | # fails | req/s | fail/s
------------------------------------------------------------------------------
GET | 16,419 | 0 (0%) | 1493 | 0.00
------------------------------------------------------------------------------
Name | Avg (ms) | Min | Max | Median
------------------------------------------------------------------------------
GET | 2.72 | 1 | 14 | 3
------------------------------------------------------------------------------
Slowest page load within specified percentile of requests (in ms):
------------------------------------------------------------------------------
Name | 50% | 75% | 98% | 99% | 99.9% | 99.99%
------------------------------------------------------------------------------
GET | 3 | 3 | 4 | 5 | 7 | 10
------------------------------------------------------------------------------
Name | Status codes
------------------------------------------------------------------------------
GET | 16,419 [200]
=== OVERVIEW ===
------------------------------------------------------------------------------
Action Started Stopped Elapsed Users
------------------------------------------------------------------------------
Increasing: 2026-08-12 06:14:20 - 2026-08-12 06:14:21 (00:00:01, 0 -> 5)
Maintaining: 2026-08-12 06:14:21 - 2026-08-12 06:14:31 (00:00:10, 5)
Decreasing: 2026-08-12 06:14:31 - 2026-08-12 06:14:31 (00:00:00, 0 <- 5)
Target host: http://127.0.0.1:8099/
goose v0.18.1
16,419リクエストを送出して毎秒1,493リクエスト、応答時間は平均2.72ミリ秒、中央値3ミリ秒、99パーセンタイルで5ミリ秒、99.99パーセンタイルで10ミリ秒でした。エラーはゼロで、全リクエストが200を返しています。毎秒の値は実行時間の10秒ではなく、起動に要した1秒を含む11秒で割った値である点に注意してください(16,419÷11=1,493)。
この数値はGooseの生成能力の上限でも、実サービスの性能でもありません。対象が同一マシン上のpython3 -m http.server、つまりシングルスレッドの静的ファイル配信であり、律速はサーバー側にあります。同じ条件で3回繰り返したところ、毎秒628リクエストから1,826リクエストまで3倍近い幅で変動しました。読み取るべき点は数値そのものではなく、5ユーザーという小さな設定でもローカルなら毎秒数百から千件規模に達すること、そしてローカルの簡易サーバーを相手にした測定値は繰り返すだけで大きく揺れるため比較対象には使えないことです。
実務で使うコマンドライン引数
主要オプションと既定値
ビルドしたバイナリに--helpを渡すと全オプションが表示されます。使用頻度が高いものを、既定値とあわせて整理します。既定値は--helpの記載と実行結果から確認したものです。
| オプション | 内容 | 既定値 |
|---|---|---|
-H, --host |
負荷をかける対象ホスト | 必須(未指定は起動時エラー) |
-u, --users |
同時ユーザー数 | CPUコア数 |
-r, --hatch-rate |
毎秒あたりの起動ユーザー数 | 1 |
-s, --startup-time |
全ユーザーを起動しきるまでの時間 | 未設定 |
-t, --run-time |
テストの実行時間(30s、20m、1h30m) | 無制限 |
--test-plan |
負荷パターンを段階指定 | 未設定 |
--report-file |
レポート出力(.html .htm .md .json) | 出力しない |
--no-reset-metrics |
起動フェーズの計測値を破棄しない | 破棄する |
--running-metrics |
実行中の途中経過を出す間隔 | 出さない |
--throttle-requests |
毎秒リクエスト数の上限 | 無制限 |
--iterations |
各ユーザーがシナリオを回す回数 | 0(時間で制御) |
--scenarios |
実行するシナリオを限定 | 全シナリオ |
--co-mitigation |
Coordinated Omissionの補正方式 | 未設定 |
--timeout |
リクエスト単位のタイムアウト秒 | 60 |
--accept-invalid-certs |
HTTPS証明書の検証を無効化 | 検証する |
-R, --request-log |
リクエスト単位のログをファイル出力 | 出力しない |
--request-format |
ログ形式(csv、json、raw、pretty) | json |
--no-telnet |
telnetコントローラを無効化 | 有効(ポート5116) |
--no-websocket |
WebSocketコントローラを無効化 | 有効(ポート5117) |
ユーザー数を指定しない場合はCPUコア数が使われるため、実行するマシンによって負荷が変わります。繰り返し比較する測定では-uを明示してください。既定ではユーザーが起動しきった時点で計測値がいったん破棄されるので、起動フェーズを含めて記録したい場合に--no-reset-metricsを付けます。またGooseは既定でtelnetとWebSocketの制御ポートを開き、実行中にユーザー数を変更できます。共用環境で走らせるときは--no-telnetと--no-websocketで閉じておくのが無難です。
テストプランによる負荷パターンの制御
「ユーザー数、その人数に到達するまでの時間」という組を;で並べると、段階的な負荷パターンを1つの引数で表現できます。時間の単位はs・m・hで指定し、省略すると秒として解釈されます。
$ cargo run --release -- -H http://local.dev/ --test-plan "10,1m;10,5m;0,0s"
$ cargo run --release -- -H http://local.dev/ --test-plan "500,5m;500,5m;2500,45s;500,45s;500,5m;0,0s"
上の例は「1分かけて10ユーザーまで増やし、その人数で5分維持し、その後できるだけ速く停止する」という指定です。下の例は5分かけて500ユーザーまで増やし、5分維持したあとに2,500ユーザーへ45秒のスパイクを与えて500へ戻すもので、キャンペーン開始直後のような瞬間的な集中を再現する場合に使います。--usersと--run-timeの組み合わせでは表現できない形の負荷は、この--test-planで組み立てます。
メトリクスの読み方とレポート出力
コンソール出力の各セクション
テスト終了時にコンソールへ出力されるメトリクスは、シナリオ単位、トランザクション単位、リクエスト単位、パーセンタイル、ステータスコード、全体概要という順に並びます。失敗が発生した場合はここにエラーの要約も加わります。シナリオ単位は各シナリオを何周したか、トランザクション単位は登録した処理がそれぞれ何回実行されたか、リクエスト単位はHTTPメソッドとパスごとの件数・失敗数・毎秒あたりの件数・応答時間を示します。1つのトランザクションが1リクエストしか行わない場合、後ろの2つはほぼ同じ値になります。
性能の判断で重要なのはパーセンタイルの行です。平均値は少数の極端に遅い応答に引きずられて実態を隠すため、50パーセンタイルと99パーセンタイル以降を並べて見ます。前掲の実測では中央値3ミリ秒に対して99.99パーセンタイルが10ミリ秒で、約3倍の開きがありました。
HTML・Markdown・JSONでのレポート出力
--report-fileに渡すファイル名の拡張子で出力形式が決まります。対応しているのは.html、.htm、.md、.jsonで、このうちMarkdownとJSONは0.18.0で追加されました。オプション自体は複数回指定できるため、人が読むHTMLと機械処理用のJSONを同時に出すこともできます。
JSONレポートに入るのは集計済みの値です。トップレベルには全体のメトリクス、リクエストとレスポンスの集計、シナリオ別・トランザクション別の集計、ステータスコード集計、エラー、Coordinated Omission関連の各項目が並びます。1リクエストごとの生ログが必要な場合は、レポートではなく--request-logを使ってください。CI上で前回結果と閾値を比較するだけなら、JSONレポートを読むほうがHTMLの解析より確実です。
Coordinated Omissionへの補正
負荷テストの計測には、サーバーが停止している間はリクエストを送れないため「遅かったはずの応答」が記録から抜け落ち、結果として平均が実態より良く見えるというCoordinated Omissionと呼ばれる問題があります。Gooseは各ユーザーがシナリオを一巡する間隔を追跡し、停止を検知して欠測分を補う仕組みを持っており、--co-mitigationで補正方式を指定します。0.18.1では、この補正に関する指標が重大度の分類を伴う詳細なものへ拡張されました。
指定できる値はaverage、maximum、minimum、disabledの4つです。公式ドキュメントの例には--co-mitigation enabledという記述がありますが、0.18.1でこれを渡すと起動に失敗します。
$ cargo run --release -- --co-mitigation enabled --host http://127.0.0.1:8099 -u1 -t1s
Running `target/release/loadtest --co-mitigation enabled --host 'http://127.0.0.1:8099' -u1 -t1s`
target/release/loadtest: invalid argument to option `--co-mitigation`: GooseError: invalid option or value specified
未指定時に補正が働くかどうかはドキュメントとソースの記述が食い違っているため、補正を効かせたい場合は--co-mitigation averageのように明示するのが確実です。長時間の耐久テストや、途中でスループットが落ちる可能性がある対象を測る場合は、この指標を確認してください。
シナリオ設計の実装パターン
複数シナリオの重み付けと待ち時間
実際のトラフィックは、閲覧中心のユーザーと検索を多用するユーザーのように性質が分かれます。Gooseではシナリオを複数登録し、それぞれに重みを与えて出現比率を調整します。以下は閲覧8対検索2の比率で、閲覧シナリオ内では記事ページを3倍の頻度で叩く例です。
use goose::prelude::*;
use std::time::Duration;
async fn view_index(user: &mut GooseUser) -> TransactionResult {
let _goose = user.get("/").await?;
Ok(())
}
async fn view_article(user: &mut GooseUser) -> TransactionResult {
let _goose = user.get("/articles/1").await?;
Ok(())
}
async fn search(user: &mut GooseUser) -> TransactionResult {
let _goose = user.get("/search?q=goose").await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), GooseError> {
GooseAttack::initialize()?
.register_scenario(
scenario!("ReadOnlyUser")
.set_weight(8)?
.set_wait_time(Duration::from_secs(1), Duration::from_secs(5))?
.register_transaction(transaction!(view_index).set_name("index"))
.register_transaction(transaction!(view_article).set_name("article").set_weight(3)?),
)
.register_scenario(
scenario!("SearchUser")
.set_weight(2)?
.register_transaction(transaction!(search).set_name("search")),
)
.set_default(GooseDefault::Host, "http://localhost:8080")?
.set_default(GooseDefault::Users, 50)?
.set_default(GooseDefault::RunTime, 300)?
.execute()
.await?;
Ok(())
}
set_wait_timeは各処理のあとに挟む待ち時間の範囲で、指定した2つの値の間からランダムに選ばれます。これを入れないと待ち時間ゼロで回り続けるため、前掲の実測のように現実離れした密度になります。人の操作を模したいなら必ず設定してください。set_defaultを使うとホストやユーザー数をコード側の既定値として持てますが、コマンドライン引数のほうが優先されます。
レスポンス内容の検証
ステータスコードが200でも、エラーページが返っていれば負荷テストとしては失敗です。公式が別crateとして提供しているgoose-eggsを使うと、本文に含まれるべき文字列まで検証できます。
use goose::prelude::*;
use goose_eggs::{validate_and_load_static_assets, Validate};
async fn view_index(user: &mut GooseUser) -> TransactionResult {
let goose = user.get("/").await?;
let validate = &Validate::builder()
.status(200)
.text("hello goose")
.build();
validate_and_load_static_assets(user, goose, validate).await?;
Ok(())
}
validate_and_load_static_assetsは、検証に加えてページ内の画像やCSSも読み込むため、ブラウザに近い負荷になります。検証に失敗したリクエストは、テスト終了時のメトリクスで失敗として集計されます。「200が返っているのに中身が空」という状態を見逃さないために、実運用の負荷テストではこの層を入れておく価値があります。
ログイン状態の引き継ぎ
ログインしてから認証済みページを回るシナリオでは、取得したトークンを後続の処理へ渡す必要があります。GooseはこれをGooseUser単位のセッションデータとして扱います。以下は公式リポジトリのexamples/session.rsを土台にしたもので、こちらもコンパイルを確認済みです。
use goose::prelude::*;
use serde::Deserialize;
struct Session {
jwt_token: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AuthenticationResponse {
jwt_token: String,
}
async fn login(user: &mut GooseUser) -> TransactionResult {
let params = [("username", "test_user"), ("password", "secret")];
let response = match user.post_form("/login", ¶ms).await?.response {
Ok(r) => match r.json::<AuthenticationResponse>().await {
Ok(j) => j,
Err(e) => return Err(Box::new(e.into())),
},
Err(e) => return Err(Box::new(e.into())),
};
user.set_session_data(Session {
jwt_token: response.jwt_token,
});
Ok(())
}
async fn view_profile(user: &mut GooseUser) -> TransactionResult {
let jwt_token = user.get_session_data_unchecked::<Session>().jwt_token.clone();
let request_builder = user
.get_request_builder(&GooseMethod::Get, "/profile")?
.bearer_auth(jwt_token);
let goose_request = GooseRequest::builder()
.set_request_builder(request_builder)
.expect_status_code(200)
.build();
let _goose = user.request(goose_request).await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), GooseError> {
GooseAttack::initialize()?
.register_scenario(
scenario!("AuthenticatedUser")
.register_transaction(transaction!(login).set_on_start())
.register_transaction(transaction!(view_profile)),
)
.execute()
.await?;
Ok(())
}
set_on_start()を付けたトランザクションは、そのユーザーが起動したときに1回だけ実行されます。ここでログインし、任意の構造体をset_session_dataで保存しておくと、以降のトランザクションからget_session_data_uncheckedで取り出せます。保存先はユーザーごとに独立しているため、ユーザー間でトークンが混ざることはありません。
後半では、ヘッダーを自分で組み立てるためにget_request_builderでReqwestのビルダーを取得し、GooseRequest::builder()経由でリクエストを構築しています。expect_status_code(200)を付けると、想定と異なるステータスコードが返った場合にメトリクス上で失敗として集計されます。
実在しないAPIを使ったコード例の見分け方
「シナリオ間でデータを共有する」という文脈で、Scenarioにset_shared_data()を呼び、user.shared_data()で読み書きするコード例が古い記事や生成AIの出力に見られます。しかしこれらのメソッドは0.18.1のソースに1件も存在せず、ビルドすると次のように失敗します。
error[E0599]: no method named `set_shared_data` found for struct `goose::goose::Scenario` in the current scope
error[E0599]: no method named `goose_send` found for mutable reference `&mut goose::goose::GooseUser` in the current scope
error[E0599]: no method named `body` found for struct `GooseResponse` in the current scope
error[E0061]: this method takes 2 arguments but 1 argument was supplied
error: could not compile `oldcode` (bin "oldcode") due to 4 previous errors
同様にuser.goose_send(request_builder)も現在は存在しません。user.post(path)を1引数で呼んで戻り値に.body()を繋ぐ書き方も通らず、postはパスとボディの2引数を取ります。参照したコードが動くかどうかは、読み進める前にcargo checkを通せば数秒で判別できます。
なお、全ユーザーで1つの値を共有するための専用APIをGooseは用意していません。Rustの通常の手段、つまりArcと同期プリミティブで包んだ値をクロージャで各処理に渡す形で実装します。ユーザーごとの状態で足りるなら、前掲のセッションデータのほうが素直です。
よくある質問
cargo install goose でインストールできないのはなぜですか?
Gooseが実行可能ファイルを持たないライブラリcrateだからです。0.18.1に対して実行すると「there is nothing to install in goose v0.18.1, because it has no binaries」というエラーで停止します。cargo newでプロジェクトを作り、依存としてGooseとTokioを追加してください。
Gooseの最新バージョンと必要なRustのバージョンは?
最新の安定版は0.18.1で、2025年8月14日に公開されました。その前は0.18.0(2025年2月25日)、さらに前は0.17.2(2023年8月28日)です。ビルドにはrustc 1.70.0以上が必要です。
同時ユーザー数や実行時間を指定しないとどうなりますか?
ユーザー数は実行マシンのCPU数が既定値として使われ、起動レートは毎秒1ユーザーです。実行時間を指定しない場合は停止させるまで走り続けます。一方--hostだけは既定値が無く、コード側でホストを設定していなければ起動時にエラーで終了します。
複数のサーバーに分散して負荷をかけられますか?
0.18.1ではできません。分散実行のGaggleは0.17.0で一時的に削除され、0.18.1でも復帰していません。公式ドキュメントは、この機能が必要な場合は0.16.4を使うよう案内しています。なお公式は、Gooseが1プロセスで全CPUコアを使うため1台あたりの生成能力は高いと説明していますが、複数台への分散が要件であれば別のツールを検討することになります。
AIエージェントの「goose」とは別物ですか?
別物です。Block社が公開しているAIコーディングエージェントも「goose」という名前ですが、この記事で扱っているのはRust製の負荷テストライブラリで、crates.ioで配布されているgooseクレートを指します。MongoDB向けのMongooseとも関係ありません。