...

Threadpool-optimalisatie voor webservers: Apache versus NGINX en LiteSpeed in vergelijking

Dit artikel laat zien hoe de thread pool webserver Configuratie bij Apache, NGINX en LiteSpeed Paralleliteit, latentie en geheugenbehoefte regelen. Ik leg uit welke instellingen onder belasting belangrijk zijn en waar zelfafstemming voldoende is – met duidelijke verschillen in het aantal verzoeken per seconde.

Centrale punten

  • Architectuur: Processen/threads (Apache) versus gebeurtenissen (NGINX/LiteSpeed)
  • Zelfafstelling: Automatische aanpassing vermindert latentie en onderbrekingen
  • Bronnen: CPU-kernen en RAM bepalen zinvolle threadgroottes
  • Werkbelasting: I/O-intensief vereist meer threads, CPU-intensief minder
  • Afstemmen: Kleine, gerichte parameters hebben een groter effect dan forfaitaire waarden.

Vergelijking van threadpool-architecturen

Ik begin met de Architectuur, omdat het de grenzen van de tuningruimte definieert. Apache maakt gebruik van processen of threads per verbinding; dat kost meer RAM en verhoogt de latentie tijdens piekuren [1]. NGINX en LiteSpeed volgen een gebeurtenisgestuurd model, waarbij een klein aantal workers veel verbindingen multiplexen – dat bespaart contextwisselingen en verlaagt de overhead [1]. In tests verwerkte NGINX 6.025,3 verzoeken/s, Apache haalde in hetzelfde scenario 826,5 verzoeken/s en LiteSpeed kwam met 69.618,5 verzoeken/s als beste uit de bus [1]. Wie dieper in de architectuurvergelijking wil duiken, vindt meer kerngegevens op Apache versus NGINX, die ik gebruik voor een eerste classificatie.

Het is ook belangrijk hoe elke engine omgaat met blokkerende taken. NGINX en LiteSpeed ontkoppelen de event-loop van het bestandssysteem of upstream-I/O via asynchrone interfaces en beperkte hulpthreads. Apache koppelt in het klassieke model één thread/proces per verbinding; met MPM event kan Keep-Alive worden ontlast, maar de geheugenvoetafdruk per verbinding blijft hoger. In de praktijk betekent dit: hoe meer gelijktijdige trage clients of grote uploads, hoe meer het gebeurtenismodel zich terugbetaalt.

Hoe zelfafstemming echt werkt

Moderne servers controleren de Discussie-Aantal vaak automatisch. De controller controleert in korte cycli de belasting, vergelijkt actuele met historische waarden en schaalt de poolgrootte op of neer [2]. Als een wachtrij vastloopt, verkort het algoritme zijn cyclus en voegt het extra threads toe totdat de verwerking weer stabiel verloopt [2]. Dit bespaart ingrepen, voorkomt overallocatie en vermindert de kans op head-of-line-blokkades. Als referentie gebruik ik het gedocumenteerde gedrag van een self-tuning-controller in Open Liberty, dat de mechanica duidelijk beschrijft [2].

Ik let daarbij op drie factoren: een Hysterese tegen flapping (geen onmiddellijke reactie op elke spike), een harde bovengrens tegen RAM-overflows en een minimale grootte, zodat er niet bij elke burst opwarmkosten worden gemaakt. Het is ook zinvol om een aparte streefwaarde in te stellen voor actief Threads (coreThreads) versus maximale threads (maxThreads). Zo blijft de pool actief zonder dat er inactieve bronnen worden geblokkeerd [2]. In gedeelde omgevingen beperk ik de uitbreidingssnelheid, zodat de webserver niet agressief CPU-slots claimt ten opzichte van naburige diensten [4].

Kerncijfers uit benchmarks

Reële waarden helpen bij Beslissingen. In burst-scenario's scoort NGINX met een zeer lage latentie en hoge stabiliteit [3]. Bij extreme parallelliteit levert Lighttpd in tests het hoogste aantal verzoeken per seconde, terwijl OpenLiteSpeed en LiteSpeed op de voet volgen [3]. Grote bestandsoverdrachten slagen NGINX met tot 123,26 MB/s, OpenLiteSpeed ligt net achter, wat de efficiëntie van de gebeurtenisgestuurde architectuur onderstreept [3]. Ik gebruik dergelijke kengetallen om te beoordelen waar threadaanpassingen echt nut hebben en waar beperkingen voortkomen uit de architectuur.

Server Model/threads Voorbeeldtarief kernboodschap
Apache Proces/thread per verbinding 826,5 verzoeken/s [1] Flexibel, maar hogere RAM-vereisten
NGINX Gebeurtenis + weinig werknemers 6.025,3 verzoeken/s [1] Laag Latency, zuinig
LiteSpeed Gebeurtenis + LSAPI 69.618,5 verzoeken/s [1] Zeer snel, GUI-tuning
Lighttpd Gebeurtenis + Asynchroon 28.308 verzoeken/s (hoog parallel) [3] Schaalt in Tips zeer goed

De tabel toont relatieve Voordelen, geen vaste toezeggingen. Ik beoordeel ze altijd in de context van mijn eigen workloads: korte dynamische antwoorden, veel kleine statische bestanden of grote streams. Afwijkingen kunnen het gevolg zijn van het netwerk, de opslag, TLS-offloading of de PHP-configuratie. Daarom correleer ik statistieken zoals CPU-steal, run-queue-lengte en RSS per worker met het aantal threads. Alleen deze benadering maakt het mogelijk om echte thread-bottlenecks te onderscheiden van I/O- of applicatiebeperkingen.

Voor betrouwbare cijfers gebruik ik ramp-up-fasen en vergelijk ik p50/p95/p99-latenties. Een steile p99-curve bij constante p50-waarden duidt eerder op wachtrijen dan op pure CPU-verzadiging. Open (RPS-gestuurde) in plaats van gesloten (alleen concurrency-gestuurde) belastingsprofielen laten bovendien beter zien waar het systeem begint met het actief afwijzen van verzoeken. Zo kan ik het punt definiëren waarop thread-verhogingen niets meer opleveren en backpressure of rate-limits zinvoller zijn.

Praktijk: Werknemers en verbindingen dimensioneren

Ik begin met de CPU-Kernen: worker_processes of LSWS-workers mogen kernen niet overtreffen, anders neemt de contextwisseling toe. Voor NGINX pas ik worker_connections zo aan dat de som van verbindingen en bestandsdescriptoren onder de ulimit-n blijft. Bij Apache vermijd ik te hoge MaxRequestWorkers, omdat de RSS per kind snel het RAM-geheugen opslokt. Onder LiteSpeed houd ik PHP-procespools en HTTP-workers in evenwicht, zodat PHP geen bottleneck wordt. Wie de snelheidsverschillen tussen engines wil begrijpen, heeft baat bij de vergelijking. LiteSpeed vs. Apache, die ik als tuningachtergrond gebruik.

Een eenvoudige vuistregel: ik bereken eerst het FD-budget (ulimit-n minus reserve voor logs, upstreams en bestanden), deel dit door het geplande aantal gelijktijdige verbindingen per worker en controleer of het totaal voldoende is voor HTTP + upstream + TLS-buffer. Vervolgens dimensioner ik de backlog-wachtrij gematigd – groot genoeg voor pieken, klein genoeg om overbelasting niet te verbergen. Ten slotte stel ik de keep-alive-waarden zo in dat ze passen bij de verzoekpatronen: korte pagina's met veel assets profiteren van langere time-outs, API-verkeer met weinig verzoeken per verbinding eerder van lagere waarden.

LiteSpeed-fijnafstemming voor hoge belasting

Bij LiteSpeed vertrouw ik op LSAPI, omdat het contextwisselingen minimaliseert. Zodra ik merk dat CHILD-processen uitgeput zijn, verhoog ik LSAPI_CHILDREN stapsgewijs van 10 naar 40, indien nodig tot 100 – telkens vergezeld van CPU- en RAM-controles [6]. De GUI vergemakkelijkt het aanmaken van listeners, het vrijgeven van poorten, het doorsturen en het inlezen van .htaccess, wat wijzigingen versnelt [1]. Onder continue belasting test ik het effect van kleine stappen in plaats van grote sprongen om latentiepieken vroegtijdig te detecteren. In gedeelde omgevingen verlaag ik coreThreads wanneer andere services CPU gebruiken, zodat de Self-Tuner niet te veel actieve threads vasthoudt [2][4].

Daarnaast houd ik Keep-Alive per listener en het gebruik van HTTP/2/HTTP/3 in de gaten: multiplexing vermindert het aantal verbindingen, maar verhoogt de geheugenbehoefte per socket. Ik houd daarom de verzendbuffer conservatief en activeer compressie alleen waar het nettowinst duidelijk is (veel tekstuele antwoorden, nauwelijks CPU-limiet). Voor grote statische bestanden vertrouw ik op zero-copy-mechanismen en beperk ik gelijktijdige downloadslots, zodat PHP-workers niet vastlopen wanneer er pieken in het verkeer optreden.

NGINX: efficiënt gebruikmaken van het gebeurtenismodel

Voor NGINX stel ik worker_processes in op auto of het kerncijfer. Met epoll/kqueue, actieve accept_mutex en aangepaste backlog-waarden houd ik verbindingsacceptaties gelijkmatig. Ik zorg ervoor dat keepalive_requests en keepalive_timeout zo worden ingesteld dat inactieve sockets de FD-pool niet verstoppen. Grote statische bestanden verplaats ik met sendfile, tcp_nopush en een passende output_buffers. Ik gebruik alleen rate limiting en verbindingslimieten als bots of bursts de threadpool indirect belasten, omdat elke beperking extra statusbeheer met zich meebrengt.

In proxy-scenario's is Upstream-keepalive Cruciaal: te laag veroorzaakt vertraging bij het opzetten van verbindingen, te hoog blokkeert FD's. Ik kies waarden die passen bij de backendcapaciteit en houd time-outs voor connect/read/send duidelijk gescheiden, zodat defecte backends de event loops niet blokkeren. Met reuseport en optionele CPU-affiniteit verdeel ik de belasting gelijkmatiger over de cores, zolang de IRQ-/RSS-instellingen van de NIC dit ondersteunen. Voor HTTP/2/3 kalibreer ik header- en flowcontrol-limieten zorgvuldig, zodat afzonderlijke grote streams niet de hele verbinding domineren.

Apache: MPM event correct instellen

Bij Apache gebruik ik evenement in plaats van prefork, zodat Keep-Alive-sessies niet permanent workers binden. Ik stel MinSpareThreads en MaxRequestWorkers zo in dat de run-queue per kern onder 1 blijft. Ik houd de ThreadStackSize klein, zodat er meer workers in het beschikbare RAM passen; deze mag niet te klein worden, anders loop je het risico op stack-overflows in modules. Met een gematigde KeepAlive-time-out en beperkte KeepAliveRequests voorkom ik dat een klein aantal clients veel threads blokkeert. Ik verplaats PHP naar PHP-FPM of LSAPI, zodat de webserver zelf licht blijft.

Ik let ook op de verhouding tussen ServerLimit, ThreadsPerChild en MaxRequestWorkers: deze drie bepalen samen hoeveel threads er daadwerkelijk kunnen worden aangemaakt. Voor HTTP/2 gebruik ik MPM event met gematigde streamlimieten; te hoge waarden verhogen het RAM-verbruik en de scheduler-kosten. Modules met grote globale caches laad ik alleen als ze nodig zijn, omdat de voordelen van copy-on-write verdwijnen zodra processen lang draaien en het geheugen veranderen.

RAM en threads: geheugen correct berekenen

Ik tel de RSS per worker/child maal het geplande maximum aantal en tel daar de kernelbuffer en caches bij op. Als er geen buffer overblijft, verminder ik het aantal threads of verhoog ik nooit de swap, omdat swapping de latentie doet exploderen. Voor PHP-FPM of LSAPI bereken ik bovendien de gemiddelde PHP-RSS, zodat de som van webserver en SAPI stabiel blijft. Ik houd rekening met TLS-terminatiekosten, omdat certificaathandshakes en grote outbound-buffers het verbruik verhogen. Pas als het RAM-budget klopt, draai ik de thread-schroeven verder aan.

Bij HTTP/2/3 houd ik rekening met extra header-/flowcontrolstatussen per verbinding. GZIP/Brotli buffert gecomprimeerde en ongecomprimeerde gegevens tegelijkertijd; dat kan per verzoek enkele honderden KB extra betekenen. Ik houd ook rekening met reserves voor logbestanden en tijdelijke bestanden. Bij Apache verhogen kleinere ThreadStackSize-waarden de dichtheid, bij NGINX en LiteSpeed zijn vooral het aantal parallelle sockets en de grootte van de verzend-/ontvangstbuffers van invloed. Door alle componenten vóór het afstemmen bij elkaar op te tellen, voorkom je later onaangename verrassingen.

Wanneer ik handmatig ingrijp

Ik vertrouw op Zelfafstelling, totdat statistieken het tegendeel aantonen. Als ik de machine deel in shared hosting, rem ik coreThreads of MaxThreads af, zodat andere processen voldoende CPU-tijd behouden [2][4]. Als er een harde threadlimiet per proces bestaat, stel ik maxThreads conservatief in om OS-fouten te voorkomen [2]. Als er deadlock-achtige patronen optreden, verhoog ik alleen tijdelijk de poolgrootte, observeer ik de wachtrijen en verlaag ik deze daarna weer. Wie typische patronen met meetwaarden wil vergelijken, vindt aanwijzingen in de Webserver snelheidsvergelijking, die ik graag gebruik als plausibiliteitscontrole.

Als interventiesignalen gebruik ik vooral: aanhoudende p99-pieken ondanks lage CPU-belasting, stijgende socketwachtrijen, sterk groeiende TIME_WAIT-cijfers of een plotselinge stijging van open FD's. In dergelijke gevallen beperk ik eerst de aannames (verbindings-/snelheidslimieten), koppel ik backends los met time-outs en verhoog ik pas daarna voorzichtig de threads. Zo voorkom ik dat ik de overbelasting alleen maar naar binnen verplaats en de latentie voor iedereen verslechter.

Typische fouten en snelle controles

Ik kijk vaak naar hoog Keep-Alive-time-outs die threads binden, hoewel er geen gegevens worden verzonden. Ook veelvoorkomend: MaxRequestWorkers ver boven het RAM-budget en ulimit-n te laag voor de beoogde parallelliteit. In NGINX onderschatten velen het FD-gebruik door upstream-verbindingen; elke backend telt dubbel. In LiteSpeed groeien PHP-pools sneller dan HTTP-workers, waardoor verzoeken wel worden geaccepteerd, maar te laat worden afgehandeld. Met korte belastingstests, heap/RSS-vergelijking en een blik op de run-queue vind ik deze patronen binnen enkele minuten.

Ook vaak voorkomend: syn-backlog te klein, waardoor verbindingen al vóór de webserver worden geweigerd; toegangslogs zonder buffer, die synchroon naar trage opslag schrijven; debug-/trace-logs die per ongeluk actief blijven en CPU-capaciteit in beslag nemen. Bij de overstap naar HTTP/2/3 verhogen te royale streamlimieten en headerbuffers het geheugengebruik per verbinding – vooral zichtbaar wanneer veel clients weinig gegevens overdragen. Ik controleer daarom de verdeling van korte versus lange antwoorden en pas de limieten dienovereenkomstig aan.

HTTP/2 en HTTP/3: wat ze betekenen voor threadpools

Multiplexing vermindert het aantal TCP-verbindingen per client aanzienlijk. Dat is goed voor FD's en acceptkosten, maar verplaatst de druk naar per-connection-states. Daarom stel ik voor HTTP/2 voorzichtige limieten in voor gelijktijdige streams en kalibreer ik flowcontrol, zodat afzonderlijke grote downloads de verbinding niet domineren. Bij HTTP/3 zijn er geen TCP-gerelateerde head-of-line-blokkades meer, maar neemt de CPU-belasting per pakket toe. Ik compenseer dit met voldoende werkcapaciteit en kleine buffers, zodat de latentie laag blijft. In alle gevallen geldt: liever minder, goed gebruikte verbindingen met zinvolle keep-alive-waarden dan te lange inactieve sessies die threads en geheugen in beslag nemen.

Platformfactoren: kernel, containers en NUMA

Bij virtualisatie let ik op CPU-steal en cgroups-limieten: als de hypervisor kernen steelt of de container slechts over deelkernen beschikt, kan worker_processes=auto te optimistisch zijn. Indien nodig pin ik workers aan echte kernen en pas ik het aantal aan het effectief beschikbaar budget. Op NUMA-hosts profiteren webservers van lokale geheugentoewijzing; ik vermijd onnodige cross-node-toegang door workers per socket te bundelen. Ik laat Transparent Huge Pages vaak uitgeschakeld voor latentiegevoelige workloads om pieken in page faults te voorkomen.

Op OS-niveau controleer ik de grenzen van de bestandsdescriptoren, de verbindingsbacklogs en het poortbereik voor uitgaande verbindingen. Ik verhoog alleen wat ik echt nodig heb, test het gedrag bij rollover en houd me strikt aan de veiligheidslimieten. Aan de netwerkzijde zorg ik ervoor dat de RSS/IRQ-verdeling en MTU-instellingen passen bij het verkeersprofiel – anders heeft het afstemmen van de webserver geen zin, omdat pakketten te langzaam aankomen of vast komen te zitten in de NIC-wachtrij.

Meten in plaats van gissen: praktische handleiding voor tests

Ik voer belastingtests uit in drie fasen: warm-up (caches, JIT, TLS-sessies), plateau (stabiele RPS/concurrency) en burst (korte pieken). Afzonderlijke profielen voor statische bestanden, API-aanroepen en dynamische pagina's helpen om geïsoleerd te zien waar threads, I/O of backends beperkingen opleggen. Ik noteer parallel FD-cijfers, run-queues, contextwisselingen, RSS per proces en p50/p95/p99-latenties. Als doel kies ik werkpunten bij 70-85 %-belasting – voldoende buffer voor reële schommelingen, zonder permanent in het verzadigingsgebied te lopen.

Beslissingsgids in het kort

Ik kies voor NGINX, wanneer lage latentie, zuinige bronnen en flexibele .conf-afstemmingsmogelijkheden belangrijk zijn. Ik kies voor LiteSpeed wanneer PHP-belasting domineert, de GUI de bediening moet vereenvoudigen en LSAPI de knelpunten vermindert. Ik kies voor Apache wanneer ik afhankelijk ben van modules en .htaccess en de MPM-event-configuratie goed onder controle heb. De zelfafstemmingsmechanismen zijn in veel gevallen voldoende; ik hoef alleen in te grijpen als de statistieken wijzen op vertragingen, harde limieten of RAM-druk [2]. Met realistische kern- en RAM-budgetten, kleine stapgroottes en observatie van de latentiecurves brengt thread-tuning me betrouwbaar naar mijn doel.

Huidige artikelen