非同期ハート

しばらく前に、PythonのWeb開発環境の見慣れた外観を変更するいくつかのイベントが発生しました。FacebookはFriendfeedサービスを取得し、プロジェクトテクノロジーのソースコード(http-serverとTornadoマイクロフレームワーク)をすぐに開きました。 同時に、開発者のFriendfeedは彼のブログにメモを公開しました。彼は、独自の非同期Webサーバーをゼロから開発することにした理由を引用しました。



この記事は、このプロジェクトと競合する(Twisted.web)プロジェクトの中心である、着信データの非同期処理のサイクルへの遠足です。







開発者のメモは、非同期アプリケーションを構築するための人気のあるフレームワークであるTwistedを不安定および不安定であると批判しました。 Twisted.web(httpおよびWeb開発に特化したTwistedのサブセット)とTornadoの単純なアプリケーションのパフォーマンスを比較した結果が示されました。 当然、後者はこれらのテストでより効果的であることが判明しました。



Twistedの重要なプログラマーの1人は、離れることできず 、Friendfeedが車輪を再発明せず、既存のツールを使用しないほうが良い理由を挙げました。 次の投稿で、彼は別の開発指摘しました-Orbited Comet-serverは、安定性と開発の容易さの理由でTwistedに移植されました。



Web開発者の観点から見ると、TornadoとTwisted.webはそれほど大きな違いはありません。リクエストや承認などを処理するための最も基本的なツールのみを提供するマイクロフレームであり、DjangoやPython World、Ruby on Rails。



非同期性





心臓、魂、およびアプリケーションと競合他社の主な違いは、サーバーによる要求の非同期処理です。これにより、多くのプロセスまたはスレッドを生成する同期サーバーに特徴的なコンテキストスイッチの拒否により、パフォーマンスを向上させることができます。



すべてのアクションは、インターフェイスを構築するためのフレームワークで見られるものと同様に、1つのサイクル(「イベントループ」)で1つのプロセス(スレッド)によって実行されます。



性能





上記のように、従来の同期サーバーと比較した場合のゲインは、カーネルコンテキストの切り替えを放棄できるような計算の単一サイクルを作成することによって実現されます。



このようなループは、トルネード(ioloop)とTwisted(さまざまなリアクター実装)の両方に存在します。 それぞれを理解し、Tornado http-serverのパフォーマンス向上の理由を判断し、各非同期サーバーのコードとアーキテクチャソリューションを評価してみましょう。



竜巻(ioloop)





Tornadoのioloopモジュールは、デフォルトのepollエンジンを使用して、非ブロッキングソケットを処理します。 そのようなプラットフォーム(実際、カーネルバージョン2.6以前のLinuxのみが適している)が提供されていない場合、

ユニバーサルセレクトが使用されます。



メインループの実装は非常にシンプルで、いくつかの小さなファイルに収まります。epoll.c-epollのラッパー、ioloop.py-ループの実装。



epoll.cでは、Python関数はepoll_create、epoll_ctl、epoll_waitをラップし、epollモジュールを宣言します。 このモジュールは、ソケットを使用した非同期操作の標準言語モジュール(選択モジュール)がepollをサポートしない場合(epollクラスを含まない場合)にコンパイルされ、使用されます。



そのため、イベントループ自体は、ioloop.pyモジュールのIOLoopクラスのstartメソッドにあります。 以下は、このメソッドの一部であり、いくつかの詳細な説明があります。



 def start(self):
     self._running = True
     Trueの場合:
         #イベントハンドラーの呼び出しサイクル間のデフォルトのタイムアウト
         #イベントプールの凍結を回避
         poll_timeout = 0.2

         #イベントハンドラーのリストを作成する
        コールバック=リスト(self._callbacks)
        コールバックのコールバック:
             #未使用のリストからハンドラーを削除して実行する
             self._callbacksのコールバックの場合:
                 self._callbacks.remove(コールバック)
                 self._run_callback(コールバック)
	
	 #ハンドラーがある場合、ループ間の遅延の必要はありません
         self._callbacksの場合:
             poll_timeout = 0.0

	 #時間遅延イベントハンドラがあり、指定された
         #時間が経過しました-そのようなハンドラーを実行します。 
         self._timeoutsの場合:
             now = time.time()
            一方、self._timeoutsおよびself._timeouts [0] .deadline <= now:
                タイムアウト= self._timeouts.pop(0)
                 self._run_callback(timeout.callback)
             #次の一連のイベントは標準時間のいずれかに収集されます
	     #遅延、または遅延ハンドラをより早く呼び出す必要がある場合、 
             #このハンドラーに設定された時間の後
             self._timeoutsの場合:
                ミリ秒= self._timeouts [0] .deadline-now
                 poll_timeout = min(ミリ秒、poll_timeout)
	 #プロセス内のプロセッサが作業を停止することを決定した場合-ループを終了します
         self._runningではない場合:
            破る

	 #さらに、一定の時間、プールイベントが収集されます
        試してください:
             event_pairs = self._impl.poll(poll_timeout)
        例外を除く、e:
             e.args ==(4、「システムコールの中断」)の場合:
                 logging.warning(「システムコールの中断」、exc_info = 1)
                続ける
            その他:
                上げる

	 #指定されたファイル記述子(ソケット)に対して、イベントが取り出され、 
	 #ハンドラーが呼び出されます(たとえば、ソケットからデータを読み取る関数-fdopen)
         self._events.update(event_pairs)
        一方self._events:
             fd、events = self._events.popitem()
            試してください:
                 self._handlers [fd](fd、イベント)
             KeyboardInterruptを除く:
                上げる
             OSErrorを除く、e:
                 e [0] == errno.EPIPEの場合:
                     #クライアントへの接続が失われたときに発生
                    合格する
                その他:
                     logging.error( "fd%dのI / Oハンドラーの例外"、
                                   fd、exc_info = True)
            を除く:
                 logging.error( "fd%dのI / Oハンドラーの例外"、
                               fd、exc_info = True)




それは、一般に、すべてです。 特定の時間(または1サイクル)延期された呼び出しとイベントハンドラーは周期的に呼び出されます。 ハンドラーが受信したデータは、完全に読み書きされるのではなく、バッファーを介して徐々に読み書きされます。



同じ単純で簡潔なスタイルで、フレームワークの他のすべてのレベル(httpサーバー、要求ハンドラー、および個々の接続)が記述されます。



ツイスト(リアクター)





フレームワークのtwisted.internet.reactorモジュールは、イベントハンドラーと考えられるエラーを処理する同じイベントループです。



デフォルトでは、Webサーバーリアクター(およびフレームワーク全体)は、非ブロッキングソケットのイベント配信に選択エンジンメカニズムを使用します。 このメカニズムは、UnixおよびWin32プラットフォームで一般的ですが、kqueue(FreeBSD)またはepollリアクター(Linuxのみ)に比べてやや劣ります



トルネードで使用される主なメカニズム(ioloop、epollで動作)の類似物として、EPollReactorリアクターの操作を検討してください。



リアクトルには、すべての非同期ループロジックが集中するいくつかの辞書が含まれています。 辞書はクラスのコンストラクターで宣言されます:



クラスEPollReactor(posixbase.PosixReactorBase):
    実装(IReactorFDSet)
     def __init __(self):
         self._poller = _epoll.epoll(1024)
         self._reads = {}
         self._writes = {}
         self._selectables = {}
         posixbase.PosixReactorBase .__ init __(self)




これにより、イベントプール自体(_poller)が作成されます。 ファイル記述子の整数の乱数へのマッピングを含む辞書(_readsおよび_writes)。 基本的に、これらは単にデータの読み取り(_reads)および書き込み(_writes)のための複数の記述子です。



非同期イベント処理サイクル自体が興味深いため、リアクタクラス(およびその基本クラス)で宣言されたユーティリティメソッドの説明は省略します。



イベントの選択とその処理の反復は次のとおりです(コメントは翻訳され、可能であれば展開されます)。



     def doPoll(self、timeout):タイムアウトがNoneの場合:timeout = 1#反復遅延(イベント収集の時間)をミリ秒に変換timeout = int(timeout * 1000)try:#選択されるイベントの数は、追跡される入力/出力オブジェクトの数によって制限されます#(数はヒューリスティックに選択されます)#およびループをブロックする時間は、引数をループ呼び出し関数に渡します。  l = self._poller.wait(len(self._selectables)、タイムアウト)、IOErrorを除く、err:if err.errno == errno.EINTR:return#信号が中断された場合、反復を終了します。  #その他の場合はすべて、エラーはアプリケーション側でのみ発生する可能性があると想定され、さらに例外を渡す価値があります#イベントの収集中にエラーが発生しなかった場合は、記述子のイベントハンドラーの呼び出しに進みます。  _drdw = fdのself._doReadOrWrite、lのイベント:try:selectable = self._selectables [fd]例外KeyError:pass else:log.callWithLogger(selectable、_drdw、selectable、fd、event) 




リアクタメソッドself._doReadOrWrite(名前は_drdw)には、記述子、その上で発生したイベント、およびイベントハンドラ(見つかった場合)が渡されます。 メソッド自体を見てみましょう。



     def _doReadOrWrite(self、selectable、fd、event):
        理由=なし
         inRead = False
        イベント&_POLL_DISCONNECTEDであり、そうでない場合(イベント&_epoll.IN):
            なぜ= CONNECTION_LOST
        その他:
            試してください:
                イベントと_epoll.INの場合:
                     why = selectable.doRead()
                     inRead = True
                そうでない場合、イベントと_epoll.OUT:
                     why = selectable.doWrite()
                     inRead = False
                 selectable.fileno()!= fdの場合:
                     why = error.ConnectionFdescWentAway(
                           「ファイル記述子がなくなりました」)
                     inRead = False
            を除く:
                 log.err()
                なぜ= sys.exc_info()[1]
        なぜか:
             self._disconnectSelectable(選択可能、なぜ、inRead)




ここでは、ディスクリプタから/へのデータの受信および記録のイベントが処理され、エラー処理が行われます(存在する場合)。



したがって、最低レベルでは、TornadoとTwistedは類似しており、違いはより高い抽象レベルで始まります。 Friendfeedチームの開発では、サイクル上でいくつかの単純なアドオンのみを作成しています(HttpStream-> HttpConnection-> HttpServerなど)。 ここのループは、epollまたはselectのみに基づいています。



Twisted Frameworkは、特別な抽象化(Deferredなど)に基づいて構築されています。 そのリアクターは、Win32用の一対のツールであるpoll、epoll、select、kqueue(MacOSおよびfreeBSD)の幅広いソリューションに実装されています。 インターフェイスを構築するためのフレームワークサイクルに組み込まれたリアクター(PyGTK、wxWidgets)があります。



結論





厳密に言えば、ユニバーサルネットワークフレームワークと特殊なアプリケーションを比較することは困難です。 トルネードコードは一般的にはるかにシンプルで簡潔であり、Pythonの原則とより一貫しています。 不可解なのはテストの欠如だけで、これは現代の開発では下品だと考えられています。



一方、Twistedは、その幅広い機能をすべて備えた、調和と一貫性を保持する普遍的なツールです。 そしてその意味で、それは素晴らしいQt(C ++の元の実装)と比較することができます。 httpサーバーは、そのアプリケーションの特殊なケースにすぎません。 もっとコーディングする

フレームワークのコンポーネントの一部は十分にテストされており、独自のテストツール(試用版)も提供されています。



当然、Twistedは、他の一般化システムと同様に、特殊な開発よりもパフォーマンスが劣ります。



TwistedがTornadoに比べて効率が劣るもう1つの理由と、別の高性能非同期ディーゼルフレームワークは、より高度なエラー処理であり、信頼性を高めますが、大切なRPSを隠します。



そのため、Twistedの主な利点は汎用性です。 竜巻-パフォーマンス。



何を選択しますか? 自分で決めてください。 両方のフレームワークは、Webプログラマーに非常に質素な開発ツールのセットを提供します。これは、そのシンプルさとZopeの包括性においてDjangoより明らかに劣っています。 どちらも速度が向上します(Apacheソリューションと比較して最大20〜30%増加)。



All Articles