2013年2月4日月曜日

Bottleの非同期アプリケーションへのプライマー


非同期アプリケーションへのプライマー

非同期デザインパターンは、WSGIの同期性とよく混在させないでください。ほとんどの非同期フレームワークは(tornado, twisted, ...)彼らの非同期機能を公開するために特殊なAPIを実装する理由はここにあります。ボトルはWSGIフレームワークはもともと備えてたWSGIとの同期性及び、素晴らしいgeventプロジェクトのおかげで、Bottleで非同期アプリケーションを作成することも可能です。この資料では、BottleのWSGIと非同期の使用を説明します。

同期WSGIの限界

簡単に言葉で表現すると、WSGI仕様(PEP 3333)は、要求/応答のサイクルを処理します。呼び出し可能なアプリケーションは各要求に対して1回ずつ呼び出され、ボディの反復子を返す必要があります。次に、サーバーは、ボディを反復処理し、ソケットに各チャンクを書き込みます。ボディの反復子が使い果たされるとすぐに、クライアント接続が閉じられます。
十分に単純ですが、思わぬ障害があります:これはすべて同期として発生します。アプリケーションがデータ(IO、ソケット、データベース、...)を待つ必要がある場合、それは空の文字列(ビジーウェイト)を得たり、現在のスレッドをブロックする必要があります。両方のソリューションは、処理スレッドを占有し、新しい要求に答えてからそれを防ぐことができます。その結果スレッドごとに1つだけ継続的な要求となります。
ほとんどのサーバーは、その比較的高いオーバーヘッドを回避するために、スレッドの数を制限します。 20以下のスレッドプールが共通しています。できるだけ早くすべてのスレッドが占有された場合は、任意の新しい接続が停止されています。サーバは他の皆のために効果的に終了させます。あなたはリアルタイムでアップデートを取得するためにロングポーリングAjaxリクエストを使用してチャットを実装する場合は、20の同時接続に制限された状態に到達すると思います。それはかなり小さいチャットです。

救助にGreenlets

ほとんどのサーバは切り替え処理において、新しいスレッドの作成にかかわる高いオーバーヘッドのために、同時実行スレッドの比較的低い数字にOSのワーキングプールのサイズを制限します。スレッドがプロセスをspawn(fork)に比べて安いですが、彼らはまだ新しい接続ごとに作成することが高価である。
geventモジュールを加え、greenletsを混在させます。 Greenletsは、従来のスレッドに似て動作しますが、作成することが非常にローコストです。 geventベースのサーバーは、ほとんどオーバーヘッドなしでgreenlets何千もの(接続ごとに1つ)を生成することができます。個々のgreenletsをブロックすると、新しい要求を受け入れるようにサーバの能力に影響を与えません。同時接続数は事実上無制限です。
彼らが見て、同期アプリケーションのように感じるので、これは驚くほど簡単に非同期アプリケーションを作成することができます。 geventベースのサーバーは、実際に非同期ですが、大規模なマルチスレッドではありません。次に例を示します。
from gevent import monkey; monkey.patch_all()

from time import sleep
from bottle import route, run

@route('/stream')
def stream():
    yield 'START'
    sleep(3)
    yield 'MIDDLE'
    sleep(5)
    yield 'END'

run(host='0.0.0.0', port=8080, server='gevent')
最初の行は重要です。それは、現在のスレッドをブロックしないように、PythonのブロッキングAPIのほとんどのmonkey-patchがgevent発生し、次のgreenletにCPUを渡す。それは実際にgeventベースの疑似スレッドでPythonのスレッドを置き換えます。あなたはまだ正常に全体のスレッドをブロックするtime.sleep()を使用することができます。あなたはmonkey-patchをpythonの組込みに快適に感じていない場合は、(この例ではgevent.sleep())に対応するgevent機能を使用することができます。
このスクリプトを実行してhttp://localhost:8080/streamをブラウザに表示した場合は、start、middle、およびEND1(むしろ一度にすべてを見るために8秒を待っているより)ずつを表示を表示する必要があります。それは通常のスレッドと全く同じに動作しますが、現在、サーバーは問題なく同時要求の数千を処理することができます。
注意

彼らはページのレンダリングを開始する前に、一部のブラウザでは、一定量のデータをバッファリングする。これらのブラウザで効果を確認するためにいくつかのバイト以上を得るために必要があるかもしれません。さらに、多くのブラウザはURLごとに同時接続の制限があります。このような場合は、2番目のブラウザやパフォーマンスを測定するベンチマーク·ツール(例えば、ABまたはhttperfの)を使用することができます。

イベントコールバック

非同期フレームワーク(including tornado, twisted, node.js and friends)で良く似た,共通のデザインパターンは非ブロッキングAPIと非同期イベントへのバインドのコールバックを使用することです。それはコールバックは後の時点で、ソケットへの書込みを許可する明示的にクローズされるまでのソケットオブジェクトが開いたままにしています。ここでtornadoライブラリに基づいて記述する例は、次のとおりです。
class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        worker = SomeAsyncWorker()
        worker.on_data(lambda chunk: self.write(chunk))
        worker.on_finish(lambda: self.finish())
主な利点は、要求ハンドラが早期に終了することです。コー​​ルバックは以前の要求のソケットへの書き込みを続行している間に処理スレッドが上に移動し、新しい要求を受け付けることができます。これは、これらのフレームワークはOSのスレッド数が少なく同時要求の多くを処理する管理方法です。
Gevent + WSGIと、処理は異なっています:まず、我々は新しい接続を受け入れるために(疑似)スレッドの無限のプールを持っているので、利点はありません早期終了します。それはソケットを(WSGIで必要)を閉じてしまうため第二に、我々は早期に終了することはできません。第三に、我々はWSGIに準拠するiterableを返さなければなりません。
WSGI標準に準拠するために、我々は非同期に書き込むことができるボディのiterableを返すことをしなければならない。 gevent.queueの助けを借りて、我々は切り離されたソケットをシミュレートし、次のように前の例を書き換えることができます。
@route('/fetch')
def fetch():
    body = gevent.queue.Queue()
    worker = SomeAsyncWorker()
    worker.on_data(lambda chunk: body.put(chunk))
    worker.on_finish(lambda: body.put(StopIteration))
    return body
サーバの観点から、キューオブジェクトは順次処理が可能です。それはブロックであれば、空と同時にそれがStopIterationに達すると停止します。これはWSGIに準拠しています。アプリケーション側では、キューオブジェクトが非ブロッキングソケットと同じように動作します。あなたは、いつでもそれに書き込み、それを周りに通過しさらにそれを非同期的に書き込みを行う新たな(疑似)スレッドを起動することができます。これは、ロングポーリングは時間のほとんどを実装する方法です。

最後に:WebSockets

しばらくの間、低レベルの詳細を忘れてWebSocketsについて話すことができます。ブラウザ(クライアント)とWebアプリケーション(サーバ)間の双方向通信チャネル:あなたがこの記事を読んでいるので、おそらくWebSocketsを理解されるでしょう。
ありがたいことに gevent-websocket packageは私たちの代わりに、すべてのハードワークを行います。ここでメッセージを受信し、ちょうどそれをクライアントに送り返す単純なはWebSocketエンドポイントは、次のとおりです。
from bottle import request, Bottle, abort
app = Bottle()

@app.route('/websocket')
def handle_websocket():
    wsock = request.environ.get('wsgi.websocket')
    if not wsock:
        abort(400, 'Expected WebSocket request.')

    while True:
        try:
            message = wsock.receive()
            wsock.send("Your message was: %r" % message)
        except WebSocketError:
            break

from gevent.pywsgi import WSGIServer
from geventwebsocket import WebSocketHandler, WebSocketError
server = WSGIServer(("0.0.0.0", 8080), app,
                    handler_class=WebSocketHandler)
server.serve_forever()
クライアントが接続を閉じるまで、whileループが実行されます。あなたのアイデア:)を得る
クライアントサイトのJavaScript APIも、まっすぐ実際に次のとおりです。
<!DOCTYPE html>
<html>
<head>
  <script type="text/javascript">
    var ws = new WebSocket("ws://example.com:8080/websocket");
    ws.onopen = function() {
        ws.send("Hello, world");
    };
    ws.onmessage = function (evt) {
        alert(evt.data);
    };
  </script>
</head>
</html>

0 件のコメント:

コメントを投稿