Web API用の負荷テストツールで遊んでみる

これはWeb API Advent Calendar 2014の16日目の記事です。

はじめに

Web API Advent Calendar 2014 8日目ではkazuchikaさんよりApigee製のAPI負荷ツールapibの紹介がありました。

この記事ではapibに加えてBoomというGolang製の類似ツールを簡単に紹介し、様々なマイクロWebフレームワークに対して両者を使ってみます。

Boom

rakyll / boom

インストール

Golang環境がセットアップされているものとします。

go get github.com/rakyll/boom

実行

boom -n 回数 -c 並列度 [URL]

その他オプションはboomコマンドで閲覧できます。

apib

apigee / apib

  • C言語
  • Apigeeエンジニアが開発
  • リクエスト回数ではなく計測時間を指定する
  • Warm upの時間を指定できる
  • リモートサーバにデーモンを仕込んだ測定もできる

インストール

brew install apib

実行

apib -d 計測時間 -c 並列度 [URL]

その他オプションはapibコマンドで閲覧できます。

実行結果

複数プログラミング言語のマイクロフレームワークを用いて単純なWebAPIを作成して負荷テストツールを実行してみます。条件は以下です。

  • boomの場合、並列度100で1万リクエスト
  • apibの場合、並列度100で10秒

Node.js - Express

Node.jsのスタンダードなWAFです。

var express = require('express')
var app = express()
var http = require('http');
http.globalAgent.maxSockets = 50;

app.get('/', function (req, res) {
  res.send('Hello')
})

var server = app.listen(3000, function () {
})

f:id:slowquery:20141216221017p:plain

f:id:slowquery:20141216221101p:plain

Node.js - Restify

Expressに似ていますが、Web APIに特化しています。

var restify = require('restify');
http = require('http');
http.globalAgent.maxSockets = 50;

function respond(req, res, next) {
    res.send('hello');
    next();
}

var server = restify.createServer();
server.get('/', respond);

server.listen(3000, function(){});

f:id:slowquery:20141216221128p:plain

f:id:slowquery:20141216221138p:plain

Node.js - Hapi

Walmart Lab製のWAFです。自由度の高いExpressや類似フレームワークに対して、ある程度統制を効かせてコードをクリーンに保とうという意識を感じます。WebAPIのルーティングも可読性が高いです。日本ではあまり知られていないように思いますが、小売系の他npm、Mozillaなどtech系企業のユースケースも目立ちます。

var Hapi = require('hapi');
var http = require('http');
http.globalAgent.maxSockets = 50;

var server = new Hapi.Server();
server.connection({ port: 3000 });

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        reply('Hello');
    }
});

server.start(function () {});

f:id:slowquery:20141216221156p:plain

f:id:slowquery:20141216221209p:plain

Ruby - Sinatra

言わずと知れたマイクロフレームワークです。フォロワーのWAFが大量に出たのがわかるミニマルな記法です。WEBRickは遅かったためThinに変更しています。

require 'sinatra'

set :server, 'thin'
set :logging, nil

get '/' do
  'Hello'
end

f:id:slowquery:20141216221231p:plain

f:id:slowquery:20141216221244p:plain

Ruby - Espresso

速度が売りのマイクロフレームワークです。こちらもSinatraに合わせてThinに変更しています。期待してなかったのですが検討しています。

require 'e'

class App < E
  map '/'

  def index
    "Hello"
  end
end

App.run :server => :Thin

f:id:slowquery:20141216221257p:plain

f:id:slowquery:20141216221305p:plain

Python - bottle

Bottle: Python Web Framework

恐らくPythonで最もミニマルなWAFで個人的にもよく使っています。WSGIに対応していてアプリケーションサーバFacebook製のTornadoに変更しました。

from bottle import route, run


@route('/', methods=['GET'])
def index():
    return "Hello"

run(server='tornado', host="0.0.0.0", port=3000, debug=False)

f:id:slowquery:20141216221318p:plain

Haskell - Scotty

Sinatraに影響を受けたHaskellのマイクロWebフレームワークです。Haskellの中では最も親しみやすい印象でした。この記法でこの性能はいい感じです。

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty

main = scotty 3000 $ do
    get "/" $ do
        text "Hello"

f:id:slowquery:20141216221342p:plain

f:id:slowquery:20141216221350p:plain

Go - Martini

DIの仕組みをもったGoのマイクロフレームワークです。通常の設定だと標準出力にログが出て速度がやや遅くなったのでzishe/app.goを参考にログを消しています。Golangの人気がうかがえる速度です。

package main

import (
    "log"
    "github.com/go-martini/martini"
)

func init(){
    martini.Env = martini.Prod
}

type myClassic struct {
    *martini.Martini
    martini.Router
}

func withoutLogging() *myClassic {
    r := martini.NewRouter()
    m := martini.New()
    m.Use(martini.Recovery())
    m.MapTo(r, (*martini.Routes)(nil))
    m.Action(r.Handle)
    return &myClassic{m, r}
}

func main() {
    m := withoutLogging()
    m.Get("/", func(lg *log.Logger) string {
        return "Hello"
    })
    m.Run()
}

f:id:slowquery:20141216221416p:plain

f:id:slowquery:20141216221424p:plain

Go - beego

MartiniよりリッチなGoのフレームワークです。マイクロフレームワークかと思っていたらフルスタックとのこと。フルスタックかつ高性能と謳っています。

package main

import (
    "github.com/astaxie/beego"
)

type MainController struct {
    beego.Controller
}

func (this *MainController) Get() {
    this.Ctx.WriteString("Hello")
}

func main() {
    beego.Router("/", &MainController{})
    beego.Run()
}

f:id:slowquery:20141216221442p:plain

f:id:slowquery:20141216221450p:plain

注意

この記事はフレームワークの性能を表すものではありません。以下のようにフレームワーク周辺領域での選択やアプリやミドルウェアの構成による影響が大きく、何をもって"フレームワークの性能"と定めるか容易には決められないからです。

  • ディスクやネットワークのIO。その性能もORMやコネクタに左右されます。
  • シリアライズやバリデーションなど入れ替え可能な関連ライブラリ
  • PythonWSGIRubyのRackのようにWebサーバとWebアプリケーションフレームワーク間の統一インタフェースが提供されている場合、Webサーバの選択が性能に大きく影響します。
  • ルーティングを辞書でなく配列に格納してマッチングするWAFの場合、探索の計算量がO(N)となるため登録されているルーティングが多くなると性能が劣化してしまいます。

※サンプルコードはテキストでHelloという文字列を返すだけでWeb APIとしては不完全です。

感想

WebAPIの負荷テストツールを使うことで新たなフレームワークを試すのが楽しくなりました。この記事以外にも様々なフレームワークを試した結果、記法が美しいものから魅力的なレスポンスタイムを持つものまで様々なフレームワークに出会えましたが、その中で共通して感じたのはWeb APIを実装する敷居が低くなったということです。

昨今、サービスがWeb APIを公開するのはもちろんのこと、Web APIが組み込まれたソフトウェアも目立つようになりました。ソフトウェアのWebAPIがこの記事で使ったようなマイクロフレームワークで実装されていることもあります。

このような流れが加速してサービスもソフトウェアもWeb APIを持つのが当たり前になり、誰もがコードから簡単に操作できる自由な世界が訪れるといいですね。

Enjoy!