GoでHTTP負荷テストフレームワークをつくった

  • 22
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

最近、HTTP負荷テストをしたいという要求が高まり、負荷テストツール/フレームワークを探しました。
要件としては、以下のような具合です。

  • シナリオを自由にかつ楽に組めること(できればコードで)
    • 同時に複数のパスへのリクエストを送信できること(単体でリクエストも可能であること)
    • 全てのリクエストに対してそれぞれ異なるリクエストヘッダやリクエストパラメータなどを付与できること
  • 並列性があり、ある程度のパフォーマンスが出ること
  • 依存が(少)なく、どこでも動かせるとうれしい

JMeterやSiegeやwrkなどいろいろ見てはみましたが、なんかどれも帯に短したすきに長しという具合で微妙というかんじになってしまいました。

そこで、これらを全て満たすやつをGoで書きました。
Goで書いたのは依存を少なくするためと、並列性能に期待したからですが、Goを書いてみたかったからというのもありました。Goを試すには打って付けのお題だと思ったのです。
なので、ほとんど初めて書いたGoコードという感じです。

こんな具合で使えます。

package main

import (
    "github.com/karupanerura/gostress"
    "log"
    "math/rand"
    "runtime"
    "time"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    rand.Seed(time.Now().UnixNano())

    client := gostress.NewHttpClient(
        gostress.HttpClientConfig{
            Server: gostress.ServerConfig{
                Hostname: "myhost.com",
                Secure: false,
            },
            Headers: map[string]string{},
            UserAgent: "Gostress/alpha",
            MaxIdleConnsPerHost: 1024,
            RequestEncoder: &gostress.JsonRequestEncoder{},
            ResponseDecoder: &gostress.JsonResponseDecoder{},
        },
    )
    state := map[string]string{}
    context := gostress.NewScenarioContext(client, state)
    scenario := makeScenario()
    context.Run(scenario)
}

func makeScenario() gostress.Scenario {
    scenarios := gostress.NewSeriesScenarioGroup(256) // 直列で実行するシナリオ
    scenarios.MinInterval =   500 * time.Millisecond
    scenarios.MaxInterval = 10000 * time.Millisecond
    scenarios.Next(makeHTTPScenario("GET", "/", nil))
    scenarios.Next(
        &gostress.SleepScenario{Duration: 1 * time.Millisecond},
    )
    scenarios.Next(
        gostress.NewConcurrentScenarioGroup(3).Add(// 並列で実行するシナリオ
            makeHTTPScenario("GET", "/api/foo", nil),
        ).Add(
            makeHTTPScenario("GET", "/api/bar", nil),
        ).Add(
            makeHTTPScenario("GET", "/api/baz", nil),
        ),
    )
    scenarios.Next(
        &gostress.DelayScenario{// 一定時間後に発火するシナリオ
            Duration: 1 * time.Millisecond,
            Scenario: makeHTTPScenario("GET", "/api/hoge", nil),
        },
    )
    scenarios.Next(
        &gostress.DeferScenario{
            Defer: func (state gostress.ScenarioState) gostress.Scenario {
                // 遅延して実行するシナリオを決定する
                return makeHTTPScenario("GET", "/api/fuga", nil)
            },
        },
    )
    scenarios.Next(
        &gostress.DeferScenario{
            Defer: func (state gostress.ScenarioState) gostress.Scenario {
                // 遅延して実行するシナリオを決定する
                return &gostress.NoopScenario{} // なにもしない
            },
        },
    )
    return scenarios
}

func makeHTTPScenario(method, path string, content interface{}) gostress.Scenario {
    return &gostress.HttpScenario{
        Method:  method,
        Path:    path,
        Content: content,
        OnComplete: func(state gostress.ScenarioState, res *gostress.HttpResponse, duration time.Duration) {
            log.Printf("method:%s\tpath:%s\tstatus:%d\ttime:%f", method, path, res.StatusCode, duration.Seconds())
        },
        OnError: func(state gostress.ScenarioState, err error) {
            log.Printf("Error: %s", err)
        },
    }
}

性能的には、別ネットワークから1台で数千並列で動かして、JSONのREST APIだけのリクエストで数百Mbps出せるくらいの性能はあるようです。
いったんGithubに公開してありますが、えいやで作ったので、まだテストもドキュメントも書けていないです。
近いうちに整えたい。

明日は @htk291 さんです!