主にプログラミングに関して。Python, .NET Framework(C#), JavaScript, その他いくらか。
記事にあるサンプルやコードは要検証。使用に際しては責任を負いかねます

Python: Tornadoのノンブロッキングという特徴を手っ取り早く試す

                
 PythonでのWebアプリ作りはGoogleAppEngineから入った。GoogleAppEngineで使うフレームワークが最初はwebappでそれがwebapp2になったが、それが一般的なフレームワークではないので、PythonのWebフレームワークで他のサーバでも使えて開発が続いているのを選ばねばとなった。それでwebapp系に割と似た書き方をするし面白い特徴を持っているTornadoを選んだ。
 Tornadoはノンブロッキングなサーバというのをウリとしている。ノンブロッキングという特徴を利用すれば、時間のかかるIO処理があったらそのデータ処理開始はIOが終わるまで後回しにして、次のリクエストの処理を先にやってしまえる。このあたりを試していなかったので今回試してみる。ちなみにノンブロッキングではなくPython標準のWSGIでも使える。

 リクエスト処理中に起こった時間のかかるIO処理は後回しにして、次のリクエストの処理を始める。これを検証する。
 時間のかかるIO処理として、MongoDBから10MBのデータを取ってくる処理を行うことにする。そのためにMongoDBを立ち上げて、サイズが10MBほどの適当なドキュメントを一件作成しておく。PyMongoを使うとおおよそ下記のように。
v = "a" * 10 ** 7
db.tekitou.insert({"x":v})


 以降のPythonスクリプトではPyMongoではなくTornado用に書かれたMotorを使う。

 上記のドキュメントの取得を、Tornadoの任意のリクエストハンドラの処理にはさむ。
class AsyncHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
data = yield db.tekitou.find_one()
self.write(data["x"])


 軽い処理ですんなり終えられそうな処理をするリクエストハンドラも用意しておく。
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")


 二つのリクエストハンドラを書いた。これらをパスへ割り当てる。今回は軽い処理しかないリクエストハンドラをルート"/"、MongoDBから10MB取ってくる処理を持つリクエストハンドラをパス"/b"へ割り当てておく。
 もしパス"/b"へのリクエストが先にあり、次にパス"/"へのリクエストがあったのに、パス"/"にリクエストしたクライアントが先にレスポンスを得られれば順序が変わっており、ノンブロッキングな特徴が確認できたことになる。

 Pythonのmultiprocessingモジュールを使って、用意したサーバに計四つのプロセスでリクエストを送り、リクエスト順に対するレスポンス順の変化があるかを調べる。リクエストに使ったスクリプトを下につけておく。まずパス"/b"へのリクエストを先に二件発生させ、次にパス"/"へのリクエストを二件発生させている。リクエストごとに固有の整数値を与えておいて、それを判別のためにリクエストクエリに入れておく。
from urllib import request
from multiprocessing import Process
import datetime
import time


url1 = "http://localhost:8888/"
url2 = "http://localhost:8888/b"


def req(url, request_id):
time.sleep(0.01 * request_id)
t = datetime.datetime.now()
print(str(request_id) + ":" + str(t))
f = request.urlopen(url + "?num=" + str(request_id))
print(str(request_id))


if __name__ == '__main__':
p1 = Process(target=req, args=(url2, 1))
p1.start()

p2 = Process(target=req, args=(url2, 2))
p2.start()

p3 = Process(target=req, args=(url1, 3))
p3.start()

p4 = Process(target=req, args=(url1, 4))
p4.start()


 サーバ側は最終的に下記。どのプロセスからのリクエストかをGETのクエリから拾っている。
import tornado.ioloop
import tornado.web
import motor


client = motor.MotorClient()
db = client.tornado_test


class MainHandler(tornado.web.RequestHandler):
def get(self):
print(self.get_argument("num"))
self.write("Hello, world")


class AsyncHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
print(self.get_argument("num"))
data = yield db.tekitou.find_one()
self.write(data["x"])


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


if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()


 スクリプトの用意ができたので走らせる。MongoDBを立ち上げた状態でTornadoサーバを立ち上げ、リクエストスクリプトを実行する。コマンドプロンプトをキャプチャしたものを下に。上がサーバを走らせているプロンプト、下がリクエストを書いたスクリプトを走らせたプロンプトである。
1503291752257.jpg
 サーバを走らせている上側を見るとリクエストIDの1から順番にリクエストを受け取っているのがわかる。次に下を見るとリクエストも確かにその順番で発生していると思われる。しかしレスポンスの順番がかわっており、3、4、1、2の順になっている。
 3と4は重いIOの発生しないリクエストハンドラへのリクエストであった。3と4のリクエストが1と2のリクエストのあとに発生しているのに、レスポンスを受け取ったのは3と4のほうが早い。これはMongoDBへの10MBのIO処理を待っている間に重いIO処理を含まない3と4への処理を先に済ませたからだと考えられる。つまりTornadoのサーバであるノンブロッキングという特徴が見られたということだろう。

 数件のリクエストに対するレスポンスの順番変更があるかを探ることで、Tornadoサーバのノンブロッキングという特徴を試してみた。確かに重いIO処理を行っていると考えられる最中にも別のリクエストを処理し、レスポンスの順番が変わっていた。Tornadoの特徴であるノンブロッキングというところが確認できた。
スポンサーサイト



プロフィール

h

Author:h

最新記事
リンク
作ったものなど
月別アーカイブ
カテゴリ
タグリスト

検索フォーム
Amazon