はじめに
この記事ではPythonのWebフレームワークであるTornadoについて紹介します。
いかんせんDjangoやFlaskより知名度は低いと思いますが(多分)、現在でも積極的にFWの更新されており、ポテンシャルの高いFWだと思っております。
Tornadoとは
PythonでノンブロッキングI/Oを提供可能なWebフレームワークのことです。
ノンブロッキングI/Oといえば代表的なのはnode.jsだと思います。
Curlやmysqlへの接続など外部への通信の際に、帰りを待ってCPUを握り続けるのではなくどんどんと次の処理を非同期に行っていく特性と考えてもらって良いです。
もしわからない方はこのあたりを参考にしてみてください。
つまりnode.js互換のような動きをPythonで実現できるということです!これはすごい!
「ノンブロッキングI/Oで実装したいよぉ...。でもnode.jsよりPythonが好きだよぉ...。」という時にぜひ採用しましょう!
ノンブロッキングI/Oの使い道
では実際どういう面でノンブロッキングI/O利用のメリットがあるのかという話ですが、一例をあげると高スループットのAPIとかがあげられます。
具体的には今流行り(?)のBFFサーバなど。アクセスが多く、自身ががっつりしたロジックを持つというよりかはマイクロサービスの橋渡し的な立ち位置になるAPIとかでその性能をいかんなく無く発揮できるでしょう。
というかノンブロッキングI/Oであることのデメリットってあまり思い浮かばないので、正直どこで採用してもいいと思います。
使い方
ではTornadoで簡単な実装を紹介したいと思います。
今回は以下の環境で実施しています。
python | tornado | OS |
---|---|---|
3.6 | 5.1.1 | Linux |
実装例ではasync/await
を利用しているのでpython3.5以上が条件になると思います。
Tornadoのインストール
pipコマンド一つでOKです。(pipが使える前提です。)
pip install tornado
インストールされたことは以下で確認しましょう。(バージョンは現在の最新ですが今後アップデートされていれば変わります。)
pip freeze | grep tornado
tornado==5.1.1
hello world
Users Guideにも載っているお約束のhello world
を出してみましょう。
(PaaSとかで動くようにポートだけ実装を変えてます。)
import os
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(os.environ.get("PORT", 8080))
tornado.ioloop.IOLoop.current().start()
これを起動してアクセスしてみましょう。
python app.py
とまあこんな感じでアクセスできたと思います。
しかし、これではただサーバを起動させただけで、ノンブロッキング云々は全然伝わらないですね。。。
非同期HTTP通信を行う
では今度は非同期で外部通信を行うクラスを作成してみましょう。
Qiitaに投稿してるので(?)、例ではQiita APIにアクセスします。
適当なディレクトリを切ってそこにHandlerクラス(MVCでいうコントローラ)を作ります。
midir routes
vim routes/access.py
import tornado.web
from tornado.httpclient import AsyncHTTPClient
class AsyncHandler(tornado.web.RequestHandler):
# Getでアクセスした際、Handlerクラスの`get`メソッドが呼び出される。
# Post等も同様。リクエストメソッドを意識せず処理したい場合は`prepare`メソッドを実装するとそこにルーティングされる。
async def get(self):
body = await self.getRate()
self.set_header("Content-type", "application/json")
self.write(body)
async def getRate(self):
http_client = AsyncHTTPClient()
try:
response = await http_client.fetch("https://qiita.com/api/v2/items?page=1")
return response.body
except Exception as e:
print("Error: %s" % e)
外部通信を行う際はTornadoが提供しているAsyncHTTPClient
を使いましょう。
一般的にPythonのリクエストで利用されてるrequests
などでは同期実行になってしまい、Tornadoの特性を活かせません。
そして作成したHandlerクラスをapp.pyに読み込ませます。
import os
import tornado.ioloop
import tornado.web
+from routes.access import AsyncHandler
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
+ (r"/async", AsyncHandler), # タプルの先頭にURL、次にHnadlerクラスを指定して、ルーティングできる。
])
if __name__ == "__main__":
app = make_app()
app.listen(os.environ.get("PORT", 8888))
tornado.ioloop.IOLoop.current().start()
これで完成です。
app.py
を再起動して/async
にアクセスしてみましょう。
これでQiita APIの内容を取得できたかと思います。
このようにTornadoで提供しているライブラリを利用することで外部通信をノンブロッキングで実装することができます。
え?これだけでは結局これが本当に非同期で動いてるかわからない?
あ、はい。。。
非同期通信の動作検証(おまけ)
おまけです。Tornadoの使い方でもなんでもないので興味ない方は読み飛ばしてください。
本来はアプリケーションの性能測定とかを行えばすぐに非同期通信の動きなども確認できるのですが、いかんせん準備とかも少々面倒なので、ここまで実装したついでにほんの少し動きを確認してみたいと思います。
access.py
に比較用の同期通信エンドポイントも作ってみましょう。
import tornado.web
from tornado.httpclient import AsyncHTTPClient
import requests
class AsyncHandler(tornado.web.RequestHandler):
async def get(self):
print("Async Request")
body = await self.getRate()
self.write(body)
async def getRate(self):
http_client = AsyncHTTPClient()
try:
response = await http_client.fetch("http://localhost:8888/")
return response.body
except Exception as e:
print("Error: %s" % e)
class SyncHandler(tornado.web.RequestHandler):
def get(self):
print("Sync Request")
body = self.getRate()
self.write(body)
def getRate(self):
try:
response = requests.get("http://localhost:8888/")
return response.text
except Exception as e:
print("Error: %s" % e)
# 略
- from routes.access import AsyncHandler
+ from routes.access import AsyncHandler, SyncHandler
# 略
(r"/", MainHandler),
(r"/async", AsyncHandler),
+ (r"/sync", SyncHandler),
])
# 略
同期通信用のライブラリをいれたのでインストールします。
(Tornadoが提供する同期通信のHTTPクライアントライブラリもあるのですが、そちらは思ったように動作せず調査中...。)
pip install requests
ここからちょっと面倒ですが、通信用と通信受けるように二つのポートでアプリケーションを起動し、ノンブロッキングI/Oの動作を確認してみましょう。
export PATH=8888
python app.py
# ターミナルの別タブとか開いて
export PATH=8080
python app.py
これでlocalhost:8080
にアクセスすることで8080ポートのアプリケーションが8888ポートのアプリケーションに対して、非同期で通信する場合と同期で通信する場合の2パターンを検証できます。
(通信先をQiita APIからlocalhostに変えたのは、検証で同時アクセスするのでQiitaを攻撃しないようにするためです。)
実際にApache Benchなどで性能測定を行ってみると、ノンブロッキングI/Oの特性を確認できると思います。
ab -n 10 -c 10 localhost:8080/async
# 略
Percentage of the requests served within a certain time (ms)
50% 34
66% 34
75% 34
80% 34
90% 35
95% 35
98% 35
99% 35
100% 35 (longest request)
ab -n 10 -c 10 localhost:8080/sync
Percentage of the requests served within a certain time (ms)
50% 23
66% 27
75% 32
80% 37
90% 41
95% 41
98% 41
99% 41
100% 41 (longest request)
100%終了までのレイテンシをみてわかる通り、非同期処理の方はほぼ同時に、同期処理は徐々にレスポンスを返しているのがわかります。
また、ab実行時に出力されるprint文の出力のされ方も、非同期の方はドバッと。同期の方は徐々に出力されるのが確認できると思います。
まとめ
Pythonで非同期処理が実行できるTornadoをご紹介しました。
Tornado自体は別にそこまで新しいFWというわけではなく、wikiによれば初版は2009年らしいです。
しかし私は最近このFWに出会い、個人的にはかなり気に入ったので今後採用されるケースがどんどん増えれば良いなと思います。
なにぶん日本語での資料はあまりなく、書籍もなんとAmazonで検索したところ英語のオライリー本一冊のみ。。。
まあ、非同期通信するなら最初からnodeで書くわ。って話なのかもしれませんが、Pythonの豊富なライブラリ等を活かせて高スループットにも耐えられそうなTornadoの今後の活躍に期待しております。
また何かTornadoの利用方法などを記事にできればよいなと思います。