...

Otimização do pool de threads para servidores web: comparação entre Apache, NGINX e LiteSpeed

Este artigo mostra como a servidor web com pool de threads Configuração no Apache, NGINX e LiteSpeed Paralelismo, latência e requisitos de memória. Explico quais configurações são importantes sob carga e onde o autoajuste é suficiente – com diferenças claras nas solicitações por segundo.

Pontos centrais

  • Arquitetura: Processos/Threads (Apache) vs. Eventos (NGINX/LiteSpeed)
  • Autoajuste: O ajuste automático reduz a latência e as interrupções
  • Recursos: Os núcleos da CPU e a RAM determinam tamanhos de thread adequados
  • Carga de trabalho: com grande carga de E/S, são necessários mais threads; com grande carga de CPU, são necessários menos threads.
  • Afinação: Parâmetros pequenos e específicos têm um efeito mais forte do que valores globais.

Comparação entre arquiteturas de pool de threads

Começo com o Arquitetura, porque define os limites do espaço de ajuste. O Apache depende de processos ou threads por ligação; isso consome mais RAM e aumenta a latência em horários de pico [1]. O NGINX e o LiteSpeed seguem um modelo orientado a eventos, no qual poucos trabalhadores multiplexam muitas ligações – isso economiza mudanças de contexto e reduz a sobrecarga [1]. Em testes, o NGINX processou 6.025,3 pedidos/s, o Apache atingiu 826,5 pedidos/s no mesmo cenário e o LiteSpeed ficou na liderança com 69.618,5 pedidos/s [1]. Quem quiser se aprofundar na comparação de arquiteturas encontrará mais dados importantes em Apache vs NGINX, que utilizo para uma primeira classificação.

Também é importante a forma como cada motor lida com tarefas bloqueantes. O NGINX e o LiteSpeed desacoplam o ciclo de eventos do sistema de ficheiros ou do I/O upstream através de interfaces assíncronas e threads auxiliares limitadas. No modelo clássico, o Apache liga um thread/processo por ligação; com o MPM event, é possível aliviar o Keep-Alive, mas a pegada de memória por ligação continua a ser maior. Na prática, isso significa que quanto mais clientes lentos simultâneos ou uploads grandes, mais vantajoso é o modelo de eventos.

Como funciona realmente o autoajuste

Os servidores modernos controlam a Tópico-Número frequentemente automático. O controlador verifica a utilização em ciclos curtos, compara os valores atuais com os históricos e aumenta ou diminui o tamanho do pool [2]. Se uma fila fica presa, o algoritmo encurta o seu ciclo e adiciona threads adicionais até que o processamento volte a funcionar de forma estável [2]. Isso evita intervenções, impede a sobrealocação e reduz a probabilidade de bloqueios head-of-line. Como referência, utilizo o comportamento documentado de um controlador de autoajuste no Open Liberty, que descreve claramente a mecânica [2].

Presto atenção a três fatores: um Histerese contra flapping (sem reação imediata a cada pico), um limite máximo rígido contra transbordamentos de RAM e uma tamanho mínimo, para que os custos de aquecimento não sejam incorridos em cada burst. Também faz sentido definir um valor-alvo separado para ativo Threads (coreThreads) vs. threads máximos (maxThreads). Assim, o pool permanece ativo sem ocupar recursos em modo inativo [2]. Em ambientes partilhados, reduzo a taxa de expansão para que o servidor web não ocupe agressivamente slots de CPU em relação aos serviços vizinhos [4].

Índices de referência

Os valores reais ajudam a Decisões. Em cenários de picos, o NGINX destaca-se pela sua latência muito baixa e elevada estabilidade [3]. Em situações de paralelismo extremo, o Lighttpd apresenta o maior número de pedidos por segundo nos testes, seguido de perto pelo OpenLiteSpeed e pelo LiteSpeed [3]. O NGINX consegue transferências de ficheiros grandes com até 123,26 MB/s, com o OpenLiteSpeed logo atrás, o que destaca a eficiência da arquitetura orientada a eventos [3]. Utilizo esses indicadores para avaliar onde os ajustes de thread realmente trazem benefícios e onde os limites são decorrentes da arquitetura.

Servidor Modelo/Tópicos Taxa de exemplo mensagem principal
Apache Processo/thread por ligação 826,5 pedidos/s [1] Flexível, mas maior necessidade de RAM
NGINX Evento + poucos trabalhadores 6.025,3 Pedidos/s [1] Baixa Latência, económico
LiteSpeed Evento + LSAPI 69.618,5 Pedidos/s [1] Muito rápido, Ajuste da GUI
Lighttpd Evento + Assíncrono 28.308 pedidos/s (altamente paralelo) [3] Escalonado em Dicas muito bom

A tabela mostra valores relativos Vantagens, sem compromissos fixos. Eu sempre os avalio no contexto das minhas próprias cargas de trabalho: respostas dinâmicas curtas, muitos pequenos ficheiros estáticos ou grandes fluxos. As variações podem ser causadas pela rede, armazenamento, descarregamento TLS ou configuração PHP. Por isso, correlaciono métricas como CPU Steal, comprimento da fila de execução e RSS por trabalhador com o número de threads. Só esta perspetiva separa os verdadeiros gargalos de threads dos limites de I/O ou de aplicações.

Para obter números confiáveis, utilizo fases de ramp-up e comparo latências p50/p95/p99. Uma curva p99 acentuada Com valores p50 constantes, isso indica mais filas do que pura saturação da CPU. Perfis de carga abertos (controlados por RPS) em vez de fechados (controlados apenas por concorrência) também mostram melhor onde o sistema começa a descartar ativamente as solicitações. Assim, posso definir o ponto em que os aumentos de thread não trazem mais benefícios e a contrapressão ou os limites de taxa são mais úteis.

Prática: Dimensionar trabalhadores e ligações

Começo com o CPU-Núcleos: worker_processes ou LSWS‑Worker não podem exceder os núcleos, caso contrário, a mudança de contexto aumenta. Para NGINX, eu ajusto worker_connections de forma que a soma das ligações e descritores de ficheiros permaneça abaixo do ulimit‑n. No Apache, evito MaxRequestWorkers muito altos, porque o RSS por filho consome rapidamente a RAM. No LiteSpeed, mantenho os pools de processos PHP e os trabalhadores HTTP em equilíbrio, para que o PHP não se torne um gargalo. Quem quiser entender as diferenças de velocidade entre os motores se beneficia da comparação. LiteSpeed vs. Apache, que utilizo como fundo de afinação.

Uma regra prática simples: primeiro calculo o orçamento FD (ulimit-n menos reserva para logs, upstreams e ficheiros), divido-o pelas ligações simultâneas planeadas por trabalhador e verifico se a soma é suficiente para HTTP + upstream + buffer TLS. Em seguida, dimensiono a fila de backlog moderadamente – grande o suficiente para picos, pequena o suficiente para não ocultar a sobrecarga. Por fim, defino os valores de keep-alive para que se adequem aos padrões de solicitação: páginas curtas com muitos recursos se beneficiam de tempos de espera mais longos, enquanto o tráfego de API com poucas solicitações por conexão se beneficia mais de valores mais baixos.

Ajuste fino do LiteSpeed para cargas elevadas

Com o LiteSpeed, eu confio em LSAPI, porque minimiza a mudança de contexto. Assim que percebo que os processos CHILD estão esgotados, aumento o LSAPI_CHILDREN gradualmente de 10 para 40, se necessário até 100 – sempre acompanhado de verificações de CPU e RAM [6]. A GUI facilita a criação de ouvintes, liberações de portas, redirecionamentos e a leitura de .htaccess, o que acelera as alterações [1]. Sob carga contínua, testo o efeito de pequenos passos em vez de grandes saltos para detectar picos de latência antecipadamente. Em ambientes partilhados, reduzo os coreThreads quando outros serviços utilizam a CPU, para que o Self‑Tuner não mantenha demasiados threads ativos [2][4].

Além disso, observo o Keep-Alive por ouvinte e o uso de HTTP/2/HTTP/3: o multiplexing reduz o número de ligações, mas aumenta a necessidade de memória por socket. Por isso, mantenho os buffers de envio conservadores e ativo a compressão apenas onde o ganho líquido é claro (muitas respostas textuais, quase nenhum limite de CPU). Para ficheiros estáticos grandes, confio em mecanismos zero-copy e limito os slots de download simultâneos para que os PHP workers não fiquem sem recursos quando ocorrem picos de tráfego.

NGINX: Utilizar o modelo de eventos de forma eficiente

Para o NGINX, defino worker_processes como carro ou o número principal. Com epoll/kqueue, accept_mutex ativo e valores de backlog ajustados, mantenho as aceitações de conexão uniformes. Tenho o cuidado de definir keepalive_requests e keepalive_timeout de forma que os sockets ociosos não entupam o pool FD. Eu transfiro grandes ficheiros estáticos com sendfile, tcp_nopush e um output_buffers adequado. Eu uso limitação de taxa e limites de conexão apenas quando bots ou bursts sobrecarregam indiretamente o pool de threads, porque cada limitação gera gerenciamento de estado adicional.

Em cenários proxy, é Keepalive a montante Decisivo: demasiado baixo gera latência na criação da ligação, demasiado alto bloqueia os FDs. Eu escolho valores que se adequam à capacidade do backend e mantenho os tempos limite para connect/read/send claramente separados, para que backends defeituosos não bloqueiem os loops de eventos. Com reuseport e afinidade de CPU opcional, distribuo a carga de forma mais uniforme pelos núcleos, desde que as configurações IRQ/RSS da NIC suportem isso. Para HTTP/2/3, calibro cuidadosamente os limites de cabeçalho e controlo de fluxo para que fluxos individuais grandes não dominem toda a ligação.

Apache: definir corretamente o MPM event

No Apache, eu uso evento em vez de prefork, para que as sessões Keep-Alive não ocupem permanentemente os trabalhadores. Defino MinSpareThreads e MaxRequestWorkers de forma a que a fila de execução por núcleo permaneça abaixo de 1. Mantenho o ThreadStackSize pequeno para que mais trabalhadores caibam na RAM disponível; ele não pode ser muito pequeno, caso contrário, corre-se o risco de overflow de pilha nos módulos. Com um tempo limite de keep-alive moderado e keep-alive requests limitados, evito que poucos clientes bloqueiem muitos threads. Mudo o PHP para PHP-FPM ou LSAPI para que o servidor web em si permaneça leve.

Também presto atenção à relação entre ServerLimit, ThreadsPerChild e MaxRequestWorkers: esses três determinam juntos quantos threads podem ser criados realmente. Para HTTP/2, utilizo MPM event com limites de fluxo moderados; valores demasiado elevados aumentam o consumo de RAM e os custos do agendador. Só carrego módulos com grandes caches globais quando são necessários, pois as vantagens do Copy-on-Write desaparecem assim que os processos funcionam durante muito tempo e alteram a memória.

RAM e threads: calcular a memória corretamente

Conto os RSS por trabalhador/filho vezes o número máximo planeado e adiciono buffer do kernel e caches. Se não sobrar buffer, reduzo os threads ou nunca aumento o swap, porque o swap faz a latência explodir. Para PHP-FPM ou LSAPI, calculo adicionalmente o PHP-RSS médio, para que a soma do servidor web e do SAPI permaneça estável. Levo em consideração os custos de terminação TLS, porque os handshakes de certificados e os grandes buffers de saída aumentam o consumo. Só quando o balanço de RAM estiver correto é que continuo a apertar os parafusos dos threads.

No HTTP/2/3, considero estados adicionais de cabeçalho/controlo de fluxo por ligação. O GZIP/Brotli armazena em buffer dados comprimidos e não comprimidos simultaneamente; isso pode significar várias centenas de KB extras por pedido. Também planeio reservas para registos e ficheiros temporários. No Apache, valores menores de ThreadStackSize aumentam a densidade, enquanto no NGINX e no LiteSpeed, o número de sockets paralelos e o tamanho dos buffers de envio/receção têm um efeito predominante. Somar todos os componentes antes do ajuste evita surpresas desagradáveis mais tarde.

Quando intervenho manualmente

Confio em Autoajuste, até que as métricas mostrem o contrário. Se eu partilhar a máquina em alojamento partilhado, reduzo os coreThreads ou MaxThreads para que outros processos mantenham tempo de CPU suficiente [2][4]. Se existir um limite rígido de threads por processo, defino maxThreads de forma conservadora para evitar erros do sistema operativo [2]. Se ocorrerem padrões semelhantes a deadlock, eu aumento apenas temporariamente o tamanho do pool, observo as filas e, em seguida, reduzo novamente. Quem quiser comparar padrões típicos com valores medidos encontrará referências no Comparação da velocidade do servidor Web, que gosto de utilizar como verificação de plausibilidade.

Como sinais de intervenção, utilizo principalmente: picos p99 persistentes apesar da baixa carga da CPU, filas de soquetes crescentes, forte crescimento TIME_WAITNúmeros ou um aumento repentino de FDs em aberto. Nesses casos, primeiro reduzo as suposições (limites de conexão/taxa), desacoplo os backends com tempos limite e só depois aumento cuidadosamente os threads. Assim, evito transferir a sobrecarga apenas para dentro e piorar a latência para todos.

Erros típicos e verificações rápidas

Vejo frequentemente elevado Keep-Alive-Timeouts que vinculam threads, embora não haja fluxo de dados. Também comum: MaxRequestWorkers muito além do orçamento de RAM e ulimit-n muito baixo para a paralelidade alvo. No NGINX, muitos subestimam o uso de FD por conexões upstream; cada backend conta duas vezes. No LiteSpeed, os pools PHP crescem mais rapidamente do que os trabalhadores HTTP, o que faz com que as solicitações sejam aceites, mas atendidas tarde demais. Com testes de carga curtos, comparação heap/RSS e análise da fila de execução, encontro esses padrões em minutos.

Também frequente: syn-backlog muito pequeno, de modo que as ligações são rejeitadas antes mesmo de chegarem ao servidor web; registos de acesso sem buffer, que escrevem de forma síncrona em armazenamento lento; registos de depuração/rastreamento que permanecem ativos acidentalmente e ocupam CPU. Ao mudar para HTTP/2/3, limites de fluxo e buffers de cabeçalho muito generosos aumentam o consumo de memória por conexão – especialmente visível quando muitos clientes transferem poucos dados. Por isso, verifico a distribuição de respostas curtas vs. longas e ajusto os limites de acordo.

HTTP/2 e HTTP/3: o que significam para os conjuntos de threads

A multiplexação reduz significativamente o número de ligações TCP por cliente. Isso é bom para FDs e custos de aceitação, mas transfere a pressão para os estados por ligação. Por isso, defino limites cautelosos para fluxos simultâneos para HTTP/2 e calibro o controlo de fluxo para que downloads individuais grandes não dominem a ligação. No HTTP/3, os bloqueios de cabeça de linha relacionados com o TCP são eliminados, mas o consumo de CPU por pacote aumenta. Compenso isso com capacidade de trabalho suficiente e tamanhos de buffer pequenos, para que a latência permaneça baixa. Em todos os casos, é melhor ter menos ligações bem utilizadas com valores Keep-Alive razoáveis do que sessões ociosas excessivamente longas que ocupam threads e memória.

Fatores da plataforma: kernel, contentor e NUMA

No que diz respeito à virtualização, presto atenção a Roubo de CPU e limites de cgroups: se o hipervisor rouba núcleos ou o contentor possui apenas núcleos parciais, worker_processes=auto pode ser demasiado otimista. Se necessário, fixo os trabalhadores a núcleos reais e ajusto o número ao eficaz orçamento disponível. Em hosts NUMA, os servidores web beneficiam da alocação de memória local; evito acessos desnecessários entre nós, agrupando os trabalhadores por socket. Deixo as páginas enormes transparentes frequentemente desativadas para cargas de trabalho críticas em termos de latência, a fim de evitar picos de falhas de página.

Ao nível do sistema operativo, controlo os limites dos descritores de ficheiros, os backlogs de ligação e o intervalo de portas para ligações de saída. Aumento apenas o que realmente preciso, testo o comportamento durante a transição e mantenho limites de segurança rigorosos. No lado da rede, garanto que a distribuição RSS/IRQ e as configurações MTU correspondam ao perfil de tráfego – caso contrário, o ajuste no servidor web será inútil, porque os pacotes chegam muito lentamente ou ficam presos na fila da placa de rede.

Medir em vez de adivinhar: guia prático para testes

Eu realizo testes de carga em três etapas: aquecimento (caches, JIT, sessões TLS), platô (RPS/concorrência estáveis) e pico (picos curtos). Perfis separados para ficheiros estáticos, chamadas de API e páginas dinâmicas ajudam a ver de forma isolada onde os threads, I/O ou backends estão a limitar. Anoto paralelamente números FD, filas de execução, mudanças de contexto, RSS por processo e latências p50/p95/p99. Como meta, escolho pontos de operação com 70-85% de utilização – buffer suficiente para flutuações reais, sem funcionar permanentemente na área de saturação.

Guia de decisão resumido

Eu escolho NGINX, quando baixa latência, recursos econômicos e opções flexíveis de ajuste .conf são importantes. Eu uso o LiteSpeed quando a carga PHP é dominante, a GUI deve simplificar a operação e o LSAPI reduz os gargalos. Eu uso o Apache quando preciso de módulos e .htaccess e tenho o controle total da configuração MPM-event. Os mecanismos de autoajuste são suficientes em muitos casos; só preciso intervir quando as métricas indicam travamentos, limites rígidos ou pressão na RAM [2]. Com orçamentos realistas de núcleo e RAM, pequenos incrementos e observação das curvas de latência, o ajuste de threads me leva ao meu objetivo de forma confiável.

Artigos actuais