Tornadoの高性能サーバ開発の一般的な方法


最近ずっとAI人の顔の識別の関連するプロジェクトを開発して、取引先にいくつかサービスを提供する必要があって、だから私はいくつかサービスの端のプログラムを開発する必要があります.AIアルゴリズムはすべてpython 3で書かれているので、私はいっそpython開発サービス端を使って、結局速度も速くて、以前Flask、Djangoを使ったことがあって、今回Tornadoがすることを決定して、このフレームワークに対して一連の呼び出しをして、彼の非同期非ブロックの機能に夢中になって、項目の開発が終わった後にいくつかの経験があって、特に以前の資料の照会に対して1つの総括をして、後で多重化することができるようにします.
高いエネルギー源はTornadoがEpoll(unixはkqueue)に基づく非同期ネットワークIOである.tornadoの単一スレッドメカニズムのため,うっかりするとブロックサービス[block]のコードが書きやすくなる.性能が向上するどころか、かえって性能が急激に低下する.従ってtornadoの非同期使用方式を探索する必要がある.
簡単に言えば、Tornadoの非同期は、非同期サービス側と非同期クライアントの2つの側面を含む.サービス側およびクライアントにかかわらず、特定の非同期モデルは、コールバック[callback]およびコモンシップ[coroutine]に分けることができる.シーンを具体的に適用しても、明確な境界はありません.1つのリクエストサービスには、他のサービスに対するクライアント非同期リクエストも含まれることが多い.
サービス側の非同期方式
サービス側は非同期であり、tornadoリクエスト内で時間のかかるタスクを行う必要があると理解できます.ビジネスロジックに直接書くと、サービス全体がブロックされる可能性があります.したがって,このタスクを非同期処理に置くことができ,非同期を実現する方法は2つあり,1つはyieldサスペンション関数であり,もう1つはクラススレッドプールを使用する方法である.
同期例(借用):
class SyncHandler(tornado.web.RequestHandler):
    def get(self, *args, **kwargs):
        #      
        os.system("ping -c 2 www.google.com")
        self.finish('It works')

このとき、時間のかかる動作は、システムのパフォーマンスを深刻にブロックし、要求を処理する時間が数秒であるため、コンカレント量が小さくなります.
一、上記のコードを非同期に変更し、コールバック関数を使用する
from tornado.ioloop import IOLoop
class AsyncHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        IOLoop.instance().add_timeout(1, callback=functools.partial(self.ping, 'www.google.com'))
        # do something others
        self.finish('It works')
    @tornado.gen.coroutine
    def ping(self, url):
        os.system("ping -c 2 www.google.com")
        return 'after'

 
この書き方では、バックグラウンドで時間のかかるタスクが実行され、同時性が大幅に向上しますが、この場合、2つの知識点が必要です.
1、装飾器
@tornado.web.asynchronous
@tornado.gen.coroutine
2つの質問:
なぜこの2つの装飾器を使うのですか?
なぜasynchronousでcoroutineを先に使うのですか?あるいはなぜこの呼び出し順序を使うのですか?
この2つの装飾器の役割:
1.1、@tornado.web.asynchronous
まず同期と非同期の役割を理解する必要があります
同期の場合、Webリクエストが到来した後、処理が完了した後に返さなければならない.これはブロックされたプロセスである.すなわち、1つのリクエストが処理されると、サーバプロセスはリクエストが完了するまで保留されます.これは、サーバの同時実行能力に影響します.
非同期の場合、webサーバプロセスはリクエスト処理を待つ間、IOループを開き、リクエストを受け入れ続けます.処理結果を取得するとコールバック関数が呼び出され、結果が返されます.これは,処理要求にも影響を及ぼさず,受信要求にも影響を及ぼさず,同時能力を著しく向上させることができる.
同期の場合、Webサービスプロセスは、リクエストを受け入れ、リクエストを処理し、結果を返し、最後に自分で接続を閉じることを理解する必要があります.この閉じる動作は自動的です.
非同期の場合、1つのリクエストを処理する際にまだ結果が得られていないため、接続のオープンを維持し、最後に結果を返した後、接続を閉じる必要があります.このクローズ動作は手動で閉じる必要があります.すなわちselfを手動で呼び出す必要がある.finish.
tornadoで@tornadoを使用します.web.asynchronousアクセサリーの役割は、接続を常に開くことです.
上記の例で用いたコールバック関数の欠点は、コールバックの深淵を引き起こす可能性があり、コールバックでコールバックを呼び出すなど、システムのメンテナンスが困難になることである.
 
非同期化を実現するには、handlerの実行が完了したときにオフにすることはできません.
だから総じて言えばweb.asynchronousの役割は、selfを呼び出すまでhttp接続を成長させることです.finish、接続はすべて待機状態です.
1.2、@tornado.gen.coroutine
この関数の役割は,非同期プログラミングを簡略化し,コードの記述を同期コードのようにし,同時に確実に非同期を実現することである.これにより、コールバック関数の書き込みが回避されます.また,非同期プログラミングを実現するために,コヒーレント方式を用いた.最新版のtornadoは、必ずしも@tornadoと書く必要はありません.web.asynchronous.
 
1.3、順序
@asynchronousは@gen.coroutineの戻り結果(Future)をリスニングし、@gen.coroutineで装飾されたコードセグメントの実行が完了するとfinishを自動的に呼び出します.Tornado 3.1バージョンから、@gen.coroutineのみ使用可能です.
 
2、関数
IOLoop.instance().add_timeout()
functools.partial()
 
2.1、IOLoop.instance().add_timeout()
まず、IOLoopとIOLoopを理解する必要があります.Instance()は、インスタンス化動作です.
IOLoopはepollに基づいて実現された下位ネットワークI/Oのコアスケジューリングモジュールであり、socket関連の接続、応答、非同期読み書きなどのネットワークイベントを処理するために使用される.各Tornadoプロセスは、グローバル一意のIOLoopインスタンスを初期化し、IOLoopで静的メソッドinstance()でカプセル化し、IOLoopインスタンスを取得して直接このメソッドを呼び出すことができます.
Tornadoサーバの起動時にリスニングsocketが作成する、socketのfile descriptorをIOLoopインスタンスに登録し、IOLoopはsocketに対するIOLoopを追加する.READイベントはコールバック処理関数を傍受して入力する.あるsocketがacceptを介して接続要求を受信すると、登録されたコールバック関数が読み書きされる.次に,主にIOLoopによるepollのパッケージングとI/Oスケジューリングの具体的な実装を解析する.
epollはLinuxカーネルで実装される拡張可能なI/Oイベント通知メカニズムであり、POISXシステムにおけるselectとpollの代替であり、より高い性能と拡張性を有し、FreeBSDにおける類似の実装はkqueueである.TornadoでPython C拡張に基づいて実装されたepollモジュール(またはkqueue)は、IOLoopオブジェクトが対応するイベント処理メカニズムによってI/Oをスケジューリングできるように、epoll(kqueue)の使用をカプセル化している.
IOLoopモジュールのネットワークイベントタイプのパッケージはepollと一致し,READ/WRITE/ERRORの3種類に分類される.
functoolsモジュールは、高次関数:他の関数に作用する関数または戻る関数に使用されます.一般に、任意の呼び出し可能なオブジェクトは、本モジュールの用途の関数として処理することができる.
functools.partialは呼び出し可能なpartialオブジェクトを返し、使用方法はpartial(func,*args,**kw)であり、funcは必ず入力され、少なくとも1つのargsまたはkwパラメータが必要である.
ここではコールバック関数を追加するpartialオブジェクトです.
 
上記のような書き方では戻り値を取得できません.戻り値を取得するにはyieldを使用して関数を保留し、関数のreturnに基づいて戻り値を取得する必要があります.
二、戻り値を持つ場合、同時に協程を使用して実現する
class AsyncTaskHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        # yield   
        response = yield tornado.gen.Task(self.ping, 'www.google.com')
        print 'response', response
        self.finish('hello')
    @tornado.gen.coroutine
    def ping(self, url):
        os.system("ping -c 2 {}".format(url))
        return 'after'

結果値も返されていることがわかります.このようなコラボレーション処理は、必ずしも同期よりも速いとは限らない.併発量が小さい場合,IO自体の開きは大きくない.コラボレーションと同期のパフォーマンスさえ悪くありません.しかし、大きなコンカレント量の場合は異なります.コンカレント要求が多いため、時間のかかる処理でブロックされると、結果が得られない要求が増えています.
yieldは、ブロックプライマリスレッドがないにもかかわらず、戻り値を処理する必要があるため、応答実行まで保留する時間があり、単一のリクエストに対して待機します.もう1つの非同期およびコヒーレンスを使用する方法は、メインスレッド以外のスレッドプールを使用し、スレッドプールはfuturesに依存する.Python 2は追加のインストールが必要です.
この使い方は比較的よく使われる使い方だと思います.
 
三、スレッドプールの使用方法を非同期処理に変更する
from concurrent.futures import ThreadPoolExecutor
class FutureHandler(tornado.web.RequestHandler):
    executor = ThreadPoolExecutor(10)
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        url = 'www.google.com'
        tornado.ioloop.IOLoop.instance().add_callback(functools.partial(self.ping, url))
        self.finish('It works')
    @tornado.concurrent.run_on_executor
    def ping(self, url):
        os.system("ping -c 2 {}".format(url))

値を返すのも簡単です.使用方法インタフェースをもう一度切り替えます.tornadoのgenモジュールの下のwith_を使用timeout機能(tornado>3.2のバージョンでなければなりません).
 
次のようになります.
class Executor(ThreadPoolExecutor):
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not getattr(cls, '_instance', None):
            cls._instance = ThreadPoolExecutor(max_workers=10)
        return cls._instance
class FutureResponseHandler(tornado.web.RequestHandler):
    executor = Executor()
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def get(self, *args, **kwargs):
        future = Executor().submit(self.ping, 'www.google.com')
        response = yield tornado.gen.with_timeout(datetime.timedelta(10), future,quiet_exceptions=tornado.gen.TimeoutError)
        if response:
            print 'response', response.result()
    @tornado.concurrent.run_on_executor
    def ping(self, url):
        os.system("ping -c 1 {}".format(url))
        return 'after'

具体的にどのような方法を使用するか、より多くの依存業務は、値を返す必要がない場合、callbackを処理する必要があります.コールバックが多すぎるとエラーになりやすいです.もちろん、多くのコールバックがネストされる必要がある場合は、まずビジネスまたは製品ロジックを最適化する必要があります.yieldの方式はとても優雅で、書き方は非同期論理で同期して書くことができて、すぐに少し速くなりましたが、一定の性能を失うことができます.