Essai et erreur lors du choix du proxy inverse HTTP

Bonjour à tous!

Aujourd'hui, nous voulons parler de la manière dont l'équipe du service de réservation d'hôtels Ostrovok.ru a résolu le problème de la croissance des microservices, dont la tâche est d'échanger des informations avec nos fournisseurs. À propos de son expérience raconte éternel , DevOps Team Lead in Ostrovok.ru.


Au début, le microservice était petit et remplissait les fonctions suivantes:

  • accepter une demande d'un service local;
  • faire une demande à un partenaire;
  • normaliser la réponse;
  • renvoyer le résultat au service d'interrogation.

Cependant, le temps a passé, le service a grandi avec le nombre de partenaires et de demandes.

À mesure que le service se développait, divers types de problèmes ont commencé à apparaître. Différents fournisseurs proposent leurs propres règles: quelqu'un limite le nombre maximum de connexions, quelqu'un limite les clients à des listes blanches.

En conséquence, nous avons dû résoudre les problèmes suivants:

  • il est souhaitable de disposer de plusieurs adresses IP externes fixes afin de pouvoir les fournir à des partenaires pour les ajouter aux listes blanches,
  • disposer d'un pool unique de connexions avec tous les fournisseurs afin que lors de la mise à l'échelle de notre microservice, le nombre de connexions reste minimal,
  • mettre fin à SSL et garder keepalive en un seul endroit, réduisant ainsi la charge sur les partenaires eux-mêmes.

Ils n'ont pas réfléchi longtemps et se sont immédiatement demandé quoi choisir: Nginx ou Haproxy.
Au début, le pendule a basculé vers Nginx, car j'ai résolu la plupart des problèmes associés à HTTP / HTTPS avec son aide et j'ai toujours été satisfait du résultat.

Le schéma était simple: une demande a été faite à notre nouveau serveur proxy sur Nginx avec un domaine de la forme <partner_tag>.domain.local , dans Nginx il y avait une map<partner_tag> correspondait à l'adresse du partenaire. Une adresse a été extraite de la map et proxy_pass été créé pour cette adresse.

Voici un exemple de map avec laquelle nous analysons le domaine et sélectionnons l'amont dans la liste:

 ###     : <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; } 

Et voici à quoi ressemble « snippet.d/upstreams_map »:
 “one” “http://one.domain.net”; “two” “https://two.domain.org”; 

Ici, nous avons le server{} - 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; } } 

Tout est cool, tout fonctionne. Il est possible de terminer cet article, sinon pour une nuance.

Lorsque vous utilisez proxy_pass, la demande va directement à l'adresse souhaitée, en règle générale, en utilisant le protocole HTTP / 1.0 sans keepalive et se ferme immédiatement une fois la réponse terminée. Même si nous proxy_http_version 1.1 , rien ne changera sans en amont ( proxy_http_version ).

Que faire La première pensée est de mettre tous les fournisseurs en amont, où le serveur sera l'adresse du fournisseur dont nous avons besoin, et dans la map garder "tag" "upstream_name" .

Ajoutez une autre map pour analyser le schéma:

 ###     : <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; } 

Et créez des upstreams avec des noms de balises:
  upstream one { keepalive 64; server one.domain.com; } upstream two { keepalive 64; server two.domain.net; } 

Le serveur lui-même est légèrement modifié pour prendre en compte le schéma et utiliser le nom de l'amont au lieu de l'adresse:

 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; } } 

Super. La solution fonctionne, ajoutez la directive keepalive à chaque amont, définissez proxy_http_version 1.1 , - nous avons maintenant un pool de connexions, et tout fonctionne comme il se doit.

Cette fois, vous pouvez certainement terminer l'article et aller boire du thé. Ou pas?

Après tout, pendant que nous buvons du thé, l'un des fournisseurs peut changer l'adresse IP ou le groupe d'adresses sous le même domaine (salut, Amazon), ainsi l'un des fournisseurs peut tomber à la hauteur même de notre thé.

Eh bien, que faire? Nginx a une nuance intéressante: lors du rechargement, il peut dégriser les serveurs à l'intérieur en upstream vers de nouvelles adresses et y mettre du trafic. En général, aussi une solution. Jetez cron reload nginx toutes les 5 minutes et continuez à boire du thé.

Mais cela me semblait encore une décision médiocre, alors j'ai commencé à regarder avec mépris vers Haproxy.

Haproxy a la possibilité de spécifier des dns resolvers et de configurer le dns cache . Ainsi, Haproxy mettra à jour le dns cache - dns cache si les entrées qu'il contient ont expiré et remplacera les adresses en amont si elles ont changé.

Super! Maintenant, c'est aux paramètres.

Voici un court exemple de configuration pour 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) 

Tout semble fonctionner comme il se doit cette fois. La seule chose que je n'aime pas dans Haproxy est la complexité de la description de la configuration. Vous devez créer beaucoup de texte pour en ajouter un fonctionnant en amont. Mais la paresse est le moteur du progrès: si vous ne voulez pas écrire la même chose, écrivez un générateur.

J'avais déjà une carte de Nginx avec le format "tag" "upstream" , j'ai donc décidé de la prendre comme base, d'analyser et de générer un backend haproxy basé sur ces valeurs.

 #! /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}) 

Maintenant, tout ce dont nous avons besoin est d'ajouter un nouvel hôte dans nginx_map, de démarrer le générateur et d'obtenir la configuration haproxy prête.

C'est probablement tout pour aujourd'hui. Cet article se réfère plus à l'introduction et a été consacré au problème du choix d'une solution et de son intégration dans l'environnement actuel.

Dans le prochain article, je parlerai plus en détail des pièges que nous avons rencontrés lors de l'utilisation de Haproxy, des mesures qu'il s'est avéré utile de surveiller et de ce qui devrait être optimisé exactement dans le système afin d'obtenir les performances maximales des serveurs.

Merci à tous pour votre attention, à bientôt!

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


All Articles