NGINX-hastighedsbegrænsende i en nøddeskal

NGINX er fantastisk ... men jeg fandt, at dokumentationen om satsbegrænsning var noget ... begrænset. Så jeg har skrevet denne guide til hastighedsbegrænsning og trafikformning med NGINX.

Vi skal til:

  • beskrive NGINX-direktiverne
  • forklare NGINXs accept / afvis logik
  • hjælpe dig med at visualisere, hvordan en reel udbrud af trafik behandles ved hjælp af forskellige indstillinger: hastighedsbegrænsning, trafikpolitik og tilladelse af små udbrud

Som en bonus har jeg inkluderet en GitHub-repo og det resulterende Docker-billede, så du kan eksperimentere og reproducere testene. Det er altid lettere at lære ved at gøre!

NGINX-satsbegrænsningsdirektiver og deres roller

Dette indlæg fokuserer på ngx_http_limit_req_module, der giver dig den limit_req_zoneog limit_reqdirektiver. Det giver også limit_req_statusog limit_req_level. Sammen giver disse dig mulighed for at kontrollere HTTP-svarstatuskoden for afviste anmodninger, og hvordan disse afvisninger logges.

Mest forvirring stammer fra afvisningslogikken.

Først skal du forstå limit_reqdirektivet, som har brug for en zoneparameter, og som også indeholder valgfriburst og nodelayparametre.

Der er flere begreber i spil her:

  • zonegiver dig mulighed for at definere en spand, et delt 'rum', hvor du kan tælle de indgående anmodninger. Alle anmodninger, der kommer i den samme bucket, tælles i den samme satsgrænse. Dette er det, der giver dig mulighed for at begrænse pr. URL, pr. IP eller noget andet.
  • burster valgfri. Hvis indstillet, definerer det, hvor mange overskridende anmodninger, du kan acceptere over basisrenten. En vigtig ting at bemærke her: burst er en absolut værdi, det er ikke en rate .
  • nodelayer også valgfri og er kun nyttig, når du også angiver en burstværdi, og vi ser hvorfor nedenfor.

Hvordan beslutter NGINX, om en anmodning accepteres eller afvises?

Når du indstiller en zone, definerer du en hastighed, som 300r/mat tillade 300 anmodninger pr. Minut eller 5r/sat tillade 5 anmodninger hvert sekund.

For eksempel:

  • limit_req_zone $request_uri zone=zone1:10m rate=300r/m;
  • limit_req_zone $request_uri zone=zone2:10m rate=5r/s;

Det er vigtigt at forstå, at disse 2 zoner har de samme grænser. Den rateindstilling bruges ved Nginx at beregne en frekvens: hvad er tidsintervallet, før at acceptere en ny anmodning? NGINX vil anvende den utætte bucket-algoritme med denne token opdateringshastighed.

For NGINX 300r/mog 5r/sbehandles på samme måde: tillad en anmodning hvert 0,2 sekund for denne zone. Hvert 0,2 sekund, i dette tilfælde, sætter NGINX et flag for at huske, at det kan acceptere en anmodning. Når der kommer en anmodning, der passer ind i denne zone, sætter NGINX flag til falsk og behandler det. Hvis der kommer en anden anmodning inden timeren tikker, afvises den straks med en 503-statuskode. Hvis timeren tikker, og flaget allerede var indstillet til at acceptere en anmodning, ændres intet.

Har du brug for hastighedsbegrænsning eller trafikformning?

Indtast burstparameteren. For at forstå det, forestil dig, at det flag, vi forklarede ovenfor, ikke længere er et boolsk, men et heltal: det maksimale antal anmodninger, som NGINX kan tillade i en burst.

Dette er ikke længere en utæt skovealgoritme, men en token-skovl. De ratestyrer, hvor hurtigt timeren flåter, men det er ikke længere en sand / falsk token, men en tæller går fra 0til 1+burst value. Hver gang timeren tikker, forøges tælleren, medmindre den allerede har den maksimale værdi på b+1. Nu skal du forstå, hvorfor burstindstillingen er en værdi og ikke en sats.

Når en ny anmodning kommer ind, kontrollerer NGINX om et token er tilgængeligt (dvs. tælleren er> 0), hvis ikke, afvises anmodningen. Hvis der er et token, accepteres anmodningen og behandles, og det token vil blive fortæret (tælleren reduceres).

Ok, så NGINX accepterer anmodningen, hvis et burst-token er tilgængeligt. Men hvornår behandler NGINX denne anmodning?

Du bad NGINX om at anvende en maksimal sats på 5r/s, NGINX accepterer de overordnede anmodninger, hvis burst-tokens er tilgængelige, men venter på, at der er plads til at behandle dem inden for den maksimale satsgrænse. Derfor behandles disse burst-anmodninger med en vis forsinkelse , eller de vil timeout.

Med andre ord vil NGINX ikke gå over den satsgrænse, der er indstillet i zonedeklarationen, og vil derfor sætte de ekstra anmodninger i kø og behandle dem med en vis forsinkelse, da token-timeren krydser og færre anmodninger modtages.

For at bruge et simpelt eksempel, lad os sige, at du har en hastighed på 1r/sog en burst på 3. NGINX modtager 5 anmodninger på samme tid:

  • Den første accepteres og behandles
  • Fordi du tillader 1 + 3, er der 1 anmodning, som straks afvises med en 503-statuskode
  • De 3 andre vil blive behandlet en efter en, men ikke med det samme. De behandles med den hastighed, 1r/sat de holder sig inden for den grænse, du har angivet. Hvis der ikke kommer nogen anden anmodning, forbruger allerede denne kvote. Når køen er tom, begynder burst-tælleren at blive forøget igen (token-spanden begynder at blive fyldt igen)

Hvis du bruger NGINX som en proxy, modtager opstrøms anmodningen med en maksimal hastighed på 1r/s, og den vil ikke være opmærksom på nogen burst af indgående anmodninger, alt vil blive begrænset til den hastighed.

Du har lige lavet noget trafikformning og introduceret en vis forsinkelse for at regulere udbrud og producere en mere regelmæssig strøm uden for NGINX.

Indtast nodelay

nodelay fortæller NGINX, at de anmodninger, den accepterer i burst-vinduet, skal behandles med det samme, som almindelige anmodninger.

Som en konsekvens vil spidserne forplante sig til NGINX opstrøms, men med en vis grænse defineret af burstværdien.

Visualisering af hastighedsgrænser

Fordi jeg tror, ​​at den bedste måde at huske dette på er at opleve det på en praktisk måde, oprettede jeg et lille Docker-billede med en NGINX-konfiguration, der udsatte forskellige hastighedsbegrænsningsindstillinger for at se svarene på en grundlæggende hastighedsbegrænset placering, til en burst-aktiveret satsbegrænset placering, og til burstmed nodelaysatsbegrænset placering, lad os lege med det.

Disse prøver bruger denne enkle NGINX-konfiguration (som vi giver et Docker-billede til i slutningen af ​​dette indlæg, så du lettere kan teste dette):

limit_req_zone $request_uri zone=by_uri:10m rate=30r/m; server { listen 80; location /by-uri/burst0 { limit_req zone=by_uri; try_files $uri /index.html; } location /by-uri/burst5 { limit_req zone=by_uri burst=5; try_files $uri /index.html; } location /by-uri/burst5_nodelay { limit_req zone=by_uri burst=5 nodelay; try_files $uri /index.html; } }

Fra og med denne konfiguration sender alle eksemplerne nedenfor 10 samtidige anmodninger på én gang. Lad os se:

  • hvor mange bliver afvist af satsgrænsen?
  • hvad er behandlingshastigheden for de accepterede?

Sending 10 parallel requests to a rate-limited endpoint

That config allows 30 requests per minute. But 9 out of 10 requests are rejected in that case. If you followed the previous steps, this should make sense: The 30r/m means that a new request is allowed every 2 seconds. Here 10 requests arrive at the same time, one is allowed, the 9 other ones are seen by NGINX before the token-timer ticks, and are therefore all rejected.

But I’m OK to tolerate some burst for some client/endpoints

Ok, so let’s add the burst=5 argument to let NGINX handle small bursts for this endpoint of the rate-limited zone:

What’s going on here? As expected with the burst argument, 5 more requests are accepted, so we went from 1 /10 to 6/10 success (and the rest is rejected). But the way NGINX refreshed its token and processed the accepted requests is quite visible here: the outgoing rate is capped at 30r/m which is equivalent to 1 request every 2 seconds.

The first one is returned after 0.2 seconds. The timer ticks after 2 seconds, and one of the pending requests is processed and returned, with a total roundtrip time of 2.02 seconds. 2 seconds later, the timer ticks again, processing another pending request, which is returned with a total roundtrip time of 4.02 seconds. And so on and so forth…

The burst argument just lets you turn NGINX rate-limit from some basic threshold filter to a traffic shaping policy gateway.

My server has some extra capacity. I want to use a rate-limit to prevent it from going over this capacity.

In this case, the nodelay argument will be helpful. Let’s send the same 10 requests to a burst=5 nodelay endpoint:

As expected with the burst=5 we still have the same number of status 200 and 503. But now the outgoing rate is no longer strictly constrained to the rate of 1 requests every 2 seconds. As long as some burst tokens are available, any incoming request is accepted and processed immediately. The timer tick rate is still as important as before to control the refresh/refill rate of these burst tokens, but accepted requests no longer suffer any additional delay.

Note: in this case, the zone uses the $request_uri but all the following tests work exactly the same way for a $binary_remote_addr config which would rate-limit by client IP. You’ll be able to play with this in the Docker image.

Let’s recap

If we try to visualize how NGINX accepts the incoming requests, then processes them depending on the rate, burst, and nodelay parameter, here’s a synthetic view.

To keep things simple, we’ll show the number of incoming requests (then accepted or rejected, and processed) per time step, the value of the time step depending on the zone-defined rate limit. But the actual duration of that step doesn’t matter in the end. What is meaningful is the number of requests NGINX has to process within each of these steps.

So here is the traffic we’ll send through various rate limit settings:

Without using the burst (i.e. burst=0), we saw that NGINX acts as a pure rate-limit/traffic-policy actor. All requests are either immediately processed, if the rate timer has ticked, or immediately rejected otherwise.

Now if we want to allow the small burst to use the unused capacity under the rate-limit, we saw that adding a burst argument lets use do that, which implies some additional delay in processing the requests consuming the burst tokens:

We can see that the overall number of rejected requests is lower, and NGINX processes more requests. Only the extra requests when no burst tokens are available are rejected. In this setup, NGINX performs some real traffic-shaping.

Endelig så vi, at NGINX kan bruges til enten at udføre nogle trafikpolitikker eller til at begrænse størrelsen på burst, men stadig formerer nogle af disse bursts til forarbejdningsarbejdere (upstreams eller local), som i sidste ende genererer mindre stabil udgående hastighed, men med en bedre ventetid, hvis du kan behandle disse ekstra anmodninger:

At spille med satsgrænsen sandkasse selv

Nu kan du gå på opdagelse i koden, klone repoen, lege med Docker-billedet og få fat i det rigtigt hurtigt for bedre at styrke din forståelse af disse koncepter. //github.com/sportebois/nginx-rate-limit-sandbox

Opdatering (14. juni 2017)

NGINX offentliggjorde for få dage siden deres egen detaljerede forklaring af deres hastighedsbegrænsende mekanisme. Du kan nu lære mere om det i deres Rate Limiting med NGINX og NGINX Plus blogindlæg.