En million anmodninger pr. Sekund med Python

Er det muligt at ramme en million anmodninger i sekundet med Python? Sandsynligvis ikke før for nylig.

Mange virksomheder migrerer væk fra Python og til andre programmeringssprog, så de kan øge deres driftsydelse og spare på serverpriser, men der er ikke behov for det. Python kan være det rigtige værktøj til jobbet.

Python-samfundet udfører en masse omkring ydeevne på det seneste. CPython 3.6 forbedrede den samlede tolkes ydeevne med ny implementering af ordbogen. CPython 3.7 bliver endnu hurtigere takket være indførelsen af ​​hurtigere opkaldskonvention og ordbogsopslagscacher.

Til nummerknusende opgaver kan du bruge PyPy med sin just-in-time kodekompilering. Du kan også køre NumPys testpakke, som nu har forbedret den samlede kompatibilitet med C-udvidelser. Senere på året forventes PyPy at nå Python 3.5-overensstemmelse.

Alt dette fantastiske arbejde inspirerede mig til at innovere inden for et af de områder, hvor Python bruges i vid udstrækning: web- og mikrotjenesteudvikling.

Indtast Japronto!

Japronto er en helt ny mikroramme, der er skræddersyet til dine mikrotjenester. Dets vigtigste mål inkluderer at være hurtig , skalerbar og let . Det giver dig mulighed for at udføre både synkron og asynkron programmering takket være asyncio . Og det er skamløst hurtigt . Endnu hurtigere end NodeJS og Go.

Errata: Som bruger @heppu påpeger, kan Go's stdlib HTTP-server være 12% hurtigere end denne graf viser, når den er skrevet mere omhyggeligt. Der er også en fantastisk fasthttp- server til Go, der tilsyneladende kun er 18% langsommere end Japronto i dette specifikke benchmark. Fantastisk! For detaljer se //github.com/squeaky-pl/japronto/pull/12 og //github.com/squeaky-pl/japronto/pull/14.

Vi kan også se, at Meinheld WSGI-server næsten er på niveau med NodeJS og Go. På trods af dets iboende blokerende design er det en god performer sammenlignet med de foregående fire, som er asynkrone Python-løsninger. Så stol aldrig på nogen, der siger, at asynkrone systemer altid er hurtigere. De er næsten altid mere samtidige, men der er meget mere til det end bare det.

Jeg udførte dette mikrobenchmark ved hjælp af en "Hello world!" applikation, men det viser tydeligt server-framework overhead til en række løsninger.

Disse resultater blev opnået på en AWS c4.2xlarge forekomst, der havde 8 VCPU'er, lanceret i São Paulo-regionen med standard delt leje- og HVM-virtualisering og magnetisk lagring. Maskinen kørte Ubuntu 16.04.1 LTS (Xenial Xerus) med Linux 4.4.0-53-generisk x86_64-kerne. Operativsystemet rapporterede Xeon® CPU E5–2666 v3 @ 2,90 GHz CPU. Jeg brugte Python 3.6, som jeg frisk kompilerede ud fra kildekoden.

For at være retfærdig kørte alle deltagerne (inklusive Go) en enkeltarbejderproces. Servere blev belastet testet ved hjælp af wrk med 1 tråd, 100 forbindelser og 24 samtidige (pipelined) anmodninger pr. Forbindelse (kumulativ parallelitet af 2400 anmodninger).

HTTP-rørledning er afgørende her, da det er en af ​​de optimeringer, som Japronto tager højde for, når de udfører anmodninger.

De fleste af serverne udfører forespørgsler fra pipelining-klienter på samme måde som fra ikke-pipelining-klienter. De prøver ikke at optimere det. (Faktisk vil Sanic og Meinheld også stille stille henvendelser fra pipelining-klienter, hvilket er en overtrædelse af HTTP 1.1-protokollen.)

Med enkle ord er pipelining en teknik, hvor klienten ikke behøver at vente på svaret, før han sender efterfølgende anmodninger over den samme TCP-forbindelse. For at sikre kommunikationens integritet sender serveren flere svar i den samme ordre, anmodninger modtages.

De blodige detaljer om optimeringer

Når mange små GET-anmodninger pipelineres sammen af ​​klienten, er der stor sandsynlighed for, at de ankommer i en TCP-pakke (takket være Nagles algoritme) på serversiden og derefter læses tilbage ved et systemopkald .

At foretage et systemopkald og flytte data fra kernel-space til user-space er en meget dyr operation sammenlignet med f.eks. At flytte hukommelse inde i procesrummet. Derfor er det vigtigt at udføre så få som nødvendigt systemopkald (men ikke mindre).

Når Japronto modtager data og med succes analyserer flere anmodninger ud af det, forsøger det at udføre alle anmodningerne så hurtigt som muligt, lim svar tilbage i den rigtige rækkefølge og skriv derefter tilbage i et systemopkald . Faktisk kan kernen hjælpe med limingsdelen takket være scatter / samle IO-systemopkald, som Japronto ikke bruger endnu.

Bemærk, at dette ikke altid er muligt, da nogle af anmodningerne kan tage for lang tid, og at vente på dem unødigt øger latenstiden.

Vær forsigtig, når du indstiller heuristikken, og overvej omkostningerne ved systemopkald og den forventede afslutningstid for anmodningen.

Udover at udskyde skrivning til pipelined-klienter er der flere andre teknikker, som koden anvender.

Japronto er skrevet næsten udelukkende i C. Parseren, protokollen, forbindelsesreaperen, routeren, anmodningen og reaktionsobjekterne er skrevet som C-udvidelser.

Japronto forsøger hårdt at forsinke oprettelsen af ​​Python-kolleger til sine interne strukturer, indtil de udtrykkeligt bliver spurgt. For eksempel oprettes der ikke en overskriftordbog, før den bliver anmodet om i en visning. Alle token-grænser er allerede markeret før, men normalisering af header-nøgler, og oprettelse af flere str-objekter sker, når de åbnes for første gang.

Japronto er afhængig af det fremragende picohttpparser C-bibliotek til parsing af statuslinje, overskrifter og en chunked HTTP-meddelelsesdel. Picohttpparser anvender direkte instruktioner til tekstbehandling, der findes i moderne CPU'er med SSE4.2-udvidelser (næsten enhver 10-årig x86_64 CPU har det) for hurtigt at matche grænserne for HTTP-tokens. I / O håndteres af den super fantastiske uvloop, som i sig selv er en indpakning omkring libuv. På det laveste niveau er dette en bro til epoll-systemopkald, der giver asynkrone meddelelser om læs-skriv-beredskab.

Python er et sprog, der er indsamlet skrald, så der skal udvises forsigtighed, når man designer højtydende systemer for ikke unødigt at øge presset på affaldssamleren. Det interne design af Japronto forsøger at undgå referencecykler og udføre så få allokeringer / deallokationer som nødvendigt. Det gør det ved at forhåndslokalisere nogle objekter i såkaldte arenaer. Det forsøger også at genbruge Python-objekter til fremtidige anmodninger, hvis de ikke længere henvises til i stedet for at smide dem væk.

Alle tildelinger udføres som multipla af 4KB. Interne strukturer er omhyggeligt lagt ud, så data, der ofte bruges sammen, er tæt nok i hukommelsen, hvilket minimerer muligheden for cache-fejl.

Japronto forsøger ikke at kopiere mellem buffere unødigt og udfører mange operationer på plads. For eksempel afkoder den procentvis stien, før den matches i routerprocessen.

Open source-bidragydere, jeg kunne bruge din hjælp.

Jeg har arbejdet på Japronto kontinuerligt i de sidste 3 måneder - ofte i weekenden såvel som normale arbejdsdage. Dette var kun muligt på grund af at jeg tog en pause fra mit almindelige programmørjob og lagde al min indsats i dette projekt.

Jeg synes, det er på tide at dele frugten af ​​mit arbejde med samfundet.

I øjeblikket implementerer Japronto et ret solidt funktionssæt:

  • HTTP 1.x implementering med understøttelse af store uploads
  • Fuld support til HTTP-rørledning
  • Hold forbindelser med konfigurerbar skærer
  • Understøttelse af synkron og asynkron visning
  • Master-multiworker-model baseret på gafler
  • Støtte til kodeindlæsning ved ændringer
  • Enkel routing

Jeg vil gerne undersøge Websockets og streame HTTP-svar asynkront næste gang.

Der er meget arbejde, der skal udføres med hensyn til dokumentation og test. Hvis du er interesseret i at hjælpe, bedes du kontakte mig direkte på Twitter. Her er Japronto's GitHub-projektlager.

Også, hvis din virksomhed er på udkig efter en Python-udvikler, der er en præstationsfreak og også gør DevOps, er jeg åben for at høre om det. Jeg vil overveje stillinger over hele verden.

Afsluttende ord

Alle de teknikker, jeg har nævnt her, er ikke rigtig specifikke for Python. De kunne sandsynligvis anvendes på andre sprog som Ruby, JavaScript eller endda PHP. Jeg ville også være interesseret i at udføre sådant arbejde, men det sker desværre ikke, medmindre nogen kan finansiere det.

Jeg vil gerne takke Python-samfundet for deres kontinuerlige investering i performance engineering. Nemlig Victor Stinner @VictorStinner, INADA Naoki @methane og Yury Selivanov @ 1st1 og hele PyPy-teamet.

For Pythons kærlighed.