Go言語を含む複数種類の言語により実装されたソフトウェアのベンチマーク

本稿では、Goを含む複数種類の言語により実装されたソフトウェア(具体的には、シャドウプロキシサーバ)のベンチマークを行うことで、各言語間の差異について考察します。

# シャドウプロキシサーバとは？

Webアプリケーションのテストをいくら入念に整備したとしても、実際に起こり得るすべての可能性を網羅することは難しいのはもちろん、同じコードを複数の環境(言語やフレームワーク、ミドルウェア等のバージョン)で同じように動作することを保証するのは至難の業です。そこで、ユーザの実リクエストを複製して、稼働中の環境とは別に用意したテスト用の環境にもリクエストを送ることで検証をするというアプローチがとられます。そのために使われるツールのことを、シャドウプロキシサーバ(shadow proxy server)と呼びます。

最近では、[気軽なMySQLバージョンアップ - まめ畑](http://d.conma.me/entry/2013/12/10/000046)において、MySQLのバージョンアップに際して、[Kage](https://github.com/cookpad/kage)というソフトウェアを利用する事例が紹介されています。Kageは、リバースプロキシとアプリケーションサーバの中間に配置され、リバースプロキシからきたリクエストを複製して、本番・テスト環境の両方に同時にリクエストを送ります。テスト環境で問題があったとしても、ユーザへのレスポンスには影響がないよう配慮されています。

# 他言語での実装

このように便利なツールなので、Rubyで実装されたKageの以外にも、他言語での実装があります。

  * [Geest](https://github.com/lestrrat/p5-Geest) (Perl)
  * [Delta](https://github.com/kentaro/delta) (Go)

Perl実装は[lestrratさん](http://lestrrat.ldblog.jp/)が、Go実装は私が書いたものです。やってる事自体はわりとシンプルなのでどの言語で実装してもよさそうなものですが、まあ、Goはこういうのが得意分野だろうので、いちばん速く効率のよい実装を目指したいところです。というわけで、まずは以上の実装のベンチマークをとってみることにしましょう。

# ライブラリの使い方

ベンチマークの前に、ライブラリの簡単に使い方を紹介しておきましょう。

上記実装のいずれもが、単体のアプリケーションではなくライブラリとして提供されています。これは、要件に応じてカスタマイズしたいからというのがその理由でしょう。Go実装であるDeltaの使用例は以下のような感じです。

```go
package main

import (
        "github.com/kentaro/delta"
        "net/http"
)

func main() {
        server := delta.NewServer("0.0.0.0", 8080)

        server.AddMasterBackend("production", "127.0.0.1", 8081)
        server.AddBackend("sandbox", "127.0.0.1", 8082)

        server.OnSelectBackend(func(req *http.Request) []string {
                return []string{"production", "sandbox"}
        })

        server.Run()
}
```

このコードは以下のことを行っています。

  1. `production`および`sandbox`という名前で、本番とテスト環境を模したバックエンドを設定している
  2. `OnSelectBackend`で、リクエストをどのようにふりわけるのかというロジックを指定している(ここではなにも考えず、本番とテストとのどちらも返している)

他にもうちょっといろいろ機能がありますが、RubyおよびPerl実装ともに、本質的にはだいたいこんな感じです。

# ベンチマーク戦略

以下の要領でベンチマークを取ることにします。各言語による実装が、最大でどれぐらいさばけるのかをベンチマークによって見極めることを目的としています。

  1. バックエンドが律速となることのないよう、静的ファイルを返すだけのnginxを用いる
  2. シャドウプロキシサーバの実装は、最低限にする(上記のGoの例と同じく、単に本番とテスト用ひとつずつの組みにリクエストを流すだけ)
  3. シャドウプロキシサーバおよびバックエンドの構成は下図の通り、最もシンプルな構成とする
  4. `ab`コマンドを用いて、ベンチマークを行う。具体的には、`ab -n 1000 -c 100 127.0.0.1:8080/`
  5. 実行時環境等の影響によりスコア揺れに対処するため、上記コマンドを各10回ずつ実行し、その平均をとる

上記戦略に基づき、下記のコードを用いてベンチマークを実施します。

https://github.com/kentaro/shadow-proxy-servers

![](http://cdn-ak.f.st-hatena.com/images/fotolife/a/antipop/20131212/20131212195546.png)

# ベンチマーク環境

## ハードウェア

```
$ system_profiler SPHardwareDataType
Hardware:

    Hardware Overview:

      Model Name: MacBook Air
      Model Identifier: MacBookAir4,1
      Processor Name: Intel Core i7
      Processor Speed: 1.8 GHz
      Number of Processors: 1
      Total Number of Cores: 2
      L2 Cache (per Core): 256 KB
      L3 Cache: 4 MB
      Memory: 4 GB
      ...
```

## ソフトウェア

### nginx

```
$ nginx -v
nginx version: nginx/1.4.4
```

### Perl

```
$ perl -v

This is perl 5, version 18, subversion 1 (v5.18.1) built for darwin-2level
```

### Ruby

```
$ ruby -v
ruby 2.0.0p353 (2013-11-22 revision 43784) [x86_64-darwin13.0.0]
```

### Go

```
$ go version
go version go1.2 darwin/amd64
```

### ab

```
$ httpd-2.4.7/support/ab -V
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
```

# ベンチマーク結果

バックエンドのnginxに対してシャドウプロキシサーバを通さずにアクセスした場合の結果も合わせて掲載します。各実装間の差異のみならず、nginx直接アクセスとの値と比較することで、より意味のある知見が得られるでしょう。また、各実装で10回ずつベンチマークを実行し、その平均を取ることで実行環境等に影響されたスコアの揺れに対処します。

単位: (リクエスト数/sec)

|回数|nginx  |Perl  |Ruby   |Go     |
|---:|------:|-----:|------:|------:|
|   1|3217.84|338.80|1101.49|1557.97|
|   2|5027.93|278.79| 764.77|1455.50|
|   3|4604.31|316.24| 785.72|1204.48|
|   4|4733.23|329.41| 775.88|1440.05|
|   5|4873.53|299.89| 543.79|1443.44|
|   6|4917.17|325.41| 938.19|1413.41|
|   7|4716.69|317.81| 960.73|1180.70|
|   8|4749.33|299.33| 873.63|1520.07|
|   9|4705.00|297.74| 892.25|1466.94|
|  10|4706.83|288.71| 972.83|1401.60|
|平均|4625.19|309.21| 860.93|1408.41|

# 考察

上記の結果から、

  * 各実装ともに、nginxへの直接アクセスに比較すると大きくスコアを下げています。
  * Goが一番速いようです
  * ベンチマーク時の、`ab`で取れる以外のメトリクス(CPU使用率など)について考慮していないので、そちらも見るほうがよさそう

といった考察が得られました。

# 議論

## Go実装の問題

ところで、議論が煩雑になるので記述を避けましたが、Go実装の実行には問題があります。上記の結果は、単純に10回分の結果を掲載していますが、Go実装に関しては、その間に以下のようなエラーによりベンチマークが完走できないことが頻発しました。ただし、完走することもあります。何が起こっているのかよくわかりません。私、匙を投げました(この件、最後に追記あり)。

つまり、上記の結果は、Goによる実装が上記のエラーでこけなかった10回分の集計ということだ。なんということだろう！びっくりですね！

```
...
21:01:13 go.1   | goroutine 1079 [select]:
21:01:13 go.1   | net/http.(*persistConn).roundTrip(0xc21056d900, 0xc2101add70, 0xc21056d900, 0x0, 0x0)
21:01:13 go.1   |       /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/transport.go:879 +0x6d6
21:01:13 go.1   | net/http.(*Transport).RoundTrip(0xc210057200, 0xc2106e3a90, 0x1, 0x0, 0x0)
21:01:13 go.1   |       /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/transport.go:187 +0x391
21:01:13 go.1   | net/http.send(0xc2106e3a90, 0x56c290, 0xc210057200, 0x0, 0x0, ...)
21:01:13 go.1   |       /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/client.go:168 +0x37f
21:01:13 go.1   | net/http.(*Client).send(0xc2106e74b0, 0xc2106e3a90, 0x16, 0x1, 0xc2101add60)
21:01:13 go.1   |       /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/client.go:100 +0xd9
21:01:13 go.1   | net/http.(*Client).doFollowingRedirects(0xc2106e74b0, 0xc2106e3a90, 0x2af1f8, 0x0, 0x0, ...)
21:01:13 go.1   |       /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/client.go:294 +0x671
21:01:13 go.1   | net/http.(*Client).Do(0xc2106e74b0, 0xc2106e3a90, 0x485dc0, 0x0, 0x0)
21:01:13 go.1   |       /usr/local/Cellar/go/1.2/libexec/src/pkg/net/http/client.go:129 +0x8f
21:01:13 go.1   | github.com/kentaro/delta.(*Handler).dispatchProxyRequest(0xc2100001f0, 0xc210036960, 0xc2106b34e0, 0xc2106a7840, 0xc2106abee0)
21:01:13 go.1   |       /Users/usr0600239/.go/src/github.com/kentaro/delta/handler.go:71 +0xb0
21:01:13 go.1   | created by github.com/kentaro/delta.(*Handler).ServeHTTP
21:01:13 go.1   |       /Users/usr0600239/.go/src/github.com/kentaro/delta/handler.go:29 +0x1fa
21:01:13 go.1   |
21:01:13 go.1   | goroutine 1078 [chan receive]:
21:01:13 go.1   | github.com/kentaro/delta.func·001()
21:01:13 go.1   |       /Users/usr0600239/.go/src/github.com/kentaro/delta/handler.go:38 +0x80
21:01:13 go.1   | created by github.com/kentaro/delta.(*Handler).ServeHTTP
21:01:13 go.1   |       /Users/usr0600239/.go/src/github.com/kentaro/delta/handler.go:52 +0x258
21:01:13 go.1   |
21:01:13 go.1   | goroutine 1076 [select]:
21:01:13 go.1   | exited with code 2
21:01:13 system | sending SIGTERM to all processes
21:01:13 go.1   | exited with code 0
```

## リクエスト回数を増やした場合

上記は`ab -n 1000 -c 100 127.0.0.1:8080/`というコマンドによるベンチマーク、すなわち総実行回数は1,000回でした。これを、10倍の10,000回にしてみるとどうなるでしょう。

  * Perl: スコアは半分程度に下がるが10,000回のリクエストを完全にさばききる
  * Go: リクエストが5,000を超えたあたりでつまってきて、`ab`コマンドがタイムアウトする(その際、上述のエラーが出たりでなかったりする)
  * Ruby: リクエストが5,000を超えたあたりでつまってきて、下記のようなエラーが頻発して`ab`コマンドがタイムアウトする

```
21:10:42 ruby.1 | 21:10:42 [422c47955b4cb152] Server(s) disconnected before response returned: [:production, :sandbox]
21:10:42 ruby.1 | 21:10:42 [865d880680799abd] Server(s) disconnected before response returned: [:production]
21:10:42 ruby.1 | 21:10:42 [865d880680799abd] Master backend closed connection. Closing downstream
```

Go, Rubyともに同じぐらいのリクエスト数で動作に支障をきたしてくるので、これは実行環境の問題(後述)かもしれません。にもかかわらず、Perl実装は完走します。すごいですね。

file discripter数の問題かと思って以下のようにかなりあげてみましたが、10,000回の試行の場合は、GoとRubyについては上記と同じでした。

```
$ sudo sysctl -n kern.maxfilesperproc
65536
```

# 提案

というわけで、いろいろと興味深い結果を得ることができました。しかし、Go Advent Calendarの一環としてのこのエントリとしては、Goの優位性を示したいところでしたが、私の力不足によりそれもかなわないこととなってしまいました(ただまあ、上記のエラーさえ直せれば、今回比較した中ではいちばん速い実装にはなりそうです)。

ところで、今日は私の誕生日です。そこで提案です。技術力に自信のある皆さんは上記したような問題を解決することにより、金銭力に自信のある皆さんはウィッシュリストからプレゼントを送ることにより、今日という聖なる日を祝いでみてはいかがでしょうか。

  * 技術力: https://github.com/kentaro/shadow-proxy-servers
  * 金銭力: http://www.amazon.co.jp/gp/registry/wishlist/3QRFGIJDY0ANF/

以上、よろしくご査収くださいますよう、お願い申し上げます。

# 追記

口頭ベースですが、ある環境(多分Linux on EC2)で900req/secぐらいのリクエストを流したところ、特に問題なく動いていたということです。

また、下記のような追試も行われています。

> 手元のmba(gentoo、kernel-3.9.0)でやってみたところ直nginxは25000、Goは3000、PerlとRubyは同様のスコア。だが10000回でもエラーはでない。1000回と同じようなスコア出た。OSXの問題だろうか。

http://b.hatena.ne.jp/tmatsuu/20131215#bookmark-173062470