...

Optimering af trådpulje til webservere: Apache vs. NGINX og LiteSpeed i sammenligning

Denne artikel viser, hvordan trådpulje webserver Konfiguration i Apache, NGINX og LiteSpeed Parallelitets-, latenstid- og hukommelsesbehov. Jeg forklarer, hvilke indstillinger der tæller under belastning, og hvor selvjustering er tilstrækkelig – med klare forskelle i forespørgsler pr. sekund.

Centrale punkter

  • Arkitektur: Processer/tråde (Apache) vs. begivenheder (NGINX/LiteSpeed)
  • Selvjustering: Automatisk tilpasning reducerer latenstid og stop
  • Ressourcer: CPU-kerner og RAM bestemmer fornuftige trådstørrelser
  • Arbejdsbyrde: I/O-tung kræver flere tråde, CPU-tung kræver færre
  • Indstilling: Små, målrettede parametre har større effekt end faste værdier.

Sammenligning af thread-pool-arkitekturer

Jeg begynder med Arkitektur, fordi det definerer grænserne for tuningrummet. Apache bruger processer eller tråde pr. forbindelse, hvilket koster mere RAM og øger latenstiden i spidsbelastningsperioder [1]. NGINX og LiteSpeed følger en begivenhedsstyret model, hvor få arbejdere multiplexer mange forbindelser – det sparer kontekstskift og reducerer overhead [1]. I tests behandlede NGINX 6.025,3 anmodninger/sekund, Apache nåede op på 826,5 anmodninger/sekund i samme scenario, og LiteSpeed tog føringen med 69.618,5 anmodninger/sekund [1]. Hvis du vil dykke dybere ned i arkitektur sammenligningen, finder du flere nøgletal under Apache vs NGINX, som jeg bruger til en første klassificering.

Det er også vigtigt, hvordan hver motor håndterer blokerende opgaver. NGINX og LiteSpeed adskiller event-loop fra filsystemet eller upstream-I/O via asynkrone grænseflader og begrænsede hjælpetråde. Apache binder en tråd/proces pr. forbindelse i det klassiske model; med MPM event kan Keep-Alive aflastes, men hukommelsesforbruget pr. forbindelse forbliver højere. I praksis betyder det, at jo flere samtidige langsomme klienter eller store uploads, jo mere betaler begivenhedsmodellen sig.

Sådan fungerer selvtilpasning i praksis

Moderne servere kontrollerer Tråd-antal ofte automatisk. Controlleren kontrollerer belastningen i korte cyklusser, sammenligner aktuelle værdier med historiske værdier og skalerer poolstørrelsen op eller ned [2]. Hvis en kø hænger, forkorter algoritmen sin cyklus og tilføjer ekstra tråde, indtil behandlingen igen kører stabilt [2]. Dette sparer indgreb, forhindrer overallokering og reducerer sandsynligheden for head-of-line-blokeringer. Som reference bruger jeg den dokumenterede adfærd af en selvjusterende controller i Open Liberty, der beskriver mekanikken tydeligt [2].

Jeg er opmærksom på tre faktorer: en Hysterese mod flapping (ingen øjeblikkelig reaktion på hver spike), en hård øvre grænse mod RAM-overløb og en Minimumstørrelse, så der ikke opstår opvarmningsomkostninger ved hver burst. Det er også fornuftigt at have en separat målværdi for aktiv Tråde (coreThreads) vs. maksimale tråde (maxThreads). På den måde forbliver puljen aktiv uden at binde ressourcer i tomgang [2]. I delte miljøer begrænser jeg ekspansionshastigheden, så webserveren ikke aggressivt optager CPU-slots i forhold til nabotjenester [4].

Nøgletal fra benchmarks

Reelle værdier hjælper med Beslutninger. I burst-scenarier scorer NGINX med meget lav latenstid og høj stabilitet [3]. Ved ekstrem parallelitet leverer Lighttpd i tests det højeste antal forespørgsler pr. sekund, mens OpenLiteSpeed og LiteSpeed følger tæt efter [3]. NGINX klarer store filoverførsler med op til 123,26 MB/s, OpenLiteSpeed ligger tæt bagved, hvilket understreger effektiviteten af den begivenhedsstyrede arkitektur [3]. Jeg bruger sådanne nøgletal til at vurdere, hvor trådtilpasninger virkelig giver fordele, og hvor begrænsningerne stammer fra arkitekturen.

Server Model/tråde Eksempel på sats kernebudskab
Apache Process/tråd pr. forbindelse 826,5 anmodninger/sekund [1] Fleksibel, men højere RAM-behov
NGINX Begivenhed + få arbejdere 6.025,3 anmodninger/sekund [1] Lav Forsinkelse, sparsommelig
LiteSpeed Begivenhed + LSAPI 69.618,5 anmodninger/sekund [1] Meget hurtigt, GUI-tuning
Lighttpd Begivenhed + Asynkron 28.308 anmodninger/sekund (høj parallelitet) [3] Skaleres i Tips meget god

Tabellen viser relative Fordele, ingen faste løfter. Jeg vurderer dem altid i sammenhæng med mine egne arbejdsbelastninger: korte dynamiske svar, mange små statiske filer eller store streams. Afvigelser kan stamme fra netværk, lagerplads, TLS-offloading eller PHP-konfiguration. Derfor korrelerer jeg målinger som CPU-steal, run-queue-længde og RSS pr. worker med antallet af tråde. Først denne betragtning adskiller reelle trådflaskehalse fra I/O- eller applikationsgrænser.

For at få pålidelige tal bruger jeg ramp-up-faser og sammenligner p50/p95/p99-latenser. En stejl p99-kurve ved konstante p50-værdier tyder det snarere på ventekøer end på ren CPU-mætning. Åbne (RPS-styrede) i stedet for lukkede (kun konkurrencestyrrede) belastningsprofiler viser desuden bedre, hvor systemet begynder at afvise forespørgsler aktivt. Således kan jeg definere det punkt, hvor trådforøgelser ikke længere nytter noget, og hvor backpressure eller hastighedsbegrænsninger er mere hensigtsmæssige.

Praksis: Dimensionering af arbejdere og forbindelser

Jeg begynder med CPU-Kerner: worker_processes eller LSWS-Worker må ikke overstige kerner, ellers øges kontekstskiftet. For NGINX tilpasser jeg worker_connections, så summen af forbindelser og filbeskrivere forbliver under ulimit-n. For Apache undgår jeg for høje MaxRequestWorkers, fordi RSS pr. barn hurtigt spiser RAM. Under LiteSpeed holder jeg PHP-procespuljer og HTTP-arbejdere i balance, så PHP ikke bliver en flaskehals. Hvis du vil forstå hastighedsforskellene mellem motorer, kan du drage fordel af sammenligningen LiteSpeed vs. Apache, som jeg bruger som tuningbaggrund.

En simpel tommelfingerregel: Først beregner jeg FD-budgettet (ulimit-n minus reserve til logfiler, upstreams og filer), deler det med det planlagte antal samtidige forbindelser pr. worker og kontrollerer, om summen er tilstrækkelig til HTTP + upstream + TLS-buffer. Derefter dimensionerer jeg backlog-køen moderat – stor nok til bursts, lille nok til ikke at skjule overbelastning. Til sidst indstiller jeg Keep-Alive-værdierne, så de passer til forespørgselsmønstrene: korte sider med mange aktiver drager fordel af længere timeouts, API-trafik med få forespørgsler pr. forbindelse drager snarere fordel af lavere værdier.

LiteSpeed-finjustering til høj belastning

Med LiteSpeed satser jeg på LSAPI, fordi det minimerer kontekstskift. Så snart jeg bemærker, at CHILD-processer er udtømte, øger jeg LSAPI_CHILDREN gradvist fra 10 til 40, om nødvendigt op til 100 – altid ledsaget af CPU- og RAM-kontroller [6]. GUI'en gør det lettere for mig at oprette lyttere, frigive porte, videresende og indlæse .htaccess, hvilket fremskynder ændringer [1]. Under vedvarende belastning tester jeg effekten af små skridt i stedet for store spring for at opdage latenstoppe tidligt. I delte miljøer sænker jeg coreThreads, når andre tjenester belaster CPU'en, så selvtuner ikke holder for mange aktive tråde [2][4].

Derudover overvåger jeg Keep-Alive pr. lytter og brugen af HTTP/2/HTTP/3: Multiplexing reducerer antallet af forbindelser, men øger hukommelsesbehovet pr. socket. Jeg holder derfor sendebufferen konservativ og aktiverer kun komprimering, hvor nettogevinsten er klar (mange tekstbaserede svar, næsten ingen CPU-begrænsning). For store statiske filer stoler jeg på zero-copy-mekanismer og begrænser samtidige download-slots, så PHP-workere ikke sulter, når der opstår trafikspidser.

NGINX: Effektiv brug af hændelsesmodellen

For NGINX indstiller jeg worker_processes til bil eller kernetal. Med epoll/kqueue, aktiv accept_mutex og tilpassede backlog-værdier holder jeg forbindelsesaccept ensartet. Jeg sørger for at indstille keepalive_requests og keepalive_timeout, så inaktive sockets ikke tilstopper FD-puljen. Store statiske filer flytter jeg med sendfile, tcp_nopush og en passende output_buffers. Jeg bruger kun hastighedsbegrænsning og forbindelsesbegrænsninger, hvis bots eller bursts indirekte belaster trådpuljen, fordi hver begrænsning skaber ekstra statusadministration.

I proxy-scenarier er Upstream-Keepalive Afgørende: for lav værdi skaber forsinkelser i oprettelsen af forbindelsen, for høj værdi blokerer FD'er. Jeg vælger værdier, der passer til backend-kapaciteten, og holder timeouts for connect/read/send klart adskilt, så defekte backends ikke binder event-loops. Med reuseport og valgfri CPU-affinitet fordeler jeg belastningen mere jævnt over kernerne, så længe NIC'ens IRQ-/RSS-indstillinger understøtter dette. For HTTP/2/3 kalibrerer jeg header- og flow-control-grænser forsigtigt, så enkelte store streams ikke dominerer hele forbindelsen.

Apache: Indstil MPM event korrekt

I Apache bruger jeg begivenhed i stedet for prefork, så Keep-Alive-sessioner ikke binder arbejdere permanent. MinSpareThreads og MaxRequestWorkers indstiller jeg, så run-queue pr. kerne forbliver under 1. Jeg holder ThreadStackSize lille, så flere arbejdere kan være i den tilgængelige RAM; den må ikke blive for lille, ellers risikerer du stack-overflows i moduler. Med moderat KeepAlive-timeout og begrænsede KeepAliveRequests forhindrer jeg, at få klienter blokerer mange tråde. Jeg flytter PHP til PHP-FPM eller LSAPI, så webserveren selv forbliver let.

Jeg er også opmærksom på forholdet mellem ServerLimit, ThreadsPerChild og MaxRequestWorkers: Disse tre bestemmer tilsammen, hvor mange tråde der reelt kan opstå. Til HTTP/2 bruger jeg MPM event med moderate stream-grænser; for høje værdier øger RAM-forbruget og scheduler-omkostningerne. Moduler med store globale caches indlæser jeg kun, når de er nødvendige, da Copy-on-Write-fordelene forsvinder, så snart processer kører i lang tid og ændrer hukommelsen.

RAM og tråde: Beregn hukommelsen korrekt

Jeg tæller RSS pr. worker/child gange det planlagte maksimale antal og tilføjer kernelbuffer samt caches. Hvis der ikke er nogen buffer tilbage, reducerer jeg tråde eller øger aldrig swappen, fordi swapping får latenstiden til at eksplodere. For PHP-FPM eller LSAPI beregner jeg desuden den gennemsnitlige PHP-RSS, så summen af webserver og SAPI forbliver stabil. Jeg tager TLS-termineringsomkostninger med i betragtning, fordi certifikathåndtryk og store udgående buffere øger forbruget. Først når RAM-balancen er i orden, strammer jeg trådene yderligere.

Ved HTTP/2/3 tager jeg højde for ekstra header-/flow-control-tilstande pr. forbindelse. GZIP/Brotli bufferer komprimerede og ukomprimerede data samtidigt, hvilket kan betyde flere hundrede KB ekstra pr. anmodning. Jeg planlægger også reserver til logfiler og midlertidige filer. I Apache øger mindre ThreadStackSize-værdier densiteten, mens i NGINX og LiteSpeed er det primært antallet af parallelle sockets og størrelsen af send/receive-buffere, der har betydning. Ved at lægge alle komponenter sammen før tuning undgår man ubehagelige overraskelser senere.

Hvornår jeg griber manuelt ind

Jeg stoler på Selvjustering, indtil målinger viser det modsatte. Hvis jeg deler maskinen i shared hosting, bremser jeg coreThreads eller MaxThreads, så andre processer har tilstrækkelig CPU-tid [2][4]. Hvis der er en hård trådgrænse pr. proces, indstiller jeg maxThreads konservativt for at undgå OS-fejl [2]. Hvis der opstår mønstre, der ligner deadlock, øger jeg kun poolstørrelsen kortvarigt, observerer køerne og sænker den derefter igen. Hvis du vil sammenligne typiske mønstre med måleværdier, kan du finde vejledning i Sammenligning af webserverens hastighed, som jeg gerne bruger som plausibilitetskontrol.

Som indgrebssignaler bruger jeg primært: vedvarende p99-spidser trods lav CPU-belastning, stigende socket-køer, kraftigt voksende TIME_WAIT-tal eller en pludselig stigning i åbne FD'er. I sådanne tilfælde begrænser jeg først antagelser (forbindelses-/hastighedsbegrænsninger), afkobler backends med timeouts og øger først derefter forsigtigt tråde. På den måde undgår jeg at flytte overbelastningen indad og forværre latenstiden for alle.

Typiske fejl og hurtige kontroller

Jeg ser ofte til høj Keep-Alive-timeouts, der binder tråde, selvom der ikke flyder data. Også udbredt: MaxRequestWorkers langt over RAM-budgettet og ulimit-n for lavt til målparalleliteten. I NGINX undervurderer mange FD-brugen gennem upstream-forbindelser; hver backend tæller dobbelt. I LiteSpeed vokser PHP-puljer hurtigere end HTTP-arbejdere, hvilket betyder, at anmodninger accepteres, men betjenes for sent. Med korte belastningstests, heap-/RSS-sammenligning og et kig på køen finder jeg disse mønstre på få minutter.

Også hyppigt: syn-backlog for lille, så forbindelser afvises allerede før webserveren; adgangslogfiler uden buffer, der skriver synkront til langsomt lager; debug-/trace-logfiler, der ved en fejl forbliver aktive og binder CPU. Ved skift til HTTP/2/3 øger for generøse stream-grænser og header-buffere hukommelsesforbruget pr. forbindelse – hvilket er særlig synligt, når mange klienter overfører få data. Jeg kontrollerer derfor fordelingen af korte vs. lange svar og justerer grænserne i overensstemmelse hermed.

HTTP/2 og HTTP/3: Hvad betyder det for trådpuljer?

Multiplexing reducerer antallet af TCP-forbindelser pr. klient markant. Det er godt for FD'er og acceptomkostninger, men flytter presset til per-forbindelsesstatusser. Derfor indstiller jeg forsigtige grænser for samtidige streams for HTTP/2 og kalibrerer flowkontrol, så enkelte store downloads ikke dominerer forbindelsen. Med HTTP/3 bortfalder TCP-relaterede head-of-line-blokeringer, men til gengæld stiger CPU-forbruget pr. pakke. Det kompenserer jeg for med tilstrækkelig arbejdskapacitet og små bufferstørrelser, så latenstiden forbliver lav. I alle tilfælde gælder: hellere færre, godt udnyttede forbindelser med fornuftige keep-alive-værdier end alt for lange tomgangssessioner, der binder tråde og hukommelse.

Platformfaktorer: Kernel, container og NUMA

Når det gælder virtualisering, er jeg opmærksom på CPU-tyveri og cgroups-begrænsninger: Hvis hypervisoren stjæler kerner, eller containeren kun har delkerner, kan worker_processes=auto være for optimistisk. Jeg fastgør arbejdere til reelle kerner efter behov og tilpasser antallet til effektiv tilgængeligt budget. På NUMA-værter drager webservere fordel af lokal hukommelsestildeling; jeg undgår unødvendige cross-node-adgange ved at samle arbejdere pr. socket. Transparent Huge Pages lader jeg ofte være deaktiveret for latenstidskritiske arbejdsbelastninger for at undgå spidsbelastninger i sidefejl.

På OS-niveau kontrollerer jeg filbeskrivelsesgrænser, forbindelsesbacklogs og portområdet for udgående forbindelser. Jeg øger kun det, jeg rent faktisk har brug for, tester adfærden ved rollover og overholder sikkerhedsgrænserne strengt. På netværkssiden sikrer jeg, at RSS/IRQ-fordeling og MTU-indstillinger passer til trafikprofilen – ellers går tuning i webserveren til spilde, fordi pakker ankommer for langsomt eller sidder fast i NIC-køen.

Måling i stedet for gætteri: Praktisk vejledning til test

Jeg udfører belastningstests i tre trin: Opvarmning (caches, JIT, TLS-sessioner), plateau (stabil RPS/samtidighed) og burst (korte spidsbelastninger). Separate profiler for statiske filer, API-kald og dynamiske sider hjælper med at isolere, hvor tråde, I/O eller backends begrænser. Jeg noterer parallelt FD-tal, run-queues, kontekstskift, RSS pr. proces og p50/p95/p99-latenser. Som mål vælger jeg driftspunkter ved 70-85 %-udnyttelse – nok buffer til reelle svingninger uden at køre permanent i mætningsområdet.

Kort vejledning til beslutningstagning

Jeg vælger NGINX, når lav latenstid, sparsomme ressourcer og fleksible .conf-tuningmuligheder tæller. Jeg satser på LiteSpeed, når PHP-belastningen dominerer, GUI'en skal forenkle driften, og LSAPI reducerer flaskehalse. Jeg bruger Apache, når jeg er afhængig af moduler og .htaccess og har styr på MPM-event-konfigurationen. Selvjusteringsmekanismerne er i mange tilfælde tilstrækkelige; jeg behøver først at gribe ind, når målinger indikerer hæng, hårde grænser eller RAM-pres [2]. Med realistiske kerne- og RAM-budgetter, små trin og observation af latenstidskurver bringer trådjustering mig pålideligt i mål.

Aktuelle artikler