2010年06月18日

Twisted vs Tornado vs Go で非同期Webサーバー対決

はてなブックマークに登録

昨日の takada-at の記事で「サーバー側では単純に100ms待ってからレスポンスを返すだけのページを用意しておき、」とあったのですが、今日はそのサーバー側の話をします。

もともとこのサーバーを作った動機は、takada-at が作成中の負荷試験システムがちゃんと並列に負荷をかけられるかどうかを検証するためでした。 すぐにレスポンスを返してしまうと、負荷試験スクリプトがきちんと並列に負荷をかけられなくても PV/sec が出てしまいます。 そこで、 epoll を使って高速に並列接続を扱えるTwistedフレームワークを使って、100msの遅延をしつつ数千PV/secに耐えるWebサーバーを作ってみました。 さらに、同じく epoll を使っている Tornado や Go にも興味があったので、こちらでも同じものを作成し、パフォーマンスを比較してみました。

コード

まずは、コードを見てみましょう。

Twisted Python:

from twisted.internet import epollreactor
epollreactor.install()

from twisted.web import server, resource
from twisted.internet import reactor

class Lazy(resource.Resource):
    isLeaf = True
    def __init__(self, delay=0.1):
        self._delay = delay

    def render_GET(self, request):
        def after():
            request.write('Hello, World!')
            request.finish()
        reactor.callLater(self._delay, after)
        return server.NOT_DONE_YET

if __name__ == '__main__':
    import sys
    delay = 0.1
    try:
        delay = float(sys.argv[1]) / 1000
    except Exception:
        pass
    site = server.Site(Lazy(delay))
    reactor.listenTCP(8000, site)
    reactor.run()

次は、Tornadoです。

from tornado import httpserver, ioloop, web

from time import time

DELAY = 0.1

class MainHandler(web.RequestHandler):
    @web.asynchronous
    def get(self):
        def _hello():
            self.write("Hello, World!")
            self.finish()
        ioloop.IOLoop.instance().add_timeout(time() + DELAY, _hello)

application = web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    import sys
    try:
        DELAY = float(sys.argv[1]) / 1000
    except Exception:
        pass
    http_server = httpserver.HTTPServer(application)
    http_server.listen(8001)
    ioloop.IOLoop.instance().start()

最後に、Goです。

package main

import (
    "flag"
    "http"
    "io"
    "log"
    "time"
    )

var addr = flag.String("addr", ":9000", "http service address")
var delay = flag.Int64("delay", 100, "response delay[ms]")

func main() {
    flag.Int64Var(delay, "d", 100, "response delay[ms]")
    flag.Parse()
    http.Handle("/", http.HandlerFunc(hello))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Exit("ListenAndServe:", err)
    }
}

func hello(c *http.Conn, req *http.Request) {
    time.Sleep(int64(1000000) * int64(*delay))
    io.WriteString(c, "Hello, World!\n")
}

TwistedやTornadoは、ループに後で呼び出す関数を登録していていて明らかにイベントドリブンフレームワークという感じがします。 それに対してGoは Sleep() を使っていて、一見普通のスレッドべースの並列処理に見えます。 しかし、Goはネイティブスレッドではなくgoroutineという軽量なユーザーランドスレッドを利用しており、そのスケジューラは(Linuxの場合)epollを利用しています。通常のスレッドを使うよりも効率的に動作するはずです。

ベンチマーク

それでは、この3つにそれぞれ負荷をかけてみましょう。といっても、100msの遅延だと負荷をかけるのが大変なので、10msの遅延で実行し、ab -c100 -n10000で実験してみます。

Twisted:
Requests per second:    2620.31 [#/sec] (mean)
Time per request:       38.163 [ms] (mean)
Time per request:       0.382 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     31
  66%     32
  75%     33
  80%     37
  90%     38
  95%     38
  98%     40
  99%     44
 100%   3082 (longest request)


Tornado:
Requests per second:    5299.04 [#/sec] (mean)
Time per request:       18.871 [ms] (mean)
Time per request:       0.189 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     19
  66%     19
  75%     19
  80%     19
  90%     19
  95%     19
  98%     21
  99%     22
 100%     33 (longest request)


Go:
Requests per second:    4712.63 [#/sec] (mean)
Time per request:       21.220 [ms] (mean)
Time per request:       0.212 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     19
  66%     20
  75%     20
  80%     21
  90%     32
  95%     40
  98%     42
  99%     42
 100%     48 (longest request)

TornadoやGoに比べると、Twistedが残念な結果になっています。 これは、Twistedが比較的汎用的で高機能なフレームワークということもありますが、遅延実行する関数をリストに格納していて、実行する関数があるかどうかをリストを全部チェックしているのが大きそうです。(Tornadoはソート済みリストを利用していてます)

Goも、コンパイラ型なのにTornadoに負けてるのが残念ですが、epollから直接イベントドリブンしているTornadoに比べると goroutine という抽象化レイヤを1枚はさんでいるので仕方ないかもしれません。

さらに高速化

まずはTornadoのJITによる高速化に挑戦してみます。PyPy-1.2が一番よさそうですが、セットアップが面倒なのでお手軽なpsycoを使ってみました。もとのソースコードに、次の2行を追加します。

import psyco
psyco.full()

結果はこうなりました。

Requests per second:    7503.16 [#/sec] (mean)
Time per request:       13.328 [ms] (mean)
Time per request:       0.133 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     13
  66%     13
  75%     14
  80%     14
  90%     14
  95%     14
  98%     15
  99%     15
 100%     23 (longest request)

5299req/sec => 7503req/sec で、20%以上高速化できました。topで見ている限り、メモリ使用量(RES)も1MB以下です。Tornado+psycoは非常に優秀ですね。

次に、Goでマルチスレッド化を試してみます。goroutineは複数のネイティブスレッドを利用して並列に動作することができます。しかし、現時点ではまだスケジューラがよくないらしく、デフォルトではマルチスレッドを使わないようになっています。環境変数 GOMAXPROCS に並列数を指定して実験してみました。

GOMAXPROCS=6 の場合:
Requests per second:    5760.33 [#/sec] (mean)
Time per request:       17.360 [ms] (mean)
Time per request:       0.174 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     16
  66%     17
  75%     17
  80%     18
  90%     28
  95%     33
  98%     37
  99%     40
 100%     53 (longest request)

実験マシンはCore2 Duoで2コアしか載っていない(Hyper Threadingもなし)なのですが、2を指定してもあまり性能が上がらず、そこから1つずつ数字を上げていったところ6までは少しずつ性能が向上していきました。しかも、topを見ている限りCPU使用率は最大の200%には全く届かず120%以下しか利用できていません。 CPUを原因は判りませんが、今回のようなイベントループをヘビーに使う用途にはスケジューラが最適化されておらず、もっとサーバー側でCPUを利用する計算を行う処理をしないとマルチコアを使いきれないのかもしれません。

Goは今回のケースではTornadoに負けてしまいましたが、IO多重化をイベントドリブンよりシンプルに記述でき、コンパイル型言語なので複雑な計算をPythonより圧倒的に高速に計算できそうなので、非常に期待しています。


@methane
klab_gijutsu2 at 18:37│Comments(0)TrackBack(0)Python | golang

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント