de Paweł Piotr Przeradowski

Un milion de cereri pe secundă cu Python

Un milion de cereri pe secunda cu Python

Este posibil să atingi un milion de cereri pe secundă cu Python? Probabil nu până de curând.

O mulțime de companii migrează departe de Python și către alte limbaje de programare, astfel încât să își poată spori performanța de funcționare și să economisească la prețurile serverului, dar nu este nevoie cu adevărat. Python poate fi instrumentul potrivit pentru job.

Comunitatea Python face multe performanțe în ultimul timp. CPython 3.6 a îmbunătățit performanța generală a interpretului cu noua implementare a dicționarului. CPython 3.7 va fi și mai rapid, grație introducerii unor convenții de apeluri mai rapide și a unor cache-uri de căutare a dicționarului.

Pentru sarcini de reducere a numărului, puteți utiliza PyPy cu compilarea codului său just-in-time. De asemenea, puteți rula suita de testare NumPy, care acum a îmbunătățit compatibilitatea generală cu extensiile C. La sfârșitul acestui an, PyPy va atinge conformitatea Python 3.5.

ad-banner

Toate aceste lucrări minunate m-au inspirat să inovez într-unul dintre domeniile în care Python este utilizat pe scară largă: dezvoltarea web și a microserviciului.

Intrați în Japronto!

Japronto este un mic-cadru nou, adaptat nevoilor dvs. de micro-servicii. Principalele sale obiective includ ființa rapid, scalabil, și ușor. Vă permite să faceți ambele sincron și asincron programare datorită asincio. Și este nerușinat rapid. Chiar mai rapid decât NodeJS și Go.

Un milion de cereri pe secunda cu Python
Microcadre Python (albastru), partea întunecată a forței (verde) și Japronto (violet)

Erată: După cum subliniază utilizatorul @heppu, serverul HTTP stdlib al lui Go poate fi Cu 12% mai rapid decât arată acest grafic când este scris mai atent. De asemenea, există o minunată fasthttp server pentru Go, care se pare că este doar cu 18% mai lent decât Japronto în acest punct de referință special. Minunat! Pentru detalii vezi https://github.com/squeaky-pl/japronto/pull/12 și https://github.com/squeaky-pl/japronto/pull/14.

1611267909 362 Un milion de cereri pe secunda cu Python

De asemenea, putem vedea că serverul Meinheld WSGI este aproape la egalitate cu NodeJS și Go. În ciuda designului său de blocare inerent, este un performant excelent în comparație cu cele patru precedente, care sunt soluții Python asincrone. Așadar, nu aveți încredere în nimeni care spune că sistemele asincrone sunt întotdeauna mai rapide. Sunt aproape întotdeauna mai concurente, dar există mult mai mult decât atât.

Am realizat acest micro benchmark folosind „Hello world!” aplicație, dar demonstrează în mod clar overhead-ul server-framework pentru o serie de soluții.

Aceste rezultate au fost obținute pe o instanță AWS c4.2xlarge care avea 8 VCPU-uri, lansate în regiunea São Paulo cu chirie partajată implicită și virtualizare HVM și stocare magnetică. Mașina rulează Ubuntu 16.04.1 LTS (Xenial Xerus) cu nucleul Linux 4.4.0–53-generic x86_64. Sistemul de operare raporta CPU Xeon® E5–2666 v3 @ 2.90GHz CPU. Am folosit Python 3.6, pe care l-am compilat proaspăt din codul său sursă.

Pentru a fi corect, toți concurenții (inclusiv Go) derulau un proces cu un singur lucrător. Serverele au fost testate la încărcare folosind wrk cu 1 thread, 100 de conexiuni și 24 de cereri simultane (conectate prin conducte) per conexiune (paralelism cumulativ de 2400 de cereri).

1611267909 804 Un milion de cereri pe secunda cu Python
HTTP pipelining (imagine credit Wikipedia)

Canalizare HTTP este crucial aici, deoarece este una dintre optimizările pe care Japronto le ia în considerare atunci când execută cereri.

Majoritatea serverelor execută cereri de la clienți de canalizare în același mod ca și clienții care nu sunt de canalizare. Nu încearcă să-l optimizeze. (De fapt, Sanic și Meinheld vor renunța, de asemenea, în liniște, la cererile clienților de canalizare, ceea ce reprezintă o încălcare a protocolului HTTP 1.1).

În cuvinte simple, pipelining-ul este o tehnică în care clientul nu trebuie să aștepte răspunsul înainte de a trimite cereri ulterioare prin aceeași conexiune TCP. Pentru a asigura integritatea comunicării, serverul trimite înapoi mai multe răspunsuri în aceeași comandă sunt primite cereri.

Detaliile sângeroase ale optimizărilor

Când multe solicitări GET mici sunt conectate de client, există o mare probabilitate ca acestea să ajungă într-un pachet TCP (datorită Algoritmul lui Nagle) pe partea de server, apoi fiți citit înapoi cu unul apel de sistem.

Efectuarea unui apel de sistem și mutarea datelor din spațiul kernel în spațiul utilizator este o operațiune foarte costisitoare în comparație cu, să zicem, mutarea memoriei în spațiul procesului. De aceea, este important să efectuați cât mai puține apeluri de sistem necesare (dar nu mai puțin).

Când Japronto primește date și analizează cu succes mai multe cereri din acesta, încearcă să execute toate cererile cât mai repede posibil, lipiți răspunsurile înapoi în ordine corectă, apoi scrie înapoi în un apel de sistem. De fapt, nucleul poate ajuta la partea de lipire, datorită împrăștie / adună IO apeluri de sistem, pe care Japronto nu le folosește încă.

Rețineți că acest lucru nu este întotdeauna posibil, deoarece unele dintre cereri ar putea dura prea mult, iar așteptarea lor ar crește inutil latența.

Aveți grijă când reglați euristicile și luați în considerare costul apelurilor de sistem și timpul estimat de finalizare a cererii.

1611267909 154 Un milion de cereri pe secunda cu Python
Japronto oferă o mediană de 1.214.440 RPS de date continue grupate, calculate ca percentila 50, folosind interpolare.

În afară de întârzierea scrierilor pentru clienții canalizați, există mai multe alte tehnici pe care le folosește codul.

Japronto este scris aproape în întregime în C. Analizatorul, protocolul, secerătorul de conexiune, routerul, cererea și obiectele de răspuns sunt scrise ca extensii C.

Japronto încearcă din greu să întârzie crearea omologilor Python din structurile sale interne până când nu li se cere în mod explicit. De exemplu, un dicționar de anteturi nu va fi creat până când nu este solicitat într-o vizualizare. Toate limitele jetonului sunt deja marcate înainte, dar normalizarea cheilor de antet și crearea mai multor obiecte str se face atunci când sunt accesate pentru prima dată.

Japronto se bazează pe excelenta bibliotecă C picohttpparser pentru analiza liniei de stare, a anteturilor și a unui corp de mesaje HTTP blocat. Picohttpparser folosește în mod direct instrucțiuni de procesare a textului găsite în procesoarele moderne cu extensii SSE4.2 (aproape orice procesor x86_64 de 10 ani îl are) pentru a se potrivi rapid cu limitele token-urilor HTTP. I / O este gestionat de uvloop super minunat, care în sine este un înveliș în jurul libuv. La cel mai scăzut nivel, acesta este un pod către apelul de sistem epoll care oferă notificări asincrone privind pregătirea pentru citire-scriere.

1611267910 979 Un milion de cereri pe secunda cu Python
Picohttpparser se bazează pe SSE4.2 și CMPESTRI x86_64 intrinseci pentru a face analiza

Python este un limbaj colectat de gunoi, deci trebuie să aveți grijă atunci când proiectați sisteme de înaltă performanță, astfel încât să nu creșteți inutil presiunea asupra colectorului de gunoi. Proiectarea internă a Japronto încearcă să evite ciclurile de referință și să facă cât mai puține alocări / repartizări după cum este necesar. Face acest lucru prin prealocarea unor obiecte în așa-numitele arene. De asemenea, încearcă să refolosească obiecte Python pentru cereri viitoare dacă nu mai sunt referențiate în loc să le arunce.

Toate alocările se fac ca multipli de 4KB. Structurile interne sunt amenajate cu atenție, astfel încât datele utilizate frecvent împreună să fie suficient de aproape în memorie, minimizând posibilitatea de a pierde memoria cache.

Japronto încearcă să nu copieze în mod inutil între buffere și face multe operații în loc. De exemplu, decodează procentual calea înainte de a se potrivi în procesul routerului.

Colaboratori open source, aș putea folosi ajutorul tău.

Am lucrat la Japronto continuu pentru ultimele 3 luni – adesea în weekend, precum și în zilele normale de lucru. Acest lucru a fost posibil doar datorită faptului că am luat o pauză de la jobul meu regulat de programator și mi-am pus tot efortul în acest proiect.

Cred că este timpul să împărtășesc comunității fructul muncii mele.

În prezent Japronto implementează un set de caracteristici destul de solid:

  • Implementare HTTP 1.x cu suport pentru încărcări blocate
  • Suport complet pentru canalizarea HTTP
  • Păstrați conexiunile live cu secerorul configurabil
  • Suport pentru vizualizări sincrone și asincrone
  • Model master-multi-lucrător bazat pe bifurcare
  • Suport pentru reîncărcarea codului la modificări
  • Rutare simplă

Aș dori să mă uit în Websockets și să transmit în flux asincron răspunsurile HTTP.

Există o mulțime de muncă de făcut în ceea ce privește documentarea și testarea. Dacă sunteți interesat să ajutați, vă rog contactați-mă direct pe Twitter. Iată Depozitul de proiecte GitHub al lui Japronto.

De asemenea, dacă compania dvs. caută un dezvoltator Python care să fie ciudat în ceea ce privește performanța și să facă și DevOps, sunt deschis să aud despre asta. Voi lua în considerare poziții la nivel mondial.

Cuvinte finale

Toate tehnicile pe care le-am menționat aici nu sunt chiar specifice Python. Acestea ar putea fi folosite probabil în alte limbi precum Ruby, JavaScript sau chiar PHP. Și aș fi interesat să fac o astfel de muncă, dar, din păcate, acest lucru nu se va întâmpla decât dacă cineva o poate finanța.

Aș dori să mulțumesc comunității Python pentru investițiile lor continue în ingineria performanței. Și anume Victor Stinner @VictorStinner, INADA Naoki @methane și Yury Selivanov @ 1st1 și întreaga echipă PyPy.

Pentru dragostea lui Python.