Cuidados essenciais com dimensionamento do servidor rodando Apache

O que vou falar aqui é relacionado com o modelo worker do Apache, mas vale também para o modelo padrão prefork.

Um dos problemas recentes que me apareceram, relacionados com dimensionamento de servidores, foi relacionado com o Apache. É importante entender como o Apache funciona, bem como como as threads funcionam, para dimensionarmos o servidor de forma a não obtermos negações de serviço.

O Apache worker dispara seus processos (servers) e, em cada um deles, uma quantidade fixa de threads, que são colocadas em estado suspenso. Cada processo e cada thread aloca uma pilha com tamanho padrão. O Apache também aloca um espaço fixo no heap. O modelo de gerenciamento de memória do Apache é baseado em pools, ou seja. Para cada conexão existe uma quantidade fixa de memória pré-alocada.

Assim, a quantidade mínima de memória dimensionada para um servidor do Apache deveria seguir a seguinte fórmula:

\begin{matrix}  MinMem&=(MaxClients\cdot ThreadStackSize)+\\  &ServerLimit\cdot(StackSize+PoolSize)  \end{matrix}

Onde StackSize é o valor obtido no ulimit, PoolSize pode ser considerado como uns 16 MiB (valor empírico). Os parâmetros MaxClients e ServerLimit são definidos na configuração do Apache. Por default, ServerLimit é 16 e MaxClients é definido como ServerLimit * ThreadsPerChild. Onde ThreadsPerChild é, por default, 25.

Update:

Quando escrevi esse artigo consultei a documentação do Apache 2.1+ e lá pode ser lido que, em sistemas que usam pilhas muito grandes (o Linux usa!) pode ser conveniente diminuí-la, para cada thread, usando a diretiva ThreadStackSize, e que o Apache tentará usar o tamanho de pilha padrão (obtido com ulimit -s).

Acontece que isso não é verdade! Medindo o consumo pude observar que o tamanho das pilhas paa as threads é muito inferior ao tamanho padrão, que pode ser obtido com o seguinte código:

/* Esse programinha faz a mesma coisa que "ulimit -s" */
#include <stdio.h&gt;
#include <pthread.h&gt;

int main(void)
{
  pthread_attr_t attr;
  size_t stacksize;

  pthread_attr_init(&attr);

  /* Concorda que esse é o tamanho "default"? */
  pthread_attr_getstacksize(&attr, &stacksize)

  pthread_attr_destroy(&attr);

  printf("Default thread stack size: %u\n", stacksize);

  return 0;
}
$ gcc -o threadstk threadstk.c -lpthread
$ ./threadstk
Default thread stack size: 8388608

No Debian você obterá 8388608 bytes (ou, 8 MiB). No Red Hat Enterprise Linux, provavelmente, obterá 10 MiB. Só que, na minha medição, 16 servers com 25 threads cada consumiram cerca de 512 MiB apenas! Assim, só posso tirar a conclusão de que a pilha de cada thread tem, de fato, apenas 256 kB (não é menor porque a documentação diz que qualquer coisa menor que 128 kB dará craca!)!

É, meus amigos, eu caí no erro que tanto insisto para que vocês não caiam… Não confie na documentação. Meça sempre! Para corrigir meu erro, o texto abaixo foi modificado em relação ao post anterior.

A explicação da fórmula acima é simples: Cada thread tem uma pilha de tamanho ThreadStackSize, daí o primeiro termo. Cada processo tem pilha própria e um pool de memória fixo. Daí o segundo termo.

A configuração default de um servidor para o Apache, no modelo worker, atender 400 conexões, deveria (no Debian, onde o default stack size é de 8 MiB) ser cerca de

\displaystyle MinMem=(400\cdot0.25)+16\cdot(10+16)=100+416=516\,MiB

UAU! 516 MiB para atender apenas 400 conexões simultâneas (ênfase no simultâneas)? Pois é… note que não estou considerando as necessidades do Linux e o comportamento da aplicação que será executada sob os cuidados do Apache. Outros parâmetros que podem ser adicionados cálculo são a quantidade de bufffers e caches que o Linux precisará bem como os requisitos de memória mínimos da aplicação por conexão! A equação acima poderia ficar assim:

\displaystyle \begin{matrix}  MinMem&=MaxClients\cdot(AppMinMem+ThreadStackSize)+\\  &ServerLimit\cdot(StackSize+PoolSize)+\\  &SysMinMem\end{matrix}

Os requisitos mínimos da aplicação são, neste sentido, bem críticos, quer dizer, aumentarão consideravelmente o tamanho da memória física necessária para acomodar a aplicação e o Apache. O termo SysMinMem é relacionado aos buffers/caches e um espaço de reserva para executar outras aplicações, como crontab, ssh, etc…

Outra coisa que não mostrei aqui (porque é mais complicado avaliar) são os requisitos de processamento. Esse fator depende diretamente do uso do Apache e o maior peso recai sobre a aplicação. No entanto, de forma geral, é conveniente calcular o limite de servidores levando em conta a distribuição das threads pelos processadores. Num sistema quad core, por exemplo, os defaults distribuem-se em cerca de 100 threads por processador. É um bom limite. Acredito que até 200 threads por processador possa ser aceitável, mas dependerá, de novo, de quanta “pressão” a aplicação colocará nas threads. Uma thread mais ocupada executará mais lentamente (por causa da distribuição!). Então é necessária uma avaliação mais cuidadosa ao usar a equação abaixo:

\displaystyle MinCPUs=\frac{MaxClients}{4\cdot ThreadsPerChild}

ou

\displaystyle MinCPUs=\frac{ServerLimit}{4}

Mesmo porque ela é ainda mais empírica do que a equação anterior…

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s