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
ネットワーク環境
1 2 3 4 5 6 7 8 |
$ 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
1 2 3 4 5 6 7 8 9 10 11 12 |
#!/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) |
Sanicだと以下のようになります。まさにFlask-likeですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#!/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) |
上記SanicのコードではPython3.5以降で正式に利用できるasync
キーワードが現れています。C#やNode.JSなど他言語におけるasync/awaitと同等の機能が言語標準で提供されるようになりました。Sanicを使う場合は、個人的にNode.js開発でよくやってしまうawait
の置き忘れも気にしなくて良いのは助かります。
JSON対応
JSONレスポンスを返すには以下のように書きます。FlaskもSanicどちらも使いやすいと思います。パッケージ構成はSanicの方が整理されてる気はしますがどうでもいいレベル。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
## 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'}) |
ちなみに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を利用しました。
1 2 3 4 5 6 7 8 9 |
## ネットワーク環境(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接続を使い回す設定にもしていますが、今回のベンチマークでは意味ないですね。
1 2 3 4 5 6 7 8 |
$ 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秒間負荷を与えました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
## 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 |
レイテンシ周りの計測値を見てみるとわかりますが、これだけでもかなり性能差があることが読み取れますね。低スペックのサーバで動かしているのですがSanicの方はまだまだ余裕がありそうです。次は4スレッド同時200コネクション、4000rpsで30秒間負荷を与えました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
## 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 |
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で簡単に書けそうです。
参考
- uvloop Documentation
- uvloop: Blazing fast Python networking
- 17 Best Python Web Frameworks to Learn in 2017
ベンチマークにて、C10K問題に対応しているかどうか明確にして頂けますと、参考になります。
今の所対応しているPythonのフレームワークは、Cycloneフレームワークのみです。
Sanicと同じく、Pythonのフレームワークに、Japrontoフレームワークがありまして、こちらは、1秒間に100万件処理出来るそうです。Sanicも早いらしいですが、ベンチマーク結果が良くわからないブログ記事が多いようです。