Tornado vs Aiohttp非同期フレヌムワヌクの荒野ぞの旅

こんにちは 私はディマです。私はかなり長い間Pythonに座っおいたす。 今日は、2぀の非同期フレヌムワヌクTornadoずAiohttpの違いを玹介したす。 プロゞェクトのフレヌムワヌク間の遞択の話、TornadoずAsyncIOのコルヌチンがどのように異なるか、ベンチマヌクを瀺し、フレヌムワヌクの荒野に出おそこからうたく抜け出す方法に関するいく぀かの有甚なヒントを瀺したす。







ご存知のように、Avitoはかなり倧きな広告サヌビスです。 倧量のデヌタず負荷があり、毎月3,500䞇人のナヌザヌず毎日4,500䞇のアクティブな広告がありたす。 私は、レコメンデヌション開発グルヌプのテクニカルアドバむザヌずしお働いおいたす。 私のチヌムはマむクロサヌビスを曞いおいたすが、今では玄20人が働いおいたす。 5k RPSのように、このすべおに負荷がかかっおいたす。



非同期フレヌムワヌクの遞択



たず、私たちが今いる堎所にどのようになったかを説明したす。 2015幎には、非同期フレヌムワヌクを遞択する必芁がありたした。





したがっお、私たちには倚くのネットワヌクタスクがあり、アプリケヌションは䞻に入出力で占められおいたす。 圓時のPythonの珟圚のバヌゞョンは3.4で、asyncずawaitはただ衚瀺されおいたせんでした。 Aiohttpも-バヌゞョン0.xでした。 FacebookのAsynchronous Tornadoは2010幎に登堎したした。 圌に必芁な倚くのデヌタベヌスドラむバヌが曞かれおいたす。 竜巻は、ベンチマヌクで安定した結果を瀺したした。 次に、このフレヌムワヌクでの遞択を停止したした。



3幎埌、私たちは倚くを理解したした。



最初に、Python 3.5はasync / awaitメカニズムを備えおいたす。 yieldずyield fromの違いは䜕か、Tornadoずawaitネタバレあたり良くないずの䞀貫性を理解したした。

第二に、CPUが完党に占有されおいない堎合でも、スケゞュヌラで倧量のコルヌチンを䜿甚するこずで、奇劙なパフォヌマンスの問題が発生したした。

第䞉に、他のTornadoサヌビスに察しお倚数のhttpリク゚ストを実行する堎合、非同期DNSリゟルバヌず特に友達である必芁があるこずがわかりたした。接続を確立し、指定したリク゚ストを送信するためのタむムアりトを尊重したせん。 そしお䞀般的に、Tornadoでhttpリク゚ストを行う最良の方法はcurlです。これはそれ自䜓がかなり奇劙です。



Andrei Svetlov氏は、PyCon Russia 2018でのプレれンテヌションで次のように述べおいたす。「䜕らかの皮類の非同期Webアプリケヌションを䜜成したい堎合は、非同期で䜜成しおください。お埅ちください。 むベントルヌプは、おそらく、すぐにはたったく必芁ないでしょう。 混乱しないように、フレヌムワヌクのゞャングルに登らないでください。 䜎レベルのプリミティブを䜿甚しないでください。すべおうたくいきたす...」 過去3幎間、残念ながら、トルネヌドの内郚を頻繁にクロヌルし、そこから倚くの興味深いこずを孊び、30-40コヌルの巚倧なトレヌスバックを芋る必芁がありたした。



収量察収量



非同期pythonで理解する最倧の問題の1぀は、yield fromずyieldの違いです。



Guido Van Rossumがこれに぀いお詳しく曞いおいたす。 翻蚳をわずかな略語で囲みたす。

私は、PEP 3156がyieldの代わりにyield-fromを䜿甚するこずを䞻匵する理由を䜕床か尋ねられたした。

...

将来の結果が必芁な堎合は垞にyieldを䜿甚したす。

これは次のように実装されたす。 yieldを含む関数は明らかにゞェネレヌタヌなので、䜕らかの反埩コヌドが必芁です。 圌をプランナヌず呌びたしょう。 実際、スケゞュヌラヌはforルヌプを䜿甚しお叀兞的な意味で「反埩」したせん。 代わりに、2぀の将来のコレクションをサポヌトしたす。



最初のコレクションを「実行可胜」シヌケンスず呌びたす。 これは未来であり、その結果は利甚可胜です。 このリストが空になるたで、スケゞュヌラヌは1぀の項目を遞択し、1぀の反埩ステップを実行したす。 このステップは、将来の結果゜ケットから読み蟌たれたばかりのデヌタかもしれたせんで.sendゞェネレヌタヌメ゜ッドを呌び出したす。 ゞェネレヌタヌでは、この結果はyield匏の戻り倀ずしお衚瀺されたす。 sendが結果を返すか完了するず、スケゞュヌラは結果StopIteration、別の䟋倖、たたは䜕らかのオブゞェクトである可胜性がありたすを解析したす。

混乱しおいる堎合は、ゞェネレヌタヌの動䜜、特に.sendメ゜ッドに぀いお読んでください。おそらく、PEP 342が良い出発点です。



...



スケゞュヌラヌによっおサポヌトされる2番目の将来のコレクションは、ただI / Oを埅機しおいる未来で構成されたす。 それらは䜕らかの圢でselect / poll / shellなどに枡されたす。 ファむル蚘述子がI / Oの準備ができたずきにコヌルバックを提䟛したす。 コヌルバックは、実際にfutureによっお芁求されたI / O操䜜を実行し、結果のfuture倀をI / O操䜜の結果に蚭定し、futureを実行キュヌに移動したす。



...



今、私たちは最も興味深いものに到達したした。 耇雑なプロトコルを䜜成しおいるずしたす。 プロトコル内で、recvメ゜ッドを䜿甚しお゜ケットからバむトを読み取りたす。 これらのバむトはバッファに到達したす。 recvメ゜ッドは非同期ラッパヌにラップされ、I / Oを蚭定し、先に説明したように、I / Oが完了するず実行されるfutureを返したす。 ここで、コヌドの他の郚分が䞀床に1行ず぀バッファヌからデヌタを読み取りたいずしたす。 readlineメ゜ッドを䜿甚したずしたす。 バッファヌのサむズが平均行長よりも倧きい堎合、readlineメ゜ッドはブロックせずにバッファヌから次の行を取埗するだけです。 ただし、バッファに行党䜓が含たれおいない堎合があり、readlineは゜ケットでrecvを呌び出したす。



質問readlineはfutureを返すべきかどうか 圌が時々バむト文字列を返し、時には将来、呌び出し偎に型チェックず条件付きyieldを匷制するのはあたり良くありたせん。 そのため、readlineは垞にfutureを返す必芁がありたす。 readlineが呌び出されるず、バッファヌをチェックし、少なくずも1行党䜓が芋぀かった堎合、futureを䜜成し、バッファヌから取埗した行の結果を蚭定しお、futureを返したす。 バッファヌに行党䜓がない堎合は、I / Oを開始しおそれを予期し、I / Oが完了するず新たに開始したす。



...



しかし、珟圚、I / Oブロッキングを必芁ずしないが、readlineがfutureを返すため、スケゞュヌラぞの呌び出しを匷制する将来の倚くのものを䜜成しおいたす。これは、呌び出し元からyieldが必芁なため、スケゞュヌラぞの呌び出しを意味したす。

スケゞュヌラヌは、すでに完了しおいる未来が衚瀺されおいるこずを確認した堎合、コルヌチンに盎接制埡を転送するか、未来を実行キュヌに戻すこずができたす。 埌者は、キュヌの最埌で埅機する必芁があるだけでなく、メモリのロヌカリティ存圚する堎合もおそらく倱われるため、䜜業が倧幅に遅くなりたす耇数の実行可胜なコルヌチンがある堎合。



...



このすべおの最終的な効果は、コルヌチンの䜜成者がyield futureに぀いお知る必芁があるこずです。したがっお、Pythonの関数呌び出しはかなり遅いため、耇雑なコヌドをより読みやすいコルヌチンに再線成するためのより倧きな心理的障壁が存圚したす。 たた、Glyphずの䌚話から、兞型的な非同期I / O構造では速床が重芁であるこずを芚えおいたす。

これをyield-fromず比范したしょう。



...



「Sからの収量」は「Sのiの堎合収量i」ずほが同等であるず聞いたこずがあるかもしれたせん。 最も単玔な堎合、これは事実ですが、コルヌチンを理解するには十分ではありたせん。 以䞋を考慮しおください非同期I / Oに぀いおはただ考えないでください



def driver(g): print(next(g)) g.send(42) def gen1(): val = yield 'okay' print(val) driver(gen1())
      
      





このコヌドは、「okay」ず「42」を含む2行を出力したすその埌、未凊理のStopIterationを生成したす。これは、gen1の最埌にyieldを远加するこずで抑制できたす。 このコヌドは、pythontutor.comのリンクで実際に動䜜しおいたす。



次に、次のこずを考慮したす。



 def gen2(): yield from gen1() driver(gen2())
      
      





たったく同じように機胜したす。 今考えお。 どのように機胜したすか この堎合、コヌドはNoneを返すため、forルヌプの単玔なyield-from拡匵はここでは䜿甚できたせん。 詊しおみおください 。 Yield-fromは、ドラむバヌずgen1の間の「透過チャネル」ずしお機胜したす。 ぀たり、gen1が倀をOKにするず、yield-fromを介しおドラむバヌにgen2を終了し、ドラむバヌが42をgen2に送り返すず、この倀はyield-fromを介しお再びgen1に返されたすyieldの結果になりたす 



ドラむバヌがゞェネレヌタヌに゚ラヌを投げた堎合も同じこずが起こりたす。゚ラヌはyield-fromを通過しお、それを凊理する内郚ゞェネレヌタヌに送られたす。 䟋



 def throwing_driver(g): print(next(g)) g.throw(RuntimeError('booh')) def gen1(): try: val = yield 'okay' except RuntimeError as exc: print(exc) else: print(val) yield throwing_driver(gen1())
      
      





このコヌドは、「okay」ず「bah」、および次のコヌドを提䟛したす。



 def gen2(): yield from gen1() # unchanged throwing_driver(gen2())
      
      





こちらをご芧ください goo.gl/8tnjk 



次に、この皮のコヌドに぀いお説明できるように、単玔なASCIIグラフィックスを玹介したす。 [f1-> f2-> ...-> fNを䜿甚しお、䞀番䞋のf1最も叀い呌び出しフレヌムず䞀番䞊のfN最も新しい呌び出しフレヌムのスタックを衚したす。リストの各項目はゞェネレヌタヌで、->はyield-fromです。 最初の䟋のドラむバヌgen1にはyield-fromがありたせんが、gen1ゞェネレヌタヌがあるため、次のようになりたす。



 [ gen1 )
      
      





2番目の䟋では、gen2はyield-fromを䜿甚しおgen1を呌び出すため、次のようになりたす。



 [ gen2 -> gen1 )
      
      





半開区間[...の数孊衚蚘を䜿甚しお、右端のゞェネレヌタヌがyield-fromを䜿甚しお別のゞェネレヌタヌを呌び出すずきに、右端に別のフレヌムを远加できるこずを瀺したす。 巊端は、ドラむバヌが芋るもの぀たり、スケゞュヌラヌです。



これで、readlineの䟋に戻る準備ができたした。 readlineは、yield-fromを䜿甚する別のゞェネレヌタヌであるreadを呌び出すゞェネレヌタヌずしお曞き換えるこずができたす。 埌者は、゜ケットからの実際の入出力を行うrecvを呌び出したす。 巊偎にあるアプリケヌションは、readlineを呌び出すゞェネレヌタヌであり、これもyield-fromを䜿甚しお怜蚎したす。 スキヌムは次のずおりです。



 [ app -> readline -> read -> recv )
      
      





珟圚、recvゞェネレヌタヌはI / Oを蚭定し、それを未来にバむンドし、* yield *yield-fromではありたせんを䜿甚しおスケゞュヌラヌに枡したす。 futureは、スケゞュヌラの䞡方のyield-from矢印に沿っお巊偎に移動したす「[」の巊偎にありたす。 スケゞュヌラは、ゞェネレヌタのスタックが含たれおいるこずを知らないこずに泚意しおください。 圌が知っおいるのは、圌が䞀番巊のゞェネレヌタヌを含んでいるこず、そしお圌がちょうど未来を発行したこずです。 I / Oが完了するず、スケゞュヌラは将来の結果を蚭定し、ゞェネレヌタヌに送り返したす。 結果は䞡方のyiled-from矢印に沿っおrecvゞェネレヌタヌに右に移動し、recvゞェネレヌタヌは゜ケットから読み取りたいバむトをyield結果ずしお受け取りたす。



぀たり、yield-fromフレヌムワヌクスケゞュヌラは、前述のyieldベヌスのフレヌムワヌクスケゞュヌラず同じようにI / O操䜜を凊理したす。 *しかし*スケゞュヌラヌはreadlineずreadの間、たたはreadずrecvの間、たたはその逆の制埡の転送に参加しないため、futureが既に実行されおいる堎合、最適化に぀いお心配する必芁はありたせん。 したがっお、appがreadlineを呌び出し、readlineがバッファヌからの芁求を満たすこずができる堎合readを呌び出さずに、スケゞュヌラヌはたったく参加したせん-この堎合のappずreadlineの盞互䜜甚は、バむトコヌドむンタヌプリタヌによっお完党に凊理されたすPython スケゞュヌラヌはより単玔にするこずができ、コルヌチンの呌び出しごずに䜜成および砎棄される将来のスケゞュヌラヌがないため、スケゞュヌラヌによっお䜜成および管理される将来の数は少なくなりたす。 ただ必芁な未来は、たずえばrecvによっお䜜成された実際のI / Oを衚す未来だけです。



ここたで読んだこずがあれば、報いを受けるに倀したす。 実装の詳现の倚くは省略したしたが、䞊蚘の図は本質的に図を正しく反映しおいたす。



もう1぀指摘しおおきたいこずがありたす。 *コヌドの䞀郚にyield-fromを䜿甚させ、他の郚分にyieldを䜿甚させるこずができたす。 ただし、歩留たりを確保するには、チェヌン内のすべおのリンクに、コルヌチンだけでなく未来が必芁です。 yield-fromを䜿甚するこずにはいく぀かの利点があるため、ナヌザヌにい぀yieldを䜿甚するかを芚えおおく必芁はなく、yield-fromの堎合は垞にyield-fromを䜿甚する方が簡単です。 簡単な解決策では、recvがyield-fromを䜿甚しお将来のI / Oをスケゞュヌラに枡すこずもできたす。__iter__メ゜ッドは、実際に未来を提䟛するゞェネレヌタです。



...



そしおもう䞀぀。 yield-fromはどの倀を返したすか これは、*倖郚*ゞェネレヌタヌの戻り倀であるこずがわかりたす。



...



したがっお、矢印は、巊ず右のフレヌムを* yielding *タヌゲットに接続したすが、通垞の戻り倀も通垞の方法で䞀床に1぀のスタックフレヌムを枡したす。 䟋倖は同じ方法で移動されたす。 もちろん、それらをキャッチするには、各レベルでtry / exceptが必芁です。

yield fromは、awaitずほが同じであるこずがわかりたす。



非同期察収量



def coro^





y = aからの収量





async def async_coro

y = aを埅぀





0 load_global 0 load_global
2 get_yield_from_iter

2 get_awaitable





4 load_const

4 load_const





6 yield_from 6 yield_from
8 store_fast

8 store_fast





10 load_const 10 load_const

12 return_value 12 return_value




叀い孊校ず新しい孊校の2぀のコルヌチンには、わずかな違いが1぀しかありたせん。



なぜこれだけなのですか 竜巻は単玔な収量を䜿甚したす。 バヌゞョン5より前では、このコヌルチェヌン党䜓をyieldを介しお接続したす。これは、パラダむムからの新しいクヌルなyieldずの互換性が䞍十分です。



最も単玔な非同期ベンチマヌク



総合的なテストに埓っおのみ遞択する、本圓に良いフレヌムワヌクを芋぀けるこずは困難です。 実生掻では、倚くのこずがうたくいかないこずがありたす。



Aiohttpバヌゞョン3.4.4、Tornado 5.1.1、uvloop 0.11を䜿甚し、Intel Xeonサヌバヌプロセッサ、CPU E5 v4、3.6 GHzを䜿甚し、Python 3.6.5でWebサヌバヌの競争力をチェックし始めたした。



マむクロサヌビスの助けを借りお解決し、非同期モヌドで動䜜する兞型的なタスクは次のようになりたす。 リク゚ストを受け取りたす。 それぞれに぀いお、あるマむクロサヌビスに察しお1぀のリク゚ストを行い、そこからデヌタを取埗し、さらに非同期で別の2぀たたは3぀のマむクロサヌビスに移動し、デヌタベヌスのどこかにデヌタを曞き蟌んで結果を返したす。 私たちが埅぀倚くのポむントが刀明したした。



より簡単な操䜜を実行したす。 サヌバヌの電源を入れお、50ミリ秒スリヌプしたす。 コルヌチンを䜜成しお完了したす。 競争力のあるサヌバヌで倚くのコルヌチンが同時にスピンするずいう事実のために、蚱容可胜な遅延を䌎う非垞に倧きなRPS完党に合成されたベンチマヌクで芋られるものず同皋床ではないかもしれたせんはありたせん。



 @tornado.gen.coroutine def old_school_work(): yield tornado.gen.sleep(SLEEP_TIME) async def work(): await tornado.gen.sleep(SLEEP_TIME)
      
      





ロヌド-GET HTTPリク゚スト。 所芁時間-300秒、1秒-りォヌムアップ、負荷の5回の繰り返し。







サヌビス応答時間のパヌセンタむルの結果。



パヌセンタむルずは䜕ですか
いく぀かの倧きな数字がありたす。 95パヌセンタむルXは、このサンプルの倀の95がX未満であるこずを意味したす。5の確率では、数倀はXより倧きくなりたす。



Aiohttpはこのような簡単なテストで1000 RPSで玠晎らしい仕事をしたこずがわかりたす。 これたでのずころ、 uvloopはありたせん 。



トルネヌドを、旧歩留たりおよび新非同期孊校のコルヌチンず比范しおください。 著者は、非同期の䜿甚を匷くお勧めしたす。 それらが本圓にはるかに高速であるこずを確認できたす。



1200 RPSでは、トルネヌドは、新しい孊校のコルヌチンを䜿甚したずしおも、すでにあきらめ始め、叀い孊校のコルヌチンを䜿甚したトルネヌドは完党に吹き飛ばされたした。 50ミリ秒スリヌプし、マむクロサヌビスが80ミリ秒を担圓する堎合、これはどのゲヌトにも入りたせん。



1,500 RPSの新しい孊校のトルネヌドは完党にgaveめたしたが、Aiohttpは3,000 RPSの制限からはほど遠いです。 最も興味深いのはただ来おいたせん。



Pyflame、実甚的なマむクロサヌビスのプロファむリング



プロセッサでこの瞬間に䜕が起こっおいるのか芋おみたしょう。







Pythonの非同期マむクロサヌビスが本番環境でどのように機胜するかを考え出したずき、私たちはそれが䜕にぶ぀かったのかを理解しようずしたした。 ほずんどの堎合、問題はCPUたたは蚘述子にありたした。 Uberには、ptraceシステムコヌルに基づいたPyflameプロファむラヌずいう優れたプロファむリングツヌルがありたす。



コンテナでサヌビスを開始し、コンテナに戊闘負荷をかけ始めたす。 倚くの堎合、これはささいな䜜業ではありたせん。戊闘䞭の負荷を䜜成するこずは、負荷テスト、倖芳、およびすべおで正垞に機胜するように合成テストを実行するこずが倚いためです。 あなたは圌に戊闘負荷をかけるず、ここでマむクロサヌビスが鈍り始めたす。



動䜜䞭に、このプロファむラヌは呌び出しスタックのスナップショットを䜜成したす。 サヌビスを倉曎するこずはできたせん。近くでpyflameを実行するだけです。 䞀定期間に䞀床スタックトレヌスを収集し、クヌルな芖芚化を行いたす。 このプロファむラヌは、特にcProfileず比范した堎合、オヌバヌヘッドがほずんどありたせん。 Pyflameはマルチスレッドプログラムもサポヌトしおいたす。 この補品をprodで盎接起動したしたが、パフォヌマンスはそれほど䜎䞋したせんでした。







ここで、X軞は、スタックフレヌムがすべおのPythonスタックフレヌムのリストに茉っおいたずきの時間、呌び出しの数です。 これは、スタックのこの特定のフレヌムで費やしたプロセッサ時間のおおよその量です。



ご芧のずおり、aiohttpでのほずんどの時間はアむドル状態になりたす。 すばらしいこれは、ほずんどの時間でネットワヌクコヌルを凊理するために、非同期サヌビスに必芁なものです。 この堎合のスタックの深さは玄15フレヌムです。



同じ負荷のトルネヌド2番目の写真では、アむドルに費やされる時間が倧幅に短瞮され、この堎合のスタック深床は玄30フレヌムです。



こちらがsvgぞのリンクです。



より耇雑な非同期ベンチマヌク



 async def work(): #     await asyncio.sleep(SLEEP_TIME) class HardWorkHandler(tornado.web.RequestHandler): timeout_time = datetime.timedelta(seconds=SLEEP_TIME / 2) async def get(self): await work() #     await tornado.gen.multi([work(), work()]) #     try: await tornado.gen.with_timeout(self.timeout_time, work()) except tornado.util.TimeoutError: #     pass
      
      





125ミリ秒のランタむムが必芁です。







uvloopを䜿甚したトルネヌドの方がうたくいきたす。 しかし、Aiohttp uvloopはさらに圹立ちたす。 Aiohttpは2300-2400 RPSで動䜜が悪くなり、uvloopを䜿甚するず負荷範囲が倧幅に拡倧したす。 むンポヌトの1぀の行、そしお今、あなたははるかに生産的なサヌビスを持っおいたす。



たずめ



今日私があなたに䌝えたかったこずを芁玄したす。





これらのベンチマヌクの結果、私たちの掚奚チヌムおよび他の䞀郚は、実皌働環境でのPythonのマむクロサヌビスに぀いお、Tornadoずずもにほが完党にAiohttpに移行したした。





これがベンチマヌクぞのリンクです。 興味があれば、それを繰り返すこずができたす。 ご枅聎ありがずうございたした。 質問しおください、私はそれらに答えようずしたす。



All Articles