PythonのWebアプリケーションフレームワーク Sanicを試す

PythonのWebアプリケーションフレームワークについて、Flaskからの移行先としてSanicが有力そうなので調べています。

Sanic is a Flask-like Python 3.5+ web server that’s written to go fast.

Sanicは著名なイベントループライブラリであるuvloopを利用しており、Node.jsのように非同期I/Oによる高効率なHTTPリクエスト処理が可能です。また、SanicはFlaskとよく似たシンタックスを提供しているため、他のフレームワークよりも移行コストを抑えることができそうです。

環境

サーバ環境

  • Ubuntu 16.04 (x86_64)
  • CPU 3.30GHz 4core / 8GB RAM
  • Python 3.6.4
  • Sanic 0.7.0 / Flask 0.12

クライアント環境

  • macOS High Sierra 10.13.4 (x86_64)
  • CPU 1.70GHz 4core / 8GB RAM

ネットワーク環境

$ iperf -c 192.168.11.11
------------------------------------------------------------              
Client connecting to 192.168.11.11, TCP port 5001                         
TCP window size:  129 KByte (default)                                     
------------------------------------------------------------              
[  4] local 192.168.11.14 port 51189 connected with 192.168.11.11 port 5001                                                                          
[ ID] Interval       Transfer     Bandwidth                               
[  4]  0.0-10.0 sec   223 MBytes   187 Mbits/sec

注意点としてはSanicは asyncio および async/await 機能を利用しているためPython3.5以降が必要です。Python3移行は2018年現在なら世間的にもけっこう進んでいるとは思いますが、3.5以降となると業務で使っている人はまだ少ないかもしれません。

使い方

公式でFlask-likeと謳っているとおりインタフェースはFlaskとよく似ているので移行作業自体は楽だと思います。

Flaskでhello, world
[python]
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
app = Flask(__name__)

@app.route(‘/’)
def hello():
return ‘hello, world’

if __name__ == “__main__”:
app.run(host=”0.0.0.0″, port=8000)
[/python]
Sanicだと以下のようになります。まさにFlask-likeですね。
[python]
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sanic import Sanic
from sanic.response import text

app = Sanic(__name__)

@app.route(‘/’)
async def hello(request):
return text(‘hello, world’)

if __name__ == “__main__”:
app.run(host=”0.0.0.0″, port=8000)
[/python]
上記SanicのコードではPython3.5以降で正式に利用できるasyncキーワードが現れています。C#やNode.JSなど他言語におけるasync/awaitと同等の機能が言語標準で提供されるようになりました。Sanicを使う場合は、個人的にNode.js開発でよくやってしまうawaitの置き忘れも気にしなくて良いのは助かります。

JSON対応

JSONレスポンスを返すには以下のように書きます。FlaskもSanicどちらも使いやすいと思います。パッケージ構成はSanicの方が整理されてる気はしますがどうでもいいレベル。
[python]
## Flask
from flask import Flask, jsonify

app = Flask(__name__)

@app.route(‘/’)
def hello():
return jsonify({‘hello’:’world’})

## Sanic
from sanic import Sanic
from sanic.response import json

app = Sanic(__name__)

@app.route(‘/’)
async def hello(request):
return json({‘hello’:’world’})
[/python]
ちなみにJSONパーサーは独自実装ではなく外部モジュールが使われています。Flaskを使う場合はsimplejsonをセットで入れておくと良いです。Sanicはujsonが必須となっています。

  • Flask: simplejson or json
  • Sanic: ujson

JSONモジュールのベンチマークに関しては以下のサイトが参考になります。

ujson優勢みたいなのでJSONレスポンスを含めたベンチマークだとFlaskが不利だと思うので、テキスト(“hello,world”)のみを返すシンプルなコードで計測します。ただ、有利不利とか言ってしまうとuvloopに乗っかっているSanicはズルいという話になってしまいますけども。。

ベンチマーク

前述のサンプルコード(‘hello, world’文字列を返却)で組み込みHTTPサーバのベンチマーク。同一LAN内でクライアント/サーバの2台用意して計測、ツールはwrk2を利用しました。

## ネットワーク環境(iperfで計測)
$ iperf -c 192.168.11.11
------------------------------------------------------------              
Client connecting to 192.168.11.11, TCP port 5001                         
TCP window size:  129 KByte (default)                                     
------------------------------------------------------------              
[  4] local 192.168.11.14 port 51189 connected with 192.168.11.11 port 5001                                                                          
[ ID] Interval       Transfer     Bandwidth                               
[  4]  0.0-10.0 sec   223 MBytes   187 Mbits/sec

ネットワーク周りのカーネルパラメータは以下の通りです。TCPのsyn backlogをsomaxconnの値と合わせて増やす程度のチューニングはしてあります。TCP接続を使い回す設定にもしていますが、今回のベンチマークでは意味ないですね。

$ sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 16384
$ sysctl net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_reuse = 1
$ sysctl net.core.somaxconn
net.core.somaxconn = 16384
$ sysctl net.core.netdev_max_backlog
net.core.netdev_max_backlog = 1000

まずは4スレッド同時100コネクション、2000rpsで30秒間負荷を与えました。
[bash]
## Flask ##
$ wrk -t4 -c100 -d30s -R2000 -L –timeout 5 http://192.168.11.11:8000
Running 30s test @ http://192.168.11.11:8000
4 threads and 100 connections
Thread calibration: mean lat.: 205.030ms, rate sampling interval: 1378ms
Thread calibration: mean lat.: 206.542ms, rate sampling interval: 1379ms
Thread calibration: mean lat.: 206.822ms, rate sampling interval: 1378ms
Thread calibration: mean lat.: 206.315ms, rate sampling interval: 1378ms
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.00s 2.66s 10.44s 60.42%
Req/Sec 260.84 30.50 297.00 71.43%
Latency Distribution (HdrHistogram – Recorded Latency)
50.000% 4.64s
75.000% 7.14s
90.000% 8.97s
99.000% 10.31s
99.900% 10.44s
99.990% 10.44s
99.999% 10.44s
100.000% 10.44s

Detailed Percentile spectrum:
Value Percentile TotalCount 1/(1-Percentile)

837.631 0.000000 1 1.00
1605.631 0.100000 2066 1.11
… 省略
#[Mean = 4995.968, StdDeviation = 2660.702]
#[Max = 10436.608, Total count = 20635]
#[Buckets = 27, SubBuckets = 2048]
———————————————————-
39055 requests in 30.00s, 6.18MB read
Requests/sec: 1301.76
Transfer/sec: 211.03KB

## Sanic ##
$ wrk -t4 -c100 -d30s -R2000 -L –timeout 5 http://192.168.11.11:8000
Running 30s test @ http://192.168.11.11:8000
4 threads and 100 connections
Thread calibration: mean lat.: 3.764ms, rate sampling interval: 13ms
Thread calibration: mean lat.: 3.692ms, rate sampling interval: 12ms
Thread calibration: mean lat.: 3.857ms, rate sampling interval: 12ms
Thread calibration: mean lat.: 8.985ms, rate sampling interval: 22ms
Thread Stats Avg Stdev Max +/- Stdev
Latency 61.15ms 146.51ms 668.16ms 88.67%
Req/Sec 516.95 410.74 2.82k 70.37%
Latency Distribution (HdrHistogram – Recorded Latency)
50.000% 4.69ms
75.000% 10.02ms
90.000% 267.26ms
99.000% 617.47ms
99.900% 662.53ms
99.990% 667.65ms
99.999% 668.67ms
100.000% 668.67ms

Detailed Percentile spectrum:
Value Percentile TotalCount 1/(1-Percentile)

0.860 0.000000 1 1.00
2.317 0.100000 3894 1.11
… 省略
#[Mean = 61.153, StdDeviation = 146.513]
#[Max = 668.160, Total count = 38861]
#[Buckets = 27, SubBuckets = 2048]
———————————————————-
58577 requests in 30.00s, 7.32MB read
Requests/sec: 1952.50
Transfer/sec: 249.78KB
[/bash]
レイテンシ周りの計測値を見てみるとわかりますが、これだけでもかなり性能差があることが読み取れますね。低スペックのサーバで動かしているのですがSanicの方はまだまだ余裕がありそうです。次は4スレッド同時200コネクション、4000rpsで30秒間負荷を与えました。
[bash]
## Flask ##
$ wrk -t4 -c200 -d30s -R4000 -L –timeout 5 http://192.168.11.11:8000
Running 30s test @ http://192.168.11.11:8000
4 threads and 200 connections
Thread calibration: mean lat.: 2436.148ms, rate sampling interval: 9306ms
…省略
Thread Stats Avg Stdev Max +/- Stdev
Latency 10.47s 4.29s 22.66s 59.03%
Req/Sec 318.12 38.85 371.00 50.00%
Latency Distribution (HdrHistogram – Recorded Latency)
50.000% 10.11s
75.000% 14.05s
90.000% 16.49s
99.000% 19.27s
99.900% 20.76s
99.990% 22.51s
99.999% 22.68s
100.000% 22.68s

Detailed Percentile spectrum:
Value Percentile TotalCount 1/(1-Percentile)

2514.943 0.000000 1 1.00
5140.479 0.100000 2491 1.11
… 省略
#[Mean = 10466.975, StdDeviation = 4291.485]
#[Max = 22659.072, Total count = 24859]
#[Buckets = 27, SubBuckets = 2048]
———————————————————-
45876 requests in 30.00s, 7.26MB read
Socket errors: connect 0, read 0, write 0, timeout 39
Requests/sec: 1529.00
Transfer/sec: 247.87KB

## Sanic ##
$ wrk -t4 -c200 -d30s -R4000 -L –timeout 5 http://192.168.11.11:8000
Running 30s test @ http://192.168.11.11:8000
4 threads and 200 connections
Thread calibration: mean lat.: 5.816ms, rate sampling interval: 20ms
… 省略
Thread Stats Avg Stdev Max +/- Stdev
Latency 461.22ms 414.34ms 1.28s 43.98%
Req/Sec 0.96k 544.80 2.63k 67.42%
Latency Distribution (HdrHistogram – Recorded Latency)
50.000% 438.78ms
75.000% 821.76ms
90.000% 1.06s
99.000% 1.25s
99.900% 1.27s
99.990% 1.28s
99.999% 1.28s
100.000% 1.28s

Detailed Percentile spectrum:
Value Percentile TotalCount 1/(1-Percentile)

1.301 0.000000 1 1.00
5.403 0.100000 7398 1.11
… 省略
#[Mean = 461.220, StdDeviation = 414.343]
#[Max = 1276.928, Total count = 73973]
#[Buckets = 27, SubBuckets = 2048]
———————————————————-
113684 requests in 30.04s, 14.20MB read
Requests/sec: 3783.92
Transfer/sec: 484.08KB
[/bash]
Flaskの方は限界が見えてますね。タイムアウトも発生しています。ネットワーク周りのカーネルパラメータが未調整だと他のソケット関連エラー等も併せて出てくるはずです。一方でSanicの方は安定した性能です。uvloopすごい。

(横軸の数値が見えてないですが、1/ (1 – Percentile) 刻みの対数表示となっています)

おわりに

Pythonのasyncioに対応したフレームワークはSanic以外にもいくつかありますが、既存のFlask製アプリケーションの移行コストを最小化するために、Flask-likeなSanicを採用するのは悪くない選択だと思います。2,3年前にFlaskの代替として、Falconというフレームワークに移行する作業もやったことがあるのですが、使い方がFlaskと結構違うので移行コストが高かったです。その点、Sanicを使えばその辺りの面倒を省くことができますね。Flaskプラグインの移植性も高く、ベストプラクティスの流用も行いやすいのではないでしょうか。

また、Sanicはasyncioのdrop-in replacement実装としてuvloopを組み込んでおり、uvloop自体はlibuvをベースにしているので実績も十分、性能も保証されています。Python3.5以降が必要なので実業務で採用するのはまだ少しネックになりそうですが、そこは時間が解決してくれる部分でしょう(スタートアップのサービスとかなら勢いで採用しても良いかも?)。他にuvloopをベースにしたフレームワークとしてはjaprontoというのもあります。Flask-likeでなくても良いならこちらも選択肢としてアリですね。

まずはプライベートワークで作っているPython製WebAPIはSanicに移行してみようと思いました。前回紹介した pyvips(libvips) を利用すれば高速な画像処理サーバもPythonで簡単に書けそうです。

参考

あわせて読む:

2 Thoughts

  1. ベンチマークにて、C10K問題に対応しているかどうか明確にして頂けますと、参考になります。
    今の所対応しているPythonのフレームワークは、Cycloneフレームワークのみです。

  2. Sanicと同じく、Pythonのフレームワークに、Japrontoフレームワークがありまして、こちらは、1秒間に100万件処理出来るそうです。Sanicも早いらしいですが、ベンチマーク結果が良くわからないブログ記事が多いようです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です