Olá novamente!
Na última vez, conversamos sobre a escolha de uma ferramenta no
Ostrovok.ru para solucionar o problema de proxy de um grande número de solicitações para serviços externos, sem colocar ninguém ao mesmo tempo. O artigo terminou com uma seleção de
Haproxy . Hoje vou compartilhar as nuances que tive que enfrentar ao usar esta solução.

Configuração Haproxy
A primeira dificuldade foi que a opção
maxconn
é diferente dependendo do contexto:
Por hábito, ajustei apenas a primeira opção (
performance tuning
). Aqui está o que a documentação diz sobre esta opção:
Define o número máximo por processo de conexões simultâneas como <número>. Isso
é equivalente ao argumento da linha de comandos "-n". Proxies deixarão de aceitar
conexões quando esse limite é atingido.
Parece que o que é necessário. No entanto, quando me deparei com o fato de que novas conexões com o proxy não foram imediatamente, comecei a ler a documentação com mais cuidado e já encontrei o segundo parâmetro (
bind options
):
Limita os soquetes a esse número de conexões simultâneas. Estranho
conexões permanecerão na lista de pendências do sistema até que uma conexão seja
lançado. Se não especificado, o limite será o mesmo que o maxconn do frontend.
Então,
frontends maxconn
lá, procure
frontends maxconn
:
Corrija o número máximo de conexões simultâneas em um frontend
...
Por padrão, esse valor é definido como 2000.
Ótimo, o que você precisa. Adicione à configuração:
global daemon maxconn 524288 ... defaults mode http maxconn 524288
A próxima piada foi que o Haproxy é de rosca única. Estou muito acostumado com o modelo no Nginx, então essa nuance sempre me deprimiu. Mas não se desespere -
Willy Tarreau (
desenvolvedor do Haproxy ) entendeu o que estava fazendo, então acrescentou a opção -
nbproc
.
No entanto, diretamente na documentação, diz:
USANDO VÁRIOS PROCESSOS
É MAIS DIFÍCIL DE DEBUGAR E É REALMENTE DESCOBERTA.
Esta opção pode realmente causar dor de cabeça nos casos, se você precisar:
- limitar o número de solicitações / conexões com servidores (já que você já não terá um processo com um contador, mas muitos processos e cada um terá seu próprio contador);
- Coletar estatísticas do soquete de gerenciamento Haproxy
- ativar / desativar back-end através do soquete de controle;
- ... talvez outra coisa. ¯ \ _ (ツ) _ / ¯
No entanto, os deuses nos deram processadores com vários núcleos, então eu gostaria de usá-los ao máximo. No meu caso, havia quatro núcleos em dois núcleos físicos. Para o Haproxy, selecionei o primeiro núcleo, e ficou assim:
nbproc 4 cpu-map 1 0 cpu-map 2 1 cpu-map 3 2 cpu-map 4 3
Usando o
cpu-map, vinculamos os processos Haproxy a um núcleo específico. O agendador do SO não precisa mais pensar em onde planejar o Haproxy, mantendo assim a
content switch
fria e o cache da CPU quente.
Existem muitos buffers, mas não no nosso caso
- tune.bufsize - no nosso caso, não era necessário executá-lo, mas se você tiver erros com o código
400 (Bad Request)
incorreta 400 (Bad Request)
, provavelmente esse é o seu caso. - tune.http.cookielen - se você distribuir cookies grandes para os usuários, para evitar danos durante a transmissão pela rede, pode fazer sentido aumentar esse buffer também.
- tune.http.maxhdr é outra fonte possível de 400 códigos de resposta se você tiver muitos cabeçalhos.
Agora considere as coisas de nível mais baixo
tune.rcvbuf.client /
tune.rcvbuf.server ,
tune.sndbuf.client /
tune.sndbuf.server - a documentação diz o seguinte:
Normalmente, nunca deve ser definido, e o tamanho padrão (0) permite que o kernel ajuste automaticamente esse valor, dependendo da quantidade de memória disponível.
Mas, para mim, o óbvio é melhor que o implícito, então forcei os valores dessas opções a ter certeza de amanhã.
E outro parâmetro que não está relacionado aos buffers, mas muito importante é o
tune.maxaccept .
Define o número máximo de conexões consecutivas que um processo pode aceitar em um
linha antes de mudar para outro trabalho. No modo de processo único, números mais altos
oferece melhor desempenho com altas taxas de conexão. No entanto, em processos múltiplos
manter um pouco de justiça entre os processos geralmente é melhor
aumentar o desempenho.
No nosso caso, muitas solicitações de proxy são geradas, então eu criei esse valor para aceitar mais solicitações por vez. No entanto, como diz a documentação, vale a pena testar se, no modo multithread, a carga é distribuída da maneira mais uniforme possível entre os processos.
Todos os parâmetros juntos:
tune.bufsize 16384 tune.http.cookielen 63 tune.http.maxhdr 101 tune.maxaccept 256 tune.rcvbuf.client 33554432 tune.rcvbuf.server 33554432 tune.sndbuf.client 33554432 tune.sndbuf.server 33554432
O que nunca acontece são tempos limite. O que faríamos sem eles?
- timeout connect - tempo para estabelecer uma conexão com o back-end. Se a conexão com o back-end não for muito boa, é melhor desativá-la com esse tempo limite até que a rede volte ao normal.
- timeout client - timeout para a transmissão dos primeiros bytes de dados. Ajuda a desconectar aqueles que fazem solicitações "reservadas".
Kulstory sobre o cliente HTTP no GoO Go possui um cliente HTTP comum que pode manter um conjunto de conexões com servidores. Então, aconteceu uma história interessante, na qual o tempo limite e o conjunto de conexões descritos acima no cliente HTTP participaram. Uma vez que um desenvolvedor reclamou que ele periodicamente tem 408 erros de um proxy. Examinamos o código do cliente e vimos a seguinte lógica:
- Estamos tentando obter uma conexão estabelecida gratuita da piscina;
- se não der certo, inicie a instalação de uma nova conexão na goroutine;
- verifique a piscina novamente;
- se houver de graça na piscina - nós a pegamos e colocamos a nova na piscina, se não - use a nova.
Já entendeu o que é o sal?
Se o cliente estabeleceu uma nova conexão, mas não a usou, depois de cinco segundos o servidor a fecha e o caso termina. O cliente captura isso apenas quando já obtém a conexão do pool e tenta usá-la. Vale a pena ter isso em mente.
- servidor de tempo limite - o tempo máximo para aguardar uma resposta do servidor.
- timeout client-fin / timeout server-fin - aqui nos protegemos de conexões semi-fechadas para não acumulá-las na tabela do sistema operacional.
- timeout http-request é um dos tempos limite mais adequados. Permite cortar clientes lentos que não podem fazer uma solicitação HTTP no tempo alocado para eles.
- tempo limite http-keep-alive - especificamente no nosso caso, se uma conexão
keep-alive
travar sem solicitações por mais de 50 segundos, provavelmente algo deu errado e a conexão pode ser fechada, liberando memória para algo novo luz.
Todos os tempos limite juntos:
defaults mode http maxconn 524288 timeout connect 5s timeout client 10s timeout server 120s timeout client-fin 1s timeout server-fin 1s timeout http-request 10s timeout http-keep-alive 50s
Registo Por que isso é tão difícil?
Como escrevi anteriormente, na maioria das vezes nas minhas decisões uso o Nginx, portanto, sou estragado por sua sintaxe e pela simplicidade de modificar os formatos de log. Eu gostei especialmente dos registros matadores de formato de recurso na forma de json e depois analisá-los com qualquer biblioteca padrão.
O que temos no Haproxy? Existe uma oportunidade, apenas você pode escrever exclusivamente no syslog e a sintaxe da configuração é um pouco mais encapsulada.
Vou dar um exemplo de configuração com comentários:
Dor particular é causada por esses momentos:
- nomes curtos de variáveis e, especialmente, suas combinações como% HU ou% fp
- o formato não pode ser dividido em várias linhas; portanto, você deve escrever um calçado em uma linha. difícil adicionar / remover itens novos / desnecessários
- para que algumas variáveis funcionem, elas devem ser declaradas explicitamente através do cabeçalho da solicitação de captura
Como resultado, para obter algo interessante, você precisa ter apenas um calçado assim:
log-format '{"status":"%ST","bytes_read":"%B","bytes_uploaded":"%U","hostname":"%H","method":"%HM","request_uri":"%HU","handshake_time":"%Th","request_idle_time":"%Ti","request_time":"%TR","response_time":"%Tr","timestamp":"%Ts","client_ip":"%ci","client_port":"%cp","frontend_port":"%fp","http_request":"%r","ssl_ciphers":"%sslc","ssl_version":"%sslv","date_time":"%t","http_host":"%[capture.req.hdr(0)]","http_referer":"%[capture.req.hdr(1)]","http_user_agent":"%[capture.req.hdr(2)]"}'
Bem, ao que parece, pequenas coisas, mas bom
Descrevi o formato do log acima, mas não tão simples. Para depositar alguns elementos nele, como:
- http_host
- http_referer,
- http_user_agent,
primeiro, você precisa capturar esses dados da solicitação (
captura ) e colocá-los em uma matriz de valores capturados.
Aqui está um exemplo:
capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128
Como resultado, agora podemos acessar os elementos que precisamos desta maneira:
%[capture.req.hdr(N)]
, em que N é o número de sequência da definição do grupo de captura.
No exemplo acima, o cabeçalho Host estará no número 0 e o User Agent estará no número 2.
O Haproxy tem uma peculiaridade: resolve os endereços DNS dos back-ends na inicialização e, se não conseguir resolver nenhum dos endereços, cai a morte dos corajosos.
No nosso caso, isso não é muito conveniente, já que existem muitos back-ends, não os gerenciamos e é melhor obter o 503 do Haproxy do que todo o servidor proxy se recusará a iniciar por causa de um provedor. A opção a seguir nos ajuda com isso:
init-addr .
Uma linha retirada diretamente da documentação nos permite passar por todos os métodos disponíveis para resolver um endereço e, no caso de um arquivo, adiar esse assunto para mais tarde e seguir em frente:
default-server init-addr last,libc,none
E, finalmente, o meu favorito: seleção de back-end.
A sintaxe da configuração de seleção de back-end do Haproxy é familiar a todos:
use_backend <backend1_name> if <condition1> use_backend <backend2_name> if <condition2> default-backend <backend3>
Mas, palavra certa, de alguma forma não é muito. Já descrevi todos os back-ends de maneira automatizada (consulte o
artigo anterior ), também seria possível gerar
use_backend
aqui, os negócios ruins não são complicados, mas eu não queria. Como resultado, foi encontrada outra maneira:
capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128 # host_present Host acl host_present hdr(host) -m len gt 0 # , use_backend %[req.hdr(host),lower,field(1,'.')] if host_present # , default_backend default backend default mode http server no_server 127.0.0.1:65535
Assim, padronizamos os nomes de back-end e URLs pelos quais você pode acessá-los.
Bem, agora compilando os exemplos acima em um arquivo:
Versão completa da configuração global daemon maxconn 524288 nbproc 4 cpu-map 1 0 cpu-map 2 1 cpu-map 3 2 cpu-map 4 3 tune.bufsize 16384 tune.comp.maxlevel 1 tune.http.cookielen 63 tune.http.maxhdr 101 tune.maxaccept 256 tune.rcvbuf.client 33554432 tune.rcvbuf.server 33554432 tune.sndbuf.client 33554432 tune.sndbuf.server 33554432 stats socket /run/haproxy.sock mode 600 level admin log /dev/stdout local0 debug defaults mode http maxconn 524288 timeout connect 5s timeout client 10s timeout server 120s timeout client-fin 1s timeout server-fin 1s timeout http-request 10s timeout http-keep-alive 50s default-server init-addr last,libc,none log 127.0.0.1:2514 len 8192 local1 notice emerg log 127.0.0.1:2514 len 8192 local7 info log-format '{"status":"%ST","bytes_read":"%B","bytes_uploaded":"%U","hostname":"%H","method":"%HM","request_uri":"%HU","handshake_time":"%Th","request_idle_time":"%Ti","request_time":"%TR","response_time":"%Tr","timestamp":"%Ts","client_ip":"%ci","client_port":"%cp","frontend_port":"%fp","http_request":"%r","ssl_ciphers":"%sslc","ssl_version":"%sslv","date_time":"%t","http_host":"%[capture.req.hdr(0)]","http_referer":"%[capture.req.hdr(1)]","http_user_agent":"%[capture.req.hdr(2)]"}' frontend http bind *:80 http-request del-header X-Forwarded-For http-request del-header X-Forwarded-Port http-request del-header X-Forwarded-Proto capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128 acl host_present hdr(host) -m len gt 0 use_backend %[req.hdr(host),lower,field(1,'.')] if host_present default_backend default backend default mode http server no_server 127.0.0.1:65535 resolvers dns hold valid 1s timeout retry 100ms nameserver dns1 127.0.0.1:53
Obrigado a quem leu até o fim. No entanto, isso não é tudo.
Da próxima vez, examinaremos as questões de nível inferior relacionadas à otimização do próprio sistema, no qual o Haproxy funciona, para que ele e nosso sistema operacional se sintam confortáveis juntos, e haja ferro suficiente para todos.
Até breve!