2020年1月15日水曜日

PythonのSanicフレームワークは一秒間に20万件の処理が可能でしょうか?

PythonのSanicは一秒間に何件処理が可能?
ISHOCON2を意地でPython(Sanic)で20万点だした。
http://www.denzow.me/entry/2018/08/29/001136
と言うブログを拝見したのですが、これは、一秒間に20万件処理出来たと言う事でしょうか?

ちなみに、PythonのJaprontoフレームワークは一秒間にDATA百万件の処理が可能らしいですが、まだバージョンが低すぎて、未完成で完成度が低いです。個人で開発するのは厳しすぎるらしいです。

関連情報:

pythonでサクッとwebサービス作るならFlaskよりsanicがいいよって話

シェアしました。


イントロ

           ▄▄▄▄▄
        ▀▀▀██████▄▄▄       _______________
      ▄▄▄▄▄  █████████▄  /                 \
     ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |
   ▀▀█████▄▄ ▀██████▄██ | _________________/
   ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/
        ▀▀▀▄  ▀▀███ ▀       ▄▄
     ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
   ██▀▄▄▄██▀▄███▀ ▀▀████      ▄██
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███     ▌▄▄▀
▌    ▐▀████▐███▒▒▒▒▒▐██▌
▀▄▄▄▄▀   ▀▀████▒▒▒▒▄██▀
          ▀▀█████████▀
        ▄▄██▀██████▀█
      ▄██▀     ▀▀▀  █
     ▄█             ▐▌
 ▄▄▄▄█▌              ▀█▄▄▄▄▀▀▄
▌     ▐                ▀▀▄▄▄▀
 ▀▀▄▄▀
そもそもPythonでwebアプリをつくろうという選択肢が辛いですが、それでもPythonでやりたいんだ!という人向けに書きます。
あとsanicの日本語記事が少ないのでTOしていきたい。

sanicってなに

image.png
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by this article.
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
意訳をすると
  1. Flaskライクだよ!
  2. uvloop使って高速で動くwebサーバーだよ!
  3. Python3.5+だよ!
ということです。(多分)

Flaskじゃいけないの?

Flaskでもuvloopを使って高速化することはできます。
が、別途ライブラリ入れたりしなくて面倒ではあります。
(そもそもFlask自体多機能化はせずにシンプルに実装して必要に応じでライブラリを入れるという設計理念があったはず)
依存関係やらなにやら色々めんどうなことはできるだけ関わりたくないので標準ではいっているsanicを使えば非同期で高速な処理を行えつつ、Flaskライクで学習コストが低いのが魅力です。
性能がどれくらいすごいのかというのはすでに検証している方がいらっしゃるので画像を引用させていただきます。
image.png
細かい数値はリンク先を参照してください。
並列処理の高負荷だとsanicじゃなくてuvloopすげーとなりますが、結局はサクッと作ってさっとデプロイするとなるとSanicの方が良いのかなと思います。
メリットばっかり言ってもフェアじゃないのでデメリットとして以下の点があります(Flaskと比べて)
  1. 実績が少ない
  2. 日本語ドキュメントが無い
  3. 結局Flaskに戻る
1は歴史的に浅いので仕方がないの一言に終わります。
2に関しては公式ドキュメントを読めや!英語読めや!でも良いんですが、初学者が触るには日本語ドキュメントがあったほうが入り口が広がって良いことですし、母国語が日本語なので日本語で読んだほうが翻訳リソースを割かなくて嬉しい。
3に関してはコミュニティがFlaskに比べて小さいのであれもやろう、これもやろうとなったときに詰むのでなんだかんだでFlaskに戻ってしまうということがあります。webをPythonでやろうと思ってるpythonianなら自前で実装してしまうことが多いでしょうが、やはりそこがボトルネックになるのは辛いものがあります
誤解を生まないようにここで述べておきますが、Githubのstar数はFlaskの方が圧倒的に上ですが、(僕が勝手に思っているだけですが)アクティブなコミュニティ活性度の指標となるOpenIssueは俄然sanicの方が多いです。

Hello World

簡単なHello WorldはQiitaの記事を貼って終わりにしたかったんですが、一個もないので書きます。

install

pyenvで環境を離しておいた方がいいですよ。
そのあたりはこの記事の本質ではないので貼って終わりにします。
注意点は最初に書きましたがPythonのバージョンは3.5+にしてください。
準備ができたらsanicのインストールです
pip install sanic
以上です。

表示させてみる

READMEのまんまをコピペします。名前はなんでも良いんですが、ここではmain.pyとでもしておきましょう
main.py
from sanic import Sanic
from sanic.response import json

app = Sanic()

@app.route('/')
async def test(request):
    return json({'hello': 'world'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
できたら早速動かしてみましょう
python main.py
スクリーンショット 2018-08-07 15.03.52.png
Ctr+cでサーバーが止まります
以上です。

各パーツの説明

Hello Worldだけだと寂しいので各パーツの説明をしていきます。
かといって全部説明するのは難しいので適時該当箇所の公式ドキュメントのURLを貼っておくので読んでください。

import

main.py
from sanic import Sanic
from sanic.response import json
Sanicをimportしています。
import fromがわからない人むけに説明すると、pythonでライブラリを使うときは import ライブラリ名となります。
じゃぁfromって何だよ。となるので from ライブラリ名 import 関数名 と返します。
上記の例だと from sanic import Sanic だとsanicライブラリのSanicを読み込んでいる。(それはそう)
二段目はsanicの中にあるresponseクラスを呼び出して、その中のjsonという関数を呼び出している。
具体的にいうとGithubでいうところの↓を呼び出している。
https://github.com/channelcat/sanic/blob/master/sanic/response.py
あれ?ここの挙動おかしいぞ?となったらソースコードを眺めてみよう。呼び出されている関数がどういう挙動をしているか理解できればその疑問も解決するはず。

インスタンス

main.py
app = Sanic()
インスタンスってなに?と言われると時間が無限にたりないので各自ググってください。
雑でもいいなら"importしたSanicをappに入れている"という理解で3割ぐらい合ってます

デコレーター

main.py
@app.route('/')
なかなか見慣れない@ですが、これらはデコレーターというものです。
デコレーターに関しては記事を貼って終わりにします。
Pythonのデコレータについて - Qiita
Pythonのデコレータを理解するための12Step - Qiita

Routing

雑で良いなら、括弧で囲まれた場所がRoutingを指しているというところだけ理解できればなんとかなります。
例えばさっきのmain.pyを以下のように書き換えてみます。
main.py
from sanic import Sanic
from sanic.response import json

app = Sanic()

@app.route('/')
async def test(request):
    return json({'hello': 'world'})

@app.route('/api/')
async def apiindex(request):
    return json({'API': 'Index'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
先程のように python main.py を行い http://localhost:8000 にアクセスしてみましょう。
先ほどと同じく {"hello":"world"} が出たと思いますが、 次は http://localhost:8000/api にアクセスしてみましょう
スクリーンショット 2018-08-07 15.34.51.png
@app.route('/api/')の真下に書かれたjson({'API': 'Index'})が返ってきてきます。
このように@app.route()の括弧の部分をURIとして指定してあげればそれに応じたリクエスが返ってきます。

動的なRouting

数ページの静的なページだったらこれで問題ないのですが、動的なRoutingの必要が出たときにその分の@app.route()を作るのはしんどいので変数に入れてあげましょう。
main.py
from sanic import Sanic
from sanic.response import json
from sanic.response import text

app = Sanic()

@app.route('/')
async def test(request):
    return json({'hello': 'world'})

@app.route('/api/')
async def apiindex(request):
    return json({'API': 'Index'})

@app.route('/api/<path>')
async def path_handler(request, path):
    return text('path = ' + path)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
しれっとfrom sanic.response import textが加わってますが後ほど説明します
大きな追加点は
main.py
@app.route('/api/<path>')
async def path_handler(request, path):
    return text('path = ' + path)
で、見たとおりなんですが('')内において、<>でくくる事によってその囲った部分が変数となります。
スクリーンショット 2018-08-07 15.51.23.png

POSTや型

型っぽいのを宣言できたり、POSTを受け取るときもこの部分を編集します。
integer
@app.route('/number/<integer_arg:int>')
async def integer_handler(request, integer_arg):
    return text('Integer - {}'.format(integer_arg))
Post
@app.post('/post')
async def post_handler(request):
    return text('POST request - {}'.format(request.json))

static file

これだけあれば大抵のことができそうですが、少し問題点が。
現状、URLと返すリソースが1対1となっています。
webサイトを作ろうとした時CSSやjsを書くと思うのですが、このままだとcssやjsまで1回ずつ書かないといけなくなってしまいます。
そこでstaticなファイルはフォルダでまとめて置いておく機能があるので利用しましょう。
以下のようなフォルダ構成を想定します
.
├── index.html
├── main.py
└── static
    ├── css
    └── js
        └── main.js
雑にapp.static('/static', './static')を貼るだけで返したhtmlに
<script type="text/javascript" src="/static/js/main.js"></script>
と記述すると読み込んでくれます。

WebSocket

力が尽きたのでパスします

Response

ようやくサーバーに問い合わせて返ってくる内容について触れます。
富士山で言うなら五合目あたりでしょうか。
main.pyのこの部分
main.py
return json({'hello': 'world'})
sonicでは以下の8つのfile responseをサポートしています
公式ドキュメントには
from sanic import response


@app.route('/text')
def handle_request(request):
    return response.text('Hello world!')
とありますが、個人的には
from sanic.response import text


@app.route('/text')
def handle_request(request):
    return text('Hello world!')
の方がキレイかなと思います。(やっている動作自体に変わりはないです)
jsonに関してjson文字列をresponse.json()に入れるとダブルクォーテーションの前にバックスラッシュが入るバグっぽいのが手元で起こって泣く泣くtextで返したりしています。

Deploy

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
はい、ここです。
if __name__ == '__main__':ってなんですか
要はこのmain.pyが実行されたときにしか呼ばれませんよ〜ということです。
app.run()
こいつがメインなんですが、オプションは以下のとおりです
オプションデフォルト値説明
host"127.0.0.1"サーバー名
port8000ポート番号
debugFalseデバッグ出力の可否
sslNoneSSL対応させるか
sockNonewebsocketの受け入れ
workers1動かすワーカー数
loopNoneasyncioでループ処理させるか
protocolHttpProtocolプロトコル

サンプル集とか

公式のGithubにいっぱいサンプルがあるのでそれっぽいやつを眺めてみると良いのではないでしょうか

まとめ

Flaskを触ったことある人ならお気づきですが大体Flaskと同じです。
個人的には初心者はある程度ナウい機能がデフォで入ってるsanicを使って限界を感じたらFlaskに行けばいいじゃないんですかね。
あと好きなsonicは、東京エンカウントで悠木碧さんが好きなゲームを聞かれて答えるモノマネをした杉田智和の「そ゛に゛っ゛く゛ー゛」です。
次回はsanicのテンプレート(Jinja2)で動的ページ作ったり、sanic × vue.jsでイケイケ爆速SPA作ったりする記事を書ければなと思います。
最後にREADMEから注意すべき部分を引用して終わります

TODO

http2

Limitations

No wheels for uvloop and httptools on Windows :(

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます


0 コメント:

コメントを投稿