loadtest, httperf でお手軽負荷テスト

go で作った WebAPI サーバの速度検証をしてみました。 単に GET リクエストを投げるだけでよいので、JMeter でなくて お手軽にできるツールでやりました。

loadtest

node.js 用のテストツールで 自分でリクエストを好きに定義できるのが特徴です。

例えば、パラメータをランダムに変えたければ requestGeneratorModule.js に

const querystring = require('querystring');

module.exports = function(params, options, client, callback) {
    const random = (max) => Math.floor(Math.random() * Math.floor(max));
    const getParams = { 'id': random(100) };
    const paramStr = querystring.stringify(getParams);

    options.path = options.path + '?' + paramStr

    return client(options, callback);
}

こんな感じで書いて

$ npx loadtest -R requestGeneratorModule.js webapi.example.com/path/to/api/

でいけます。

httperf

loadtest だと秒間あたりのリクエスト数を指定できないので、 こっちも入れてみました。

httperf --server webapi.example.com --port 80 --uri '/path/to/api/&id=100' --num-call 1 --num-conn 100 --rate 100

こんな感じで使えます。 この場合 --rate に 100 を指定してるので秒間 100 リクエストになります。

インストール

brew が使える環境でなかったので github から落としてきて自分でコンパイルしました。

$ git clone git@github.com:httperf/httperf.git
$ cd httperf
$ autoreconf -i
$ ./configure
$ make

python のスーパーセットな関数型言語 Coconut を試す

パイプライン演算子が使える言語がないか探していたところ Coconut というものを見つけました。

公式の説明を見ると

なにそれ面白そう。 という事で、ちょっと触ってみました。

インストールは

$ pip install coconut

でいけますが、http://coconut-lang.org/ の上の方に web の実行環境があるので、ちょっと試すならこれで十分です。

さっそく hello world

"hello, world!" |> print

次は無名関数 & 部分適用 & 関数合成。

add = (x, y) -> x + y
add1 = add$(1)

prd = (x, y) -> x * y
prd2 = prd$(2)

(add1 .. prd2)(1) |> print # -> 3
(prd2 .. add1)(1) |> print # -> 4

部分適用する関数には $ をつけるみたいですね。 これは Python との互換を保つために必要なんでしょうか。

最後にいつもの偶数自乗和

result = (
    range(10)
    |> filter$((n) -> n % 2 == 0)
    |> map$((n) -> n * n)
    |> reduce$((acc, n) -> acc + n)
)
print(result) # -> 120

python でも () の中なら自由に改行できるみたいなので パイプライン演算子も気持ちよく使えます。

インストール不要のチートシートツール cheat.sh

要は man の代わりに使うツールなんですが、 こいつのすごいところはインストールしなくても使えるところで、 例えば、ls の使い方を知りたい場合

$ curl cheat.sh/ls

だけで OK です。 DL して使うコマンドラインツールもあるみたいなんですが、 サーバーがいい感じに返してくれるんで、そのままでも十分。 他にも

$ curl cheat.sh/go
$ curl cheat.sh/go/func
$ curl cheat.sh/go/:learn

のように言語の使い方も教えてくれます。 詳しい使い方は

$ curl cheat.sh/
$ curl cheat.sh/:help

とかやると教えてくれます。 ブラウザから見る事もできるので、 気分や状況によって使い分けられるのもいいですね。

Go で errgroup を使って直列 vs 並列

直列 vs 並列シリーズ第 3 弾。 あんまり errgroup を使ったサンプルがなかったのでやってみた。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
    "time"

    "golang.org/x/sync/errgroup"
)

func createUrl(sec int) string {
    return fmt.Sprintf("http://httpbin.org/delay/%d", sec)
}

func requestBody(url string) (string, error) {
    response, err := http.Get(url)
    if (err != nil) {
        return "", err
    }
    defer response.Body.Close()

    bytes, err := ioutil.ReadAll(response.Body)
    if (err != nil) {
        return "", err
    }

    return string(bytes), nil
}

// errgroup を使って非同期に関数を実行し、結果を待つ
func awaitAll(funcs []func() error) error {
    eg := errgroup.Group{}
    for _, fn := range funcs {
        eg.Go(fn)
    }

    if err := eg.Wait(); err != nil {
        return err
    }
    return nil
}
func serial(urls []string) ([]string, error) {
    bodies := make([]string, len(urls))

    for _, url := range urls {
        body, err := requestBody(url);
        if (err != nil) {
            return nil, err
        }
        // fmt.Println(body)
        bodies = append(bodies, body)
    }

    return bodies, nil
}

func parallel(urls []string) ([]string, error) {
    mutex := new(sync.Mutex)
    bodies := make([]string, 0, len(urls))
    funcs := make([]func() error, 0, len(urls))

    for _, url := range urls {
        fn := func() error {
            body, err := requestBody(url);
            if (err != nil) {
                return err
            }

            // fmt.Println(body)
            mutex.Lock()
            bodies = append(bodies, body)
            mutex.UnLock()
            return nil
        }
        funcs = append(funcs, fn)
    }
    if err := awaitAll(funcs); err != nil {
        return nil, err
    }

    return bodies, nil
}

func main() {
    max := 4
    urls := make([]string, 0, max)
    for i := 1; i <= max; i++ {
        url := createUrl(i)
        urls = append(urls, url)
    }

    fmt.Println("serial")
    startTime := time.Now()
    serial(urls)
    fmt.Println(time.Now().Sub(startTime))

    fmt.Println("parallel")
    startTime = time.Now()
    parallel(urls)
    fmt.Println(time.Now().Sub(startTime))
}

動かしてみる

$ go version
go version go1.9.2 linux/amd64
$ go get golang.org/x/sync/errgroup
$ go run concurrent.go
serial
11.157001925s
parallel
4.398581223s

ちゃんと並列で実行されてる。

PHP の Guzzle で直列 vs 並列

前やった node.js の Promise で直列 vs 並列を php でやってみた。 素の php だと非同期処理はめんどくさいので 今回は Guzzle を使ってみた。

Guzzle は非同期もできる http クライアントだけど、 Guzzle Promises を使えば 一般的な処理も非同期で書けるみたい。

<?php
/*
 * PHP の Guzzle で直列 vs 並列
 */

require './vendor/autoload.php';

function createUrl(int $sec) {
    return 'http://httpbin.org/delay/' . $sec;
}

function serial(array $urls) {
    $client = new \GuzzleHttp\Client();

    $bodies = [];
    foreach ($urls as $url) {
        $response = $client->request('GET', $url);
        $bodies[] = $response->getBody()->getContents();
    }
    return $bodies;
}

function parallel(array $urls) {
    $client = new \GuzzleHttp\Client();

    $promises = [];
    foreach ($urls as $url) {
        $promises[] = $client->requestAsync('GET', $url);
    }
    $responses = \GuzzleHttp\Promise\all($promises)->wait();

    $bodies = [];
    foreach ($responses as $response) {
        $bodies[] = $response->getBody()->getContents();
    }
    return $bodies;
}

function main() {
    $urls = [];
    foreach (range(1, 4) as $sec) {
        $urls[] = createUrl($sec);
    }

    $startTime = new \DateTime();
    serial($urls);
    $diff = $startTime->diff(new \DateTime());
    var_dump('serial: '. $diff->format("%s sec"));

    $startTime = new \DateTime();
    parallel($urls);
    $diff = $startTime->diff(new \DateTime());
    var_dump('parallel: '. $diff->format("%s sec"));
}
main();

動かしてみる。

$ composer require guzzlehttp/guzzle
$ php main.php
/path/to/main.php:50:
string(14) "serial: 11 sec"
/path/to/main.php:55:
string(15) "parallel: 4 sec"

ちゃんと並列で動いてるみたい。

php 7.2.4 で確認

AWS の ECS & Fargate でゆるいバッチ環境を作る

仕事で以下のようなバッチを作る事になったのでその記録をメモしとく。

  • 月 1 回実行
  • 内容は手動でやってた作業を自動でやって結果をメールするだけ
  • 実行には 30 分ほどかかる
  • 全然クリティカルじゃない
    • エラーが発生しても連絡があってから再実行すればよい
    • 再実行もすぐでなくてもよい

で、考えた事がこちら

  • 運用工数かけたくない
    • cron 等で定期実行
    • サーバレス
  • そんなに変更ないだろうからデプロイは自動でなくてよい
  • なるべく安くすませたい

AWS でいくつかやり方があるけど、どれがいいか検討してみる。

  • Lambda + ClowdWatch Events
    • サーバレス
    • デプロイは zip 上げるだけ
    • cron 風の記述で定期実行可能
    • 実行時間の上限が 5 分なので今回は使えず
  • Elastic Beanstalk の worker 環境
    • デプロイ、切り戻しが楽なのはよい
    • docker も使える
    • cron 風定期実行可
    • ただし、安くもないし、サーバレスでもない
      • t2.micro でも一ヶ月で 1000 円強かかる
    • 一旦保留、他の方法でダメだった時はこれで
  • AWS Batch
    • サーバレス
    • 裏は docker なので使える技術はわりと自由
    • 定期実行はあまり得意ではないみたい
  • ECS + EC2
    • docker
    • ScheduleTask でやれば cron 記法で定期実行できる
    • サーバレスでないし安くもない
  • ECS + Fargate
    • docker
    • Fargate なので使った分だけ課金
    • 東京リージョンにない
      • 注: これやった当時はまだでした
    • Fargate はまだ ScheduleTask で使えないみたい

で、安さと運用を考えて ECS + Fargate で行くことにした。

SES の設定

今回は送信制限を解除してない SES を使ったので、 予め送信先のメールアドレスを登録しておいた。

プログラム作成 & docker 化

バッチとして実行するプログラムを作る。 docker なので言語は自由に選べばよいが、今回は node.js で。 メールは aws-sdk 経由で SES で送る。

プログラムができたら Docker 化する。 Fargate は pull が遅いらしいんで、軽量化のため ベースイメージは node の alpine にした。

ECS の設定

Fargate & SES が使える us-east-1 に構築する。

  • ECR にバッチのリポジトリを作って Docker イメージを push する。
  • クラスタの作成
    • クラスタとは ECS の実行環境みたいなもんらしい。
    • ECS のコンソールから
    • クラスターの作成」を選んで
    • 「ネットワーキングのみAWS Fargate を使用)」の
    • クラスタテンプレートを選んで
    • 後はよしなに作る。
  • タスク定義
    • 実行するイメージとかロール、メモリ、CPU を設定する
    • ECS のコンソールから
    • 「新しいタスク定義の作成」を選んで後はよしなに
    • コンテナは先程追加したやつを選択。
  • スケジュール設定

まとめ

という事で、ECS & Fargate を使って いい感じのバッチ環境を作ることができた。 cron もサーバレスにしたければ Lambda & ClowdWatch Events で いけるかもだけど、力尽きたので今回はここまで。

補足

今回はバッチ 1 つだけだったんで Fargate を選択したけど、 バッチサーバ立てるくらいの規模なら クラスタを EC2 で組んで ScheduleTask にした方がよいかも。

ただし、pull してから実行までに少しタイムラグがあるので、 起動時刻に厳密なケースでは他の方法の方がよいと思われる。

typescript の async/await で直列 vs 並列

typescript でちょっとやってみました。

http://httpbin.org/delay/{delay} というのは delay 秒待ってからレスポンス返してくれる URL です。

/*
 * serial vs parallel
 */
import request from 'request-promise-native';

function createUrl(sec: number): string {
    const secStr: string = String(sec);
    return `http://httpbin.org/delay/${secStr}`;
}

async function serial(urls: string[]): Promise<string[]> {
    let results = [];
    for (const url of urls) {
        const ret = await request(url);
        results.push(ret);
    }
    return results;
}

async function parallel(urls: string[]): Promise<string[]> {
    const promises = [];
    for (const url of urls) {
        const promise = request(url);
        promises.push(promise);
    }
    const results = await Promise.all(promises);
    return results;
}

async function main() {
    const range = [1, 2, 3, 4];
    const urls = range.map(createUrl);

    let startTime = Date.now();
    let results = await serial(urls);
    console.log('serial');
    // 1 + 2 + 3 + 4 = 10 秒かかる
    console.log((Date.now() - startTime) / 1000);

    startTime = Date.now();
    results = await parallel(urls);
    console.log('parallel');

    // max(1, 2, 3, 4) = 4 秒かかる
    console.log((Date.now() - startTime) / 1000);
}
main();

動かしてみる。

$ mkdir test; cd $_
$ pbpaste > index.ts
$ npm init -y
$ npm i -S typescript @types/node
$ npm i -S request @types/request
$ npm i -S request-promise-native @types/request-promise-native
$ npx tsc --init
$ vim .tsconfig # target を es2017 に
$ npx tsc
$ node -v
v8.4.0
$ node ./index.js
serial
11.58
parallel
4.373

ちゃんと並列で動いてますね。