Na primeira parte, tratamos das principais etapas para instalação do Squid. Nesta segunda veremos como otimizar a configuração do sistema operacional e o serviço Squid para lidar com um volume de tráfego mais intenso. Demonstraremos ajustes que impactarão de forma positiva e expressiva em diferentes instalações ou serviços.

Por se tratar de otimizações relativamente granulares, partirei do princípio que o leitor já possui conhecimento básico sobre sistema operacional e funcionamento padrão do Squid.

Em ambientes maiores e complexos, a configuração padrão pode não ser suficiente para atender a demanda. Felizmente, podemos realizar otimizações bastante significativas, mas o processo nem sempre será simples e dependerá de uma combinação de estratégias.

Primeiro, precisamos dimensionar o hardware necessário em relação a configuração desejada.

Na maioria das vezes, desejamos contar com cache em disco. Portanto, é importante trabalhar com HDs de alta performance – normalmente, com taxa de leitura a partir de 4 Gb/s. Se possível, opte por discos SCSI ou SAS. Em determinados casos, sem a implementação de cluster, pode ser uma boa ideia utilizar mais de um HD na definição da área de spool (cache) e habilitar operações de I/O assíncronas (apenas se contar com mais de um). Pessoalmente, acho mais interessante criar uma estrutura de cluster e escalonar com a adição de novos nós – tende ser mais eficiente.

Para validar a performance de leitura do HD, podemos utilizar o comando hdparm:

root@proxysrv:~# sudo hdparm -Tt /dev/sda

/dev/sda:
  Timing cached reads:   9736 MB in  2.00 seconds = 4873.17 MB/sec
  Timing buffered disk reads: 328 MB in  3.00 seconds = 109.18 MB/sec

E, com o comando dd, podemos validar a taxa de escrita também:

– Sem sincronismo.

root@proxysrv:~# dd if=/dev/zero of=/tmp/teste bs=8k count=10k; rm -f /tmp/teste
10240+0 registros de entrada
10240+0 registros de saída
83886080 bytes (84 MB) copiados, 0,141014 s, 595 MB/s

– Instruindo o sincronismo completo (antes de finalizar).

root@proxysrv:~# dd if=/dev/zero of=/tmp/teste conv=fdatasync bs=8k count=10k; rm -f /tmp/teste
10240+0 registros de entrada
10240+0 registros de saída
83886080 bytes (84 MB) copiados, 0,934422 s, 89,8 MB/s

Em seguida, devemos mensurar o consumo de memória do servidor.

Infelizmente, esta não é uma tarefa trivial, ainda mais se houver serviços concorrentes – o que normalmente acontece. Então, devemos identificar o consumo principal, a demanda de acesso e avaliar a saúde do servidor periodicamente.

Os objetos armazenados em disco (cache) são indexados e, na configuração padrão (na arquitetura 64 bits), a cada 1Gb, o servidor precisa alocar 14Mb de memória. Ou seja, para disponibilizar 100Gb de cache em disco, será necessário em torno de 1.4Gb de memória RAM só para indexação.

Por questões de performance, o Squid também permite definir outra área de cache em memória. A princípio, se definirmos a opção cache_mem como “512 MB“, teríamos então uma alocação de 1.912 MB (1.400 + 512) apenas para cache. Para ampliar a margem de segurança, considere mais 100 MB para alocações dinâmicas.

Neste caso, o ideal seria dispor de um servidor com, pelo menos, 4Gb de memória RAM.

Quanto maior for a área de spool, maior será o consumo de memória. Mas, na prática, o cálculo exibido estima o pior cenário (baseado na “configuração padrão“), pois o consumo de memória também depende do número total de objetos armazenados e podemos delimitar a utilização das áreas de cache (diretórios de swap/spool).

É possível subdividir os diretórios de swap (múltiplas definições cache_dir), delimitando o tamanho máximo e mínimo dos objetos em cada um, alterando significativamente a estimativa do número total de objetos e otimizando também o sistema de indexação.

Por exemplo (em /etc/squid/squid.conf):

   mem_cache 512 MB
   ...
   maximum_object_size_in_memory 256 KB
   maximum_object_size 200 MB
   minimum_object_size 0 KB
   ...
   cache_dir ufs /var/spool/squid/squid1/cache1 7000 32 256 min-size=64000 max-size=1024000
   cache_dir ufs /var/spool/squid/squid1/cache2 29000 16 256 min-size=1024001

No exemplo acima, a primeira definição (em cache1) prevê uma área de 7Gb para objetos entre 64K e 1Mb (como arquivos html, css, js ou imagens de baixa resolução). Abaixo disto (64K), o objeto será cacheado apenas em memória (no cache de 512 Mb). No spool, não compensa armazenar e indexar objetos menores. Por esta razão definimos min-size como 64000 (em bytes). Na definição seguinte (em cache2), que prevê 29Gb de armazenamento, permitimos apenas objetos maiores que 1Mb.

Os objetos envolvidos em uma “navegação comum”, na maioria das vezes, são pequenos e serão armazenados em memória ou diretório de spool cache1. Já os objetos maiores, como downloads de até 200 MB (limite imposto por maximum_object_size), serão em cache2.

Percebam que, ao delimitar o tamanho dos objetos, alteramos expressivamente a média de objetos por cache_dir e ainda otimizamos a distribuição dos objetos em disco.

A cada alteração, em cache_dir, é recomendável recriar a estrutura de diretórios:

root@proxysrv:~# service squid stop
root@proxysrv:~# mv /var/spool/squid /var/spool/squid.old
root@proxysrv:~# mkdir -p /var/spool/squid/squid1/{cache1,cache2}
root@proxysrv:~# chown -R proxy.proxy /var/spool/squid
root@proxysrv:~# squid -z
root@proxysrv:~# service squid restart
root@proxysrv:~# rm -rf /var/spool/squid.old

Observação: “O tipo de armazenamento ufs é o mais antigo e funciona muito bem na maioria dos casos. Para trabalhar com I/O assíncrono, basta substituir ufs por aufs. No entanto, vale lembrar que: a performance será comprometida caso o administrador opte pelo aufs com múltiplas definições de cache_dir apontando para o mesmo disco. Já o formato rock é mais indicado para implementações com múltiplas instâncias (workers: suporte SMP).

Vale uma ressalva:
– Não podemos esquecer dos algoritmos para preservação e reciclagem de objetos em cache
:

cache_replacement_policy heap LFUDA
memory_replacement_policy heap GDSF

Por ser uma área mais escassa, o algoritmo ideal para o cache em memória (cache_mem) é o heap GDSF, pois privilegia objetos menores e mais populares. No caso do cache em disco (cache_dir), o algoritmo ideal é o heap LFUDA, pois privilegia os objetos mais populares independente do tamanho.

O Squid também realizará cache (em memória) das consultas de DNS. Em squid.conf, através das opções ipcache_size e fqdncache_size, é possível alterar o número máximo de entradas. Quanto maior o número, “melhor”. É evidente que, o ajuste destas opções influenciará no consumo de memória RAM e reutilização de mapeamentos de DNS desatualizados (menor incidência).

Alguns administradores, visando diminuir ainda mais o tráfego de consultas de DNS (meu caso), optam pela instalação de um serviço mais robusto para cache e controle de requisições DNS. Para dimensionar ou limitar a alocação de memória do bind9 (por exemplo), devemos ajustar as opções max-cache-size (valor padrão: ilimitado) e recursive-clients (valor padrão: 1000 requisições). A opção recursive-clientes determina o número máximo de consultas simultâneas. Logo, como cada consulta aloca 20K de memória (segundo a documentação), por padrão, o consumo máximo para atender todas as requisições será de 20 Mb mais o total em cache (por padrão, ilimitado).

Como max-cache-size é ilimitado, para evitar má utilização dos recursos de hardware, procure fixar um limite com a opção max-cache-size (o limite será por visão).

No Ubuntu, basta ajustar o bloco options do arquivo /etc/bind/named.conf.options:

options {
        directory "/var/cache/bind";

        // If there is a firewall between you and nameservers you want
        // to talk to, you may need to fix the firewall to allow multiple
        // ports to talk.  See http://www.kb.cert.org/vuls/id/800113

...

        max-cache-size 50M;

        clients-per-query 0;
        forward only;
        forwarders {
                8.8.8.8;
                8.8.4.4;
        };

        allow-query {
                10.0.0.0/8;
                127.0.0.1;
        };

        version " ";
...
}

Para consultar os mapeamentos em cache, basta utilizar os comandos:

– Primeiro, gere um dump em:
/var/cache/bind/named_dump.db

root@proxysrv:~# rndc dumpdb -cache

– Pesquisando o conteúdo de /var/cache/bind/named_dump.db

root@proxysrv:~# grep youtube /var/cache/bind/named_dump.db
youtube-ui.l.google.com. 167    A       216.58.222.14
youtube.com.            62494   NS      ns1.google.com.
www.youtube.com.        55903   CNAME   youtube-ui.l.google.com.

– Para esvaziar o cache

root@proxysrv:~# rndc flush

Existe um controle interno de memória, feito pelo sistema operacional, que merece atenção. Por questões de performance, o kernel do Linux procura manter em memória RAM áreas frequentemente referenciadas e move para Swap as que são pouco referenciadas. Algo fácil de constatar, mesmo dispondo de bastante memória livre. Em instalações de servidor, este comportamento pode ser prejudicial.

Existe um parâmetro sysctl (ajustes sob /proc), chamado vm.swappiness, que determina qual porcentagem da área não utilizada pode ser movida para Swap – em instalações de SGBDs é perfeitamente aceitável definir o valor como 0 (nenhuma área não utilizada será movida).

O valor padrão de vm.swappiness é 60. Comece diminuindo para 20 caso o servidor esteja realizando Swap desnecessariamente (avalie com os comandos free ou top, por exemplo):

– Modificando o valor em memória

root@proxysrv:~# sysctl -w vm.swappiness=20

– Verificando o valor corrente

root@proxysrv:~# sysctl -a 2>/dev/null | grep vm.swap
vm.swappiness = 20

A análise detalhada do consumo de memória virtual pode ser feita com o comando pmap:

root@proxysrv:~# pmap `pidof nome_do_processo`

Em relação ao consumo de memória, demos uma pincelada nos recursos principais e essenciais, mas cada serviço ou recurso adicional deve ser considerado também“.

Nos últimos anos, o kernel sofreu inúmeros ajustes para tornar o auto-tuning mais eficiente. Entretanto, as otimizações de rede precisam ser manuais. Dificilmente o sistema operacional será capaz de dimensionar automaticamente o volume de tráfego esperado em determinada rede, diferenciando um volume de tráfego legítimo de outro anormal. É o administrador quem deve definir tais limites. Acredite, inúmeros valores sysctl serão exatamente os mesmos em instalações desktop ou servidor.

Para evitar ataques de synflood (por exemplo), o kernel implementa um mecanismo de proteção local conhecido como tcp_syncookie. Há algum tempo atrás, vi uma discussão em que engenheiros de rede questionavam esta implementação, afirmando que o controle fere padrões estabelecidos por RFCs. Discordo da abordagem dada porque o controle syncookie atua apenas quando a fila backlog (tcp_max_syn_backlog) é excedida. Logo, o ideal é manter a proteção ativa e ampliar o tamanho da fila de conexões solicitadas e pendentes (half-open).

O valor padrão de tcp_max_syn_backlog costuma variar entre 128 ou 256 (dependendo da versão do kernel). É um valor muito baixo para um perfil de instalação servidor. Em redes de pequeno porte pode ser interessante alterar este valor para 2048. Sob tráfego muito intenso, comum em redes de grande porte, este valor pode beirar 10000 tranquilamente. Mas, seja coerente, pois, ao extrapolar demais, o servidor ficará realmente exposto à ataques de negação de serviço. Recomendo não exceder 30000.

O kernel também controla o número máximo de solicitações de conexões enfileiradas para qualquer socket em estado LISTEN (OUÇA) via parâmetro somaxconn. Ou seja, limita o número máximo de requisições simultâneas negociadas com o servidor. Novamente, o valor padrão, definido como 128, é muito baixo. Quando o fluxo de conexões de entrada exceder este limite, as requisições seguintes serão descartadas. Costumo redefinir para 4000.

Para otimizar a utilização dos sockets, liberando novos rapidamente, pode ser interessante reduzir o timeout de sockets em estado TIME_WAIT (aguardando para finalizar). O valor padrão corresponde a 60 (em segundos). Para servidores com volume de tráfego muito alto, costumo diminuir para 20. A ativação do tcp_tw_reuse pode ser desejável em ambientes com volume de conexões muito próximo do total suportado (próximo de 60000), mas oferece alguns riscos (podendo criar instabilidades com NAT).

Visando fixar as alterações permanentemente, modifique o arquivo /etc/sysctl.conf:

vm.swappiness = 20

#net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 20

net.ipv4.tcp_max_syn_backlog = 10000
net.core.netdev_max_backlog = 20000
net.core.somaxconn = 4000

Para recarregar as definições fixadas em /etc/sysctl.conf:

root@proxysrv:~# sysctl -p

Após ampliar a capacidade para tratar um volume de conexões expressivamente maior, é importante otimizar a capacidade de processamento do Squid também. Para fazer isto, podemos modificar a configuração para trabalhar com múltiplos workers.

Cada worker em execução pode ser mapeado para uma CPU específica (via cpu_affinity), processando novas requisições HTTP exclusivamente até finalizar. Ao trabalhar com mais de um núcleo ou processador, ampliamos a capacidade de processamento do proxy.

Os workers podem compartilhar os sockets definidos em http_port, porém não compartilham, entre si, as transações internas. Sendo assim, é preciso redefinir a utilização de cache em disco por worker.

Confiram um exemplo de configuração (em /etc/squid/squid.conf):

cache_mem 512 MB
...

workers 2
cpu_affinity_map process_numbers=1,2 cores=1,2

memory_cache_shared on

if ${process_number} = 1
   cache_dir ufs /var/spool/squid/squid1/cache1 7000 32 256 min-size=64000 max-size=1024000
   cache_dir ufs /var/spool/squid/squid1/cache2 29000 16 256 min-size=1024001
endif

if ${process_number} = 2
   cache_dir ufs /var/spool/squid/squid2/cache1 7000 32 256 min-size=64000 max-size=1024000
   cache_dir ufs /var/spool/squid/squid2/cache2 29000 16 256 min-size=1024001
endif

Uma alternativa seria trabalhar com uma única definição de cache_dir do tipo rock (ficarei devendo, pois ainda não implementei).

Antes de reiniciar o serviço, certifique-se de que o script de inicialização prevê as permissões de acesso corretas ao diretório SPOOL/run/squid (ou faça manualmente) e, em seguida, recrie a estrutura de diretórios de swap (conforme exemplificado no início do artigo).

De acordo com as opções de compilação (demonstradas na primeira parte), o script de inicialização (em /etc/init.d/squid) deve assegurar a seguinte estrutura de diretórios e permissões:

  mkdir -p /var/spool/squid/run/squid
  chown -R proxy.proxy /var/spool/squid/run
  chmod g+w /var/spool/squid
  chmod g+w -R /var/spool/squid/run

Na configuração do Squid, também existem opções que otimizam ainda mais o tratamento ou controle de conexões recebidas e operações de I/O.

Através da opção “accept_filter data“, o socket do Squid pode ser configurado de forma que o sistema só encaminhe ao serviço conexões que contenham dados para processamento.

Por questões de segurança, habilitar a opção client_db, para definir um limite máximo de conexões por endereço de origem (client_ip_max_connections), é uma medida desejável, mesmo demandando mais processamento. Em redes de pequeno porte, este custo adicional pode não justificar.

Com um volume de conexões maior, a otimização no tratamento dos eventos de log passa a ser importante também. Para diminuir o número de operações de I/O, podemos habilitar a opção buffered_logs. É evidente que, em função do atraso causado intencionalmente na escrita dos logs, o servidor ficará mais exposto a perda de dados.

O tempo máximo que um cliente (navegador) pode permanecer conectado ao serviço é definido por client_lifetime. Esta configuração protege o servidor de ter inúmeros sockets em estado CLOSE_WAIT sem encerrar corretamente. O valor padrão é de 1440 minutos (um dia). A documentação do Squid recomenda alterar este arquivo apenas como último recurso. Ainda assim, tenho diminuído para 30 minutos.

Por exemplo (em /etc/squid/squid.conf):

http_port 8080
htcp_port 0
accept_filter data
retry_on_error on
connect_retries 3
persistent_connection_after_error off
buffered_logs on

visible_hostname proxysrv.domain.local
shutdown_lifetime 5 seconds
pid_filename /var/run/squid.pid
coredump_dir /var/spool/squid/
logfile_rotate 0

httpd_suppress_version_string on
error_directory /usr/share/squid/errors/Portuguese

client_db on
client_ip_max_connections 200
client_lifetime 30 minutes

Acredito que o básico e fundamental foi tratado. Os ajustes envolvendo a autenticação de usuários serão tratados na próxima parte (3). O assunto prolongou muito mais que imaginei inicialmente. Futuramente, em outro artigo mais específico, demonstrarei exemplos com balanceamento de carga (LVS ou HaProxy).