en pt nl nl nl en nl nl nl nl en nl nl nl nl en nl nl nl en nl nl fr nl nl nl en nl nl nl nl nl nl nl nl de nl fr nl en fr nl nl nl nl nl nl nl nl nl nl en nl nl nl nl nl en es nl nl en nl nl nl nl hy nl nl nl nl en nl nl nl nl nl uk nl uk nl it nl nl es nl nl ca nl
Een tijdje terug liep ik tegen het probleem aan dat een bepaalde API endpoint van een applicatie duizenden keren per minuut wordt aangeroepen en de PHP de hoeveelheid requests niet meer aankon.
De specifieke endpoint geeft dynamische resultaten terug die gebaseerd is op URL, tijd, datum, of eventuele andere context. Hoewel veel requests uniek is zal toch de meerderheid van de 'unieke' data dezelfde response terugsturen. Om CPU te besparen van de PHP server, heb ik gekeken naar de mogelijkheden om zo veel mogelijk processen intern te cachen.
In deze tutorial ga ik meer vertellen over mijn oplossing die ik gevonden heb.
Let wel op: We hebben het hier over het gebruiken van 'publieke' cache. Lees onderaan deze tutorial hoe je de publieke cache beter kan onderscheiden van andere gebruikers van bijvoorbeeld je login systeem.
Mocht je nog niet eerder gehoord hebben van NGINX, dan is het goed om te weten dat dit een webserver is die je bezoekers ontvangt, net zoals Apache. Persoonlijk prefereer ik al geruime tijd de webserver NGINX boven Apache, dat komt vooral door de verhoogde prestaties en het efficiënte RAM gebruik waardoor je meer bezoekers kan ontvangen,
Cachen kan op veel verschillende niveaus, een van de populaire manier om data te cachen is op applicatie niveau door middel van Redis of Memcached. Dat zijn razendsnelle databases die data voor je tijdelijk opslaan aan de hand van een key. Meestal verlopen die keys na verloop van tijd waarna de applicatie automatisch nieuwe data ophaalt uit je productie database en het resultaat weer tijdelijk opslaat in je Redis database.
Op deze manier voorkom je dat er bij elke rekest 'verse' data uit je database gehaald moet worden.
Cachen op NGINX niveau gaat iets anders in z'n werk, maar het idee is hetzelfde.
In dit geval zal de NGINX kijken of de data nog vers genoeg is. Indien dit het geval is zal jouw applicatie de rekest niet ontvangen en komt deze niet verder dan NGINX-laag die om je applicatie zit. Dit komt omdat NGINX de vorige response naar die URL al eens eerder heeft gecached en tijdelijk opslaat. Hierdoor blijft de CPU van je applicatie bespaard.
In mijn geval heb ik binnen één NGINX server verschillende sublagen. Er is één laag die al het verkeer ontvangt en doorstuurt naar de laag eronder. Deze laag praat met de betreffende software die je geschreven hebt, dit kan alles zijn, zoals (maar niet gelimiteerd tot) bijvoorbeeld Symfony, Laravel, een NodeJS applicatie zijn.
In jouw geval is een tweede laag misschien niet nodig.
NGINX biedt mogelijkheid om een cache-zone aan te maken die data tijdelijk opslaat. Dit betekent dat je op één webserver verschillende URLs en data anders kan behandelen. In dit voorbeeld maken we gebruik van één proxy cache.
De eerste stap is dan ook om naar je NGINX default.conf configuratie bestand te gaan. In de meeste gevallen vind je deze in /etc/nginx/sites-available/default.conf.
Voeg bovenin het bestand de volgende regel toe:
proxy_cache_path /var/tmp/nginx levels=1:2 keys_zone=zone_api_cache:1m inactive=1h;
Hierbij is het handig om het volgende te weten over de bovenstaande lijn:
proxy_cache_path is een caching module binnen NGINX die we aanspreken
/var/tmp/nginx is het pad waar NGINX tijdelijk de cache resultaten opslaat als bestanden
levels=1:2 defineert de maximale mappenhiërarchie, de hoeveelheid mappen die NGINX gebruikt om de responses op slaan
keys_zone=zone_api_cache is de caching key. Deze gebruik je in je NGINX server om aan te geven van welke cache die gebruik moet maken
:1m is de hoeveelheid bytes die maximaal opgeslagen wordt. In dit geval is het 1MB
inactive=1h geeft de maximale tijd aan dat een response gecached mag worden. Gecachte data die niet wordt aangeroepen binnen die periode zal (ongeacht de versheid) worden verwijderd. Dit kan in minuten en uren (de default is 10 minuten)
Er zijn veel meer configuraties mogelijk, deze kan je teruglezen in deze documentatie van NGINX.
De NGINX server instellen
In dit voorbeeld definieer ik twee lagen, dit verschilt per situatie.
In dit voorbeeld maken we gebruik van de NGINX proxy die leid naar je applicatie. Het IP 127.0.0.1 is voor lokaal server verkeer, ook wel bekend onder localhost. 8081 is de poort naar de interne NGINX server. De poort 8081 kan je ook aanpassen naar de poort van je applicatie of een andere webserver, zoals een NodeJS of Ruby applicatie.
De proxy cache instellen
In het kort een aantal belangrijke properties:
proxy_cache: verwacht de key van de zone die je eerder hebt ingesteld
proxy_cache_methods: verwacht de rekest methodes die gecached moeten worden, zoals GET, POST, PUT, PATCH of DELETE.
Wanneer je een methode niet definieert zal de rekest niet gecached worden en verkrijgt het altijd verse data van je applicatie
proxy_cache_key: verwacht een 'unieke' tekst die NGINX zal gebruiken om te hashen, dit herleid naar het gecachte bestand op je server (mocht deze bestaan en overeenkomen). De tekst wordt volgens de documentatie omgezet naar MD5 voor de bestandsnaam.
proxy_cache_valid: verwacht de response status codes die gecached moeten worden. Alle response codes die niet in de lijst voorkomen zullen niet gecached worden. De volgende rekest krijgt dan opnieuw verse data. Hierin geef je daarnaast aan hoe lang de data moet worden gecached, in mijn geval 1m (1 minuut)
Deze property kan je meerdere keren gebruiken, dit houd in dat verschillende response status codes andere cache-valid lengtes kan krijgen.
proxy_cache_use_stale: Bepaalt in welke gevallen een verouderd, in de cache opgeslagen antwoord kan worden gebruikt tijdens communicatie met de proxyserver.
Soms kan het handig zijn om te zien hoe vers de data van je response is en hoe de server de rekest heeft behandeld. NGINX heeft veel parameters waar je gebruik van kan maken, bijvoorbeeld de $upstream_cache_status parameter. Deze zal een HIT, BYPASS, EXPIRED of MISS status tekst toevoegen aan je headers ter indicatie. Voorbeeld:
De onderstaande configuratie komt te staan binnen je http blok (meestal al gedefinieerd buiten het configuratie bestand) en is voorzien van de benodigde commentaar.
# Publieke server # De schil om je interne server of applicatie server { listen 80; # Stuur alle requests door naar de proxy location / { try_files $uri @api_backend; } # Proxied requests 'gewoon' door naar je applicatie zonder te cachen # Het is belangrijk dat de proxy buffers goed ingesteld is location @api_backend { proxy_buffer_size 128k; proxy_buffers 4 256k; # Het IP 127.0.0.1 en de poort van je applicatie proxy_pass http://127.0.0.1:8081; } # Het pad (of de paden) die gecached worden # Die sturen we door naar de controller @cached_routes location ~* ^/een-api-route/(.*)$ { try_files $uri @cached_routes; } # Er kunnen meerde paden zijn die gebruik maakt van dezelfde controller location ~* ^/andere-route/(.*)$ { try_files $uri @cached_routes; } # De proxy cache manager location @cached_routes { # Het IP 127.0.0.1 en de poort van je applicatie proxy_pass http://127.0.0.1:8081; proxy_cache zone_api_cache; # De zone cache die je bij stap 1 hebt ingesteld proxy_cache_methods GET POST; # Alleen GET of POST requests cachen (in dit geval worden PATCH en DELETE requests niet gecached) proxy_cache_key "$uri"; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_cache_valid 200 402 404 500 1m; # Response cachen voor 1 minuut proxy_cache_use_stale updating; add_header X-Cached $upstream_cache_status; } } # Interne voorbeeld server # Niet publiek toegankelijk # Dit hoeft geen aparte NGINX server te zijn, dit kan ook een NodeJS of Ruby applicatie zijn server { listen 8081; # Eventuele root, error_log etc # In mijn geval staat hier de rewrites voor de PHP applicatie }
Soms kan het nodig zijn dat je niet wilt dat er iets gecached wordt, bijvoorbeeld als er een bepaalde parameter gezet is of de rekest van een specifieke host komt.
In dat geval kun je een variabele zetten binnen je blok. Ik noem deze variabele $skip_cache en gebruik het met de proxy_cache_bypass en proxy_no_cache parameters.
location @cached_routes { proxy_pass http://127.0.0.1:8081; # Ook hier # Standaard op 0, altijd cachen set $skip_cache 0; # Wanneer de host 'devnl.nl' is, cache de response dan niet if ($host = 'devnl.nl') { set $skip_cache 1; } proxy_cache zone_api_cache; proxy_cache_methods GET POST; proxy_cache_key "$uri"; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_cache_min_uses 1; proxy_cache_valid 200 402 404 500 1m; proxy_cache_use_stale updating; proxy_cache_bypass $skip_cache; # -> nieuw proxy_no_cache $skip_cache; # -> nieuw add_header X-Cached $upstream_cache_status; add_header X-Backend cached-backend; }
De no-cache pragma header
Standaard wanneer je de NGINX proxy gebruikt zal deze de no-cache header negeren. Dit is om te voorkomen dat iemand die een DOS aanval uitvoert deze header kan gebruiken en je server toch overbelast raakt omdat er niets meer gecached wordt.
Heb je een applicatie waar je soms wilt forceren om verse data te krijgen en je denkt dat dit veilig kan? Gebruik dan de volgende proxy_cache_bypass configuratie.
Cache key specifieker defineren
In mijn geval wilde ik ook dat de rekest body ook gebruikt wordt als key (de JSON die wordt meegestuurd). De reden was in mijn geval dat er één URL-pad is waar alle requests van een specifieke site heen gaan, maar mijn applicatie een stukje data uit de rekest body uitleest om tot een resultaat te komen.
Gelukkig bestaat er een proxy_cache_key, deze heb ik al eerder genoemd. Standaard is deze ingesteld op "$uri", wat inhoud dat de URL de enige unieke parameter is die de cache-key uniek maakt. Er wordt niet gekeken naar headers of de rekest body.
In het onderstaande voorbeeld verander ik de cache key naar de URL en de $rekest_body. Mocht de body van de rekest overeenkomen dan kan deze uit de cache worden gehaald. Deze methode houd wel in dat er meer unieke cachebestanden worden geschreven.
Caching met authenticatie (datalekken voorkomen)
Heb je een authenticatie laag ingebouwd? Dan moet je opletten en oppassen met de publieke cache: wanneer gebruiker 1 een URL gebruikt, zal gebruiker 2 de data te zien krijgen die gegenereerd was voor gebruiker 1.
Om dit te voorkomen is het belangrijk dat je de proxy_pass goed instelt.
Je kan met de volgende code instellen dat de cache wordt overgeslagen en ingelogde gebruikers altijd verse data terugkrijgen (mocht je de authorization header gebruiken).
Mocht je nou willen dat gebruikersdata ook gecached wordt, dan zal je moeten sleutelen met de proxy_cache_key en moet je geen proxy_cache_bypass instellen voor de authorization header.
Let op: Dit is vooralsnog geen waterdichte methode, mocht je dit verkeerd instellen, dan kan het leiden tot data-lekken.
In dit voorbeeld gebruik ik de zelfde methode als bij Cache key specifieker defineren, alleen gebruik je dan de $http_authorization header. In principe zou je authorization header uniek moeten zijn per gebruiker (mocht je een andere header gebruiken, gebruik deze dan).
Het is belangrijk dat je de cache key zo specifiek en uniek mogelijk maakt, maar wel identificeerbaar met je gebruiker.
Je zou ervoor kunnen kiezen om het IP adres van de gebruiker mee te laten wegen (de $remote_addr variable dat beschikbaar is in NGINX). Let er wel op dat je het juiste IP-adres gebruikt.
TIP: Mocht je gebruik maken van Cloudflare, dan krijg je Cloudflares proxy-IP en wordt je content minder uniek gecached.
Door gebruik te maken van deze configuratie heb ik een webserver ingesteld die in 24 uur meer dan 1 miljoen requests kan verwerken. Daarvan gaan er daadwerkelijk maar tienduizenden naar de achterliggende PHP-FPM webserver gaan 🚀
Ik hoop dat je door deze tutorial iets hebt bijgeleerd. Mocht je vragen hebben, laat het weten door een topic aan te maken en je vraag te stellen.
Jasper Door gebruik te maken van deze configuratie heb ik een webserver ingesteld die in 24 uur meer dan 1 miljoen requests kan verwerken. Daarvan gaan er daadwerkelijk maar tienduizenden naar de achterliggende PHP-FPM webserver gaan 🚀