Apache 2.4 – Módulos de Multiprocessamento

Conceitos básicos

Apache é um servidor WEB (HTTPD) portado para várias plataformas e ambientes. O servidor WEB Apache pode ser executado em GNU/Linux, Windows, Netware ou OS/2. Para comportar essa flexibilidade de ambientes, Apache adota um projeto modular que permite ao webmaster escolher, entre um conjunto de funcionalidades, quais serão ativadas ou desativadas.

Os módulos são implementados de duas maneiras: estáticos ou dinâmicos:

  • Os módulos estáticos são inseridos em “tempo de compilação” e são incorporados ao código binário do servidor.
  • Os módulos dinâmicos são ligados (link) ao código binário do servidor em “tempo de execução”.

Os módulos do tipo MPM, são responsáveis pela definição de como o servidor atende pelas requisições HTTP dos clientes. As requisições chegam pela rede como pacotes TCP, por padrão na porta TCP/80. Para atender uma requisição HTTP, o servidor deve fazer um fork ou lançar um thread.

Esses métodos causam diferentes impactos no modo de trabalho do servidor e, principalmente, nos recursos de hardware (memória RAM e processador) necessários para atender todas as solicitações.

Originalmente, Apache utilizava o método prefork e lançava uma cópia da aplicação servidora (fork) para atender cada solicitação. Com o tempo, esse método se tornou ineficiente, pois os servidores passaram a atender milhares de requisições simultaneamente.

A quantidade de recursos de memória e de processamento para manter esse método, é enorme. O método prefork é mantido para fins de compatibilidade com antigos sistemas que não podem suportar o uso de threads, para servidores que atendem poucas requisições ou possuem um processador legado de núcleo único.

Entretanto, mesmo hoje prefork é conhecido pela estabilidade de seu código, desenvolvido ao longo de anos, e ainda é o modo preferido por muitos administradores.

Servidores GNU/Linux que demandam escalabilidade, podem escolher entre os módulos worker ou event. Esses métodos dão suporte nativo para o uso de threads e implementam escalabilidade através do compartilhamento de recursos de hardware (principalmente memória RAM) entre os diversos threads (tarefas).

Os threads são linhas de execução de um programa que se dividem como tarefas distintas e que podem compartilhar recursos comuns no contexto de software (memória). Isso provê um melhor aproveitamento da memória RAM e reduz a sobrecarga gerada pelo processo de lançar processos através de fork.

Com o advento dos processadores de múltiplos núcleos, o uso de threads atingiu o patamar mais alto; as diferentes tarefas são realmente processadas simultaneamente por diferentes núcleos de processadores. Observe que os threads compartilham o mesmo contexto de software para diferentes contextos de hardware, pois são ligados ao mesmo processo pai.

O suporte e os métodos para o funcionamento de threads são totalmente fornecidos pelo sistema operacional, que podem ser distintos entre si, apresentando diferentes níveis de maturidade da implementação das funções de thread.

Os MPM prefork, worker e event são exclusivos de sistemas do tipo UNIX. Para os demais sistemas, temos: mpm_netware, mpm_os2 e mpm_winnt. Cada um aproveitando as melhores características desses sistemas operacionais.

Atualmente, (2014) o MPM padrão para modernos sistemas do tipo UNIX é event.

Para ajudar na escolha do MPM, duas perguntas são feitas.

1. O núcleo do sistema operacional suporta threads?
2. O núcleo do sistema operacional suporta threads seguros (thread-safe polling)?

  • Se a resposta é sim para ambas perguntas, seu MPM deve ser event.
  • Se a resposta é sim para a primeira pergunta, seu MPM deve ser worker.
  • Se a resposta é não para ambas perguntas, seu MPM deve ser prefork.
MPM prefork

O módulo prefork implementa um servidor HTTPD não-thread. Cada requisição HTTP é atendida por um processo individual criado a partir de um fork. Isso pode ser útil para aplicações WEB que não aceitam ou não sabem trabalhar com threads de modo seguro.

Outra razão é o melhor isolamento evitando que um processo errante contamine e derrube o servidor. Isso pode ser útil até que sua aplicação esteja em um estado maduro para trabalhar com threads.

Esse módulo se auto regula, dessa maneira, raramente, é preciso fazer ajustes no valor padrão de seus parâmetros. O principal parâmetro é MaxRequestWorks, que deve ser grande o suficiente para manipular o número de requisições que o servidor irá receber, mas pequeno o suficiente para não exaurir seus recursos de hardware. Vejamos como esse módulo funciona na prática.

Um processo pai (httpd ou apache2) mantém sempre um certo número de processos filhos ouvindo por requisições HTTP na porta TCP/80. Esses processos são chamados de spare (reserva) ou de processos idle (desocupados). Quando uma requisição HTTP chega pela rede, eles devem estar prontos para atender. Isso evita que as requisições precisem esperar que o servidor realize o fork para criar novos processos filhos, pois em termos de computação, esse é um tempo grande.

Um conjunto de diretivas configura como o processo pai cria seus processos filhos. Na maioria dos sites o valor padrão é suficiente para obter o equilíbrio entre demanda e disponibilidade de recursos de hardware. De acordo com o manual do Apache 2.4, se um site precisa atender mais de 256 requisições simultâneas é preciso fazer ajustes manuais nesses parâmetros até encontrar o equilíbrio desejado.

Enquanto o processo pai (daemon) necessita de privilégios de administrador (root), os processos filhos são lançados com os privilégios de um usuário comum (no Debian, esse usuário se chama www-data). As diretivas User e Group definem esse usuário comum. Isso é válido para prefork, worker e event.

O privilégio de leitura (read) no sistema de arquivos, deve ser garantido ao conteúdo que esse usuário comum irá fornecer; os demais privilégios devem ser mantidos ao mínimo necessário. Vejamos como as principais diretivas de prefork devem ser configuradas.

MaxSpareServers – define o número máximo de processos spare. O valor padrão é 10. Se o número de processos spare exceder o valor definido para essa diretiva (após a liberação de um processo que estava ocupado) o servidor irá matar esses processos excedentes (isso tem um alto custo computacional, tenha cuidado!). O menor valor aceito para esse argumento é SEMPRE gerado por MinSpareServers + 1.

MinSpareServers – número mínimo de processos spare mantidos pelo servidor. O valor padrão é 5. Se o número de processos spare ficar abaixo do valor definido nesse parâmetro, o servidor lançará novos processos-filhos até atingir o valor informado nessa diretiva. Esse processo de lançar forks, ocorrerá de forma exponencial e com intervalos de um segundo, até atingir o valor de 32 processos lançados simultaneamente ou até atingir o valor mínimo definido na diretiva.

Assim, o servidor lança um processo, aguarda um segundo e lança dois processos. Aguarda outro segundo e lança quatro processos, sucessivamente até lançar 32 processos simultaneamente ou atingir o valor mínimo de spares definido na diretiva, o que acontecer primeiro.

StartServers – define o número de processos filhos lançados no momento da inicialização do servidor HTTPD. Essa diretiva é comum para os módulos prefork, worker ou event. Esse parâmetro permite um balanceamento entre os valores mínimos e máximos.

O valor padrão irá variar entre os diferentes MPMs devido as diferenças de funcionamento entre eles. Para o método prefork, esse valor é 5. Observe que esse valor deve estar entre os valores mínimo e máximo.

A inclusão dessas diretivas em um arquivo de configuração, ficaria do seguinte modo:

  • StartServers 10
  • MaxSpareServers 20
  • MinSpareServers 5

Isso gera um conjunto de processos do seguinte modo:

Linux: Apache 2.4 - Módulos de Multiprocessamento - MPM

Observe que o primeiro processo (PID 1796 em vermelho) tem privilégios de usuário administrativo (root), enquanto os demais (destacados em verde) são inicializados pelo usuário comum (www-data).

MPM worker

Essa MPM implementa um servidor multiprocesso e multithread. Utilizando threads é possível atender mais solicitações HTTP com menos processos e, deste modo, consumir menos recursos de hardware. No modo worker, os processos-filhos têm capacidade de lançar vários threads que ouvem por requisições.

Isso mantém a estabilidade do sistema isolando as requisições em cada processo filho e seus vários threads. Caso uma aplicação errante derrube um processo-filho, o servidor ainda é capaz de continuar atendendo requisições através de outros processos-filhos e seus threads.

A mais importante diretiva utilizada pelo método Worker é ThreadsPerChild, que controla o número de threads lançados por processo. Vejamos como esse módulo funciona na prática.

Um processo pai (daemon) é responsável por lançar processos filhos. Cada um desses processos filhos cria um certo número de servidores HTTP que se apresentam na forma de threads. São os threads que ouvem pelas conexões e quando elas chegam, eles passam o controle para o seu processo pai.

Apache irá manter um conjunto de threads spare ou threads idle, evitando que a requisição HTTP tenha que esperar pela criação do thread que irá atendê-la. O número de processos inicialmente lançados, é definido na diretiva StartServers. Observe que em relação ao módulo prefork esse valor é sempre menor, pois cada processo lançará vários threads compensando o número menor de processos em várias vezes.

O servidor HTTPD avalia o número de threads ociosos de todos os processos e baseado nos valores de MinSpareThreads e MaxSpareThreads, o servidor decidirá por lançar ou matar processos. Apache gerencia esses valores automaticamente e de modo satisfatório para sites que não são movimentados.

Para sites que atendem muitas requisições, o ajuste manual é necessário. A diretiva MaxRequestWorkers define o número máximo de threads permitidos em todos os processos filhos. O número máximo de processos filhos é definido pela divisão do valor de MaxRequestWorkers por ThreadsPerChild.

Duas diretivas definem o limite hard do número de processos filhos e servidores de threads em um processo filho. Essas diretivas somente podem ser modificadas com a parada total do serviço. A diretiva ServerLimit define um limite hard para o número de processos filhos e deve ser maior ou igual ao valor definido para MaxRequestWorkers dividido por ThreadsPerChild.

A diretiva ThreadLimit é um limite hard para o número de servidores de threads e deve ser maior ou igual à diretiva ThreadsPerChild.

Independente do conjunto de processos filhos, pode haver alguns processos que são terminados e um de seus threads ainda está ativo lidando com uma conexão. O valor da diretiva MaxRequestWorkers (define o número máximo de conexões simultâneas), pode interferir bloqueando novos processos, pois entende que o thread está ativo.

Para evitar esse comportamento, desabilite o término de processos-filhos definindo o valor de MaxConnectionPerChild para zero e deixe os valores de MaxSpareThreads e MaxRequestWorkers, iguais. A configuração padrão para o módulo worker é:

 ServerLimit         16
 StartServers        2
 MaxRequestWorkers   400
 MinSpareThreads     75
 MaxSpareThreads     250
 ThreadsPerChild     25

A menos que suexec esteja sendo utilizado, essas diretivas vão configurar os privilégios com que scripts CGI são executados.

A diretiva MaxConnectionsPerChild controla a frequência com que os processos são reciclados, matando processos velhos e lançando novos. Isso é uma função de segurança do sistema.

A MPM worker utiliza um Mutex (mpm-accept) para criar uma lista com as requisições que chegam. Consulte a documentação para saber como ajustar o valor do mutex.

Diretivas para worker:

ServerLimit – é uma diretiva comum em prefork, worker ou event utilizada para definir o limite superior do número de processos. Em worker essa diretiva, em combinação com ThreadLimit configura o valor máximo de MaxRequestWorkers definindo o tempo de vida útil de um processo HTTPD. Essa diretiva não pode ser reconfigurada em tempo de execução. Se o valor dessa diretiva e MaxRequestWorkers forem definidos de forma muito alta haverá um grande consumo de memória compartilhada de modo que o sistema pode não conseguir manipular, se tornando instável, ou até impedindo o serviço HTTPD de ser inicializado. O valor padrão para essa diretiva é 16.

StartServers – é uma diretiva comum em prefork, worker ou event utilizada para definir o número de processos filhos criados no momento da inicialização. O padrão para worker é 3.

MaxRequestWorkers – é uma diretiva comum em prefork, worker ou event utilizada para definir o número máximo de conexões que são processadas simultaneamente. Qualquer requisição que chegar, após esse valor ser atingido, será colocada em uma fila de espera. Isso ocorrerá até atingir o valor definido em ListenBackLog, que define o tamanho da fila. Acima desse valor as requisições são descartadas. As requisições da fila são atendidas tão logo um thread seja liberado por sua ordem de chegada. Para prefork esse valor é 256. Para worker e event é definido pelo valor de ServerLimit multiplicado por ThreadsPerChild. Esses valores devem ser mutuamente atualizados para evitar erros.

MinSpareThreads – define o número mínimo de threads ociosos para lidar com requisições entrantes. Comum em worker e event. Esse valor é medido para todo o servidor. Se o número de threads for inferior ao valor definido, então, o servidor pode gerar processos filhos para atender esse valor.

MaxSpareThreads – essa diretiva é comum para worker e event. É utilizada para definir o valor do número máximo de threads ociosos. Se houver mais threads ociosos que o valor dessa diretiva, então, processos filhos serão mortos até ser menor que esse valor. Apache irá corrigir o valor informado aqui de acordo com a seguinte regra. O valor deve ser maior ou igual a soma de MinSpareThreads e ThreadsPerChild.

ThreadsPerChild – essa é uma diretiva comum a worker e event, utilizada para definir o número de threads criados por cada processo filho. O processo filho cria esses threads no momento de sua criação e não cria mais threads depois. O valor padrão é 25. O total de threads deve ser suficiente para comportar a carga de requisições recebida pelo servidor.

MPM event

Esse MPM é uma variante de worker, elaborada para permitir mais requisições simultâneas utilizando intensamente os threads. Vejamos como isso funciona na prática.

Esse MPM tenta solucionar o problema keep alive presente no servidor HTTPD. Um cliente HTTP sempre tenta manter a conexão aberta e enviar mais requisições pelo mesmo socket. Isso representa uma sobrecarga para TCP que gerencia a abertura e o fechamento das conexões. Tradicionalmente, a abordagem utilizada por Apache é manter um processo filho e seus threads aguardando por novas requisições o que representa desvantagens óbvias.

Para tentar solucionar essa questão, a MPM event realiza uma abordagem diferente utilizando um thread dedicado para manipular os sockets, que ouvem por conexões, ao mesmo tempo que utiliza outros threads para enviar e receber dados de conexões já estabelecidas.
Para alguns casos de uso, alguns filtros se declaram incompatíveis com event e voltam a se comportar como se estivessem sendo utilizados por worker, reservando um thread por conexão. Todos os módulos providos juntamente com Apache são compatíveis com event.

Observe que muitas das diretivas de worker são válidas também para event.

AsyncRequestWorkerFactor – limita o número de conexões concorrentes por processo. A MPM event manipula algumas conexões de modo assíncrono. Requisições que trabalham com threads são alocadas por um período de tempo curto, conforme necessário, e outras requisições com um thread reservado por conexão. Isso pode levar a situação onde todos os workers estão ocupados e não há threads disponíveis para manipular novas solicitações assíncronas de conexão.

Para mitigar esse problema, event faz duas coisas. Inicialmente, ele limita o número de conexões aceitas por processo, dependendo do número de workers idle. Posteriormente, se todos os workers estão ocupados, ele fechará conexões que estão no estado keep alive mesmo se o timeout não tenha sido atingido.

Essa diretiva permite um ajuste mais fino (tunning) do limite das conexões por processo. Um processo somente aceitará novas conexões se o número de conexões correntes ( conexões em estado closing não são contadas) for menor que:

  • ThreadsPerChild + (AsyncRequestWorkerFactor * número de idle workers)

Isso significa que o número máximo de conexões concorrentes é:

  • (AsyncRequestWorkerFactor + 1) * MaxRequestWorkers
Rolar para cima