Tentativa e erro ao escolher o proxy reverso HTTP

Olá pessoal!

Hoje, queremos falar sobre como a equipe do serviço de reservas de hotéis Ostrovok.ru resolveu o problema do crescimento de microsserviços, cuja tarefa é trocar informações com nossos fornecedores. Sobre sua experiência diz imortal , DevOps Team Lead em Ostrovok.ru.


No início, o microsserviço era pequeno e executava as seguintes funções:

  • aceitar uma solicitação de um serviço local;
  • faça uma solicitação a um parceiro;
  • normalizar a resposta;
  • retorne o resultado ao serviço de consulta.

No entanto, com o tempo, o serviço cresceu junto com o número de parceiros e solicitações a eles.

À medida que o serviço cresceu, vários tipos de problemas começaram a surgir. Diferentes fornecedores apresentam suas próprias regras: alguém limita o número máximo de conexões, alguém restringe os clientes às listas brancas.

Como resultado, tivemos que resolver os seguintes problemas:

  • é desejável ter vários endereços IP externos fixos para que você possa fornecê-los aos parceiros para adicioná-los às listas brancas,
  • ter um único pool de conexões com todos os fornecedores para que, ao escalar nosso microsserviço, o número de conexões permaneça mínimo,
  • encerre o SSL e mantenha a atividade keepalive em um só lugar, reduzindo assim a carga nos próprios parceiros.

Eles não pensaram por um longo tempo e imediatamente se perguntaram o que escolher: Nginx ou Haproxy.
No começo, o pêndulo girava em direção ao Nginx, pois resolvi a maioria dos problemas associados ao HTTP / HTTPS com sua ajuda e sempre fiquei satisfeito com o resultado.

O esquema era simples: uma solicitação foi feita ao nosso novo servidor proxy no Nginx com um domínio no formato <partner_tag>.domain.local , no Nginx havia um map que <partner_tag> correspondia ao endereço do parceiro. Um endereço foi retirado do map e proxy_pass foi feito para esse endereço.

Aqui está um exemplo de map com o qual analisamos o domínio e selecionamos o montante da lista:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } 

E aqui está snippet.d/upstreams_map aparência de " snippet.d/upstreams_map ":
 “one” “http://one.domain.net”; “two” “https://two.domain.org”; 

Aqui nós temos o server{} :

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_address$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

Tudo é legal, tudo funciona. É possível terminar este artigo, se não for por uma nuance.

Ao usar proxy_pass, a solicitação vai diretamente para o endereço desejado, como regra, usando o protocolo HTTP / 1.0 sem keepalive e fecha imediatamente após a resposta ser concluída. Mesmo se proxy_http_version 1.1 , nada será alterado sem o upstream ( proxy_http_version ).

O que fazer O primeiro pensamento é colocar todos os fornecedores no upstream, onde o servidor será o endereço do fornecedor de que precisamos e, no map mantenha "tag" "upstream_name" .

Adicione outro map para analisar o esquema:

 ###     : <tag>.domain.local map $http_host $upstream_prefix { default 0; "~^([^\.]+)\." $1; } ###      map $upstream_prefix $upstream_address { include snippet.d/upstreams_map; default http://127.0.0.1:8080; } ###   upstream_host    upstream_address map $upstream_address $upstream_host { default 0; "~^https?://([^:]+)" $1; } ###   ,       https,    ,    -  http map $upstream_address $upstream_scheme { default "http://"; "~(https?://)" $1; } 

E crie upstreams com nomes de tags:
  upstream one { keepalive 64; server one.domain.com; } upstream two { keepalive 64; server two.domain.net; } 

O servidor em si é ligeiramente modificado para levar em conta o esquema e usar o nome do upstream em vez do endereço:

 server { listen 80; location / { proxy_http_version 1.1; proxy_pass $upstream_scheme$upstream_prefix$request_uri; proxy_set_header Host $upstream_host; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Proto ""; } } # service for error handling and logging server { listen 127.0.0.1:8080; location / { return 400; } location /ngx_status/ { stub_status; } } 

Ótimo. A solução funciona, adicione a diretiva keepalive em cada montante, defina proxy_http_version 1.1 , - agora temos um pool de conexões e tudo funciona como deveria.

Desta vez, você pode definitivamente terminar o artigo e tomar um chá. Ou não?

De fato, enquanto tomamos chá, um dos fornecedores pode alterar o endereço IP ou o grupo de endereços no mesmo domínio (oi, Amazon), assim um dos fornecedores pode cair no auge da festa do chá.

Bem, o que fazer? O Nginx tem uma nuance interessante: durante a recarga, ele pode aumentar os servidores internos do upstream para novos endereços e colocar tráfego neles. Em geral, também uma solução. Jogue no cron reload nginx cada 5 minutos e continue tomando chá.

Mas ainda assim me pareceu uma decisão mais ou menos, então comecei a olhar desconfiado para Haproxy.

O Haproxy tem a capacidade de especificar dns resolvers e configurar o dns cache . Portanto, o Haproxy atualizará o dns cache do dns cache se as entradas nele expirarem e substituirá os endereços do upstream se eles tiverem sido alterados.

Ótimo! Agora cabe às configurações.

Aqui está um pequeno exemplo de configuração para o Haproxy:

 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 resolvers dns hold valid 1s timeout retry 100ms nameserver dns1 1.1.1.1:53 backend one http-request set-header Host one.domain.com server one--one.domain.com one.domain.com:80 resolvers dns check backend two http-request set-header Host two.domain.net server two--two.domain.net two.domain.net:443 resolvers dns check ssl verify none check-sni two.domain.net sni str(two.domain.net) 

Tudo parece funcionar como deveria desta vez. A única coisa que eu não gosto no Haproxy é a complexidade da descrição da configuração. Você precisa criar muito texto para adicionar um trabalho a montante. Mas a preguiça é o motor do progresso: se você não quiser escrever a mesma coisa, escreva um gerador.

Eu já tinha um mapa do Nginx com o formato "tag" "upstream" , então decidi tomá-lo como base, analisar e gerar um back-end haproxy com base nesses valores.

 #! /usr/bin/env bash haproxy_backend_map_file=./root/etc/haproxy/snippet.d/name_domain_map haproxy_backends_file=./root/etc/haproxy/99_backends.cfg nginx_map_file=./nginx_map while getopts 'n:b:m:' OPT;do case ${OPT} in n) nginx_map_file=${OPTARG} ;; b) haproxy_backends_file=${OPTARG} ;; m) haproxy_backend_map_file=${OPTARG} ;; *) echo 'Usage: ${0} -n [nginx_map_file] -b [haproxy_backends_file] -m [haproxy_backend_map_file]' exit esac done function write_backend(){ local tag=$1 local domain=$2 local port=$3 local server_options="resolvers dns check" [ -n "${4}" ] && local ssl_options="ssl verify none check-sni ${domain} sni str(${domain})" [ -n "${4}" ] && server_options+=" ${ssl_options}" cat >> ${haproxy_backends_file} <<EOF backend ${tag} http-request set-header Host ${domain} server ${tag}--${domain} ${domain}:${port} ${server_options} EOF } :> ${haproxy_backends_file} :> ${haproxy_backend_map_file} while read tag addr;do tag=${tag//\"/} [ -z "${tag:0}" ] && continue [ "${tag:0:1}" == "#" ] && continue IFS=":" read scheme domain port <<<${addr//;} unset IFS domain=${domain//\/} case ${scheme} in http) port=${port:-80} write_backend ${tag} ${domain} ${port} ;; https) port=${port:-443} write_backend ${tag} ${domain} ${port} 1 esac done < <(sort -V ${nginx_map_file}) 

Agora tudo o que precisamos é adicionar um novo host no nginx_map, iniciar o gerador e obter a configuração haproxy pronta.

Provavelmente é tudo por hoje. Este artigo se refere mais ao introdutório e foi dedicado ao problema de escolher uma solução e sua integração no ambiente atual.

No próximo artigo, mostrarei mais sobre as armadilhas que encontramos ao usar o Haproxy, quais métricas se mostraram úteis para monitorar e o que exatamente deve ser otimizado no sistema para obter o máximo desempenho dos servidores.

Obrigado a todos pela atenção, até mais!

Source: https://habr.com/ru/post/pt436992/


All Articles