Versuch und Irrtum bei der Auswahl von HTTP Reverse Proxy

Hallo allerseits!

Heute möchten wir darüber sprechen, wie das Hotelreservierungsservice-Team von Ostrovok.ru das Problem des Wachstums von Mikroservices gelöst hat, dessen Aufgabe es ist, Informationen mit unseren Lieferanten auszutauschen. Über seine Erfahrung berichtet der unsterbliche DevOps-Teamleiter in Ostrovok.ru.


Der Microservice war zunächst klein und führte folgende Funktionen aus:

  • eine Anfrage von einem lokalen Dienst annehmen;
  • eine Anfrage an einen Partner stellen;
  • normalisieren Sie die Antwort;
  • Geben Sie das Ergebnis an den Abfragedienst zurück.

Mit der Zeit wuchs der Service jedoch zusammen mit der Anzahl der Partner und Anfragen an sie.

Als der Service wuchs, traten verschiedene Probleme auf. Verschiedene Lieferanten stellen ihre eigenen Regeln auf: Jemand begrenzt die maximale Anzahl von Verbindungen, jemand beschränkt Kunden auf weiße Listen.

Infolgedessen mussten wir die folgenden Probleme lösen:

  • Es ist wünschenswert, mehrere feste externe IP-Adressen zu haben, damit Sie diese Partnern zum Hinzufügen zu weißen Listen zur Verfügung stellen können.
  • haben einen einzigen Pool von Verbindungen zu allen Lieferanten, so dass bei der Skalierung unseres Microservices die Anzahl der Verbindungen minimal bleibt,
  • Beenden Sie SSL und halten Sie keepalive an einem Ort, wodurch die Belastung der Partner selbst verringert wird.

Sie dachten lange nicht nach und fragten sich sofort, was sie wählen sollten: Nginx oder Haproxy.
Zuerst schwang das Pendel in Richtung Nginx, da ich mit seiner Hilfe die meisten Probleme im Zusammenhang mit HTTP / HTTPS löste und immer mit dem Ergebnis zufrieden war.

Das Schema war einfach: Es wurde eine Anfrage an unseren neuen Proxyserver auf Nginx mit einer Domäne der Form <partner_tag>.domain.local . In Nginx gab es eine map , in der <partner_tag> der Adresse des Partners entsprach. Eine Adresse wurde von map und proxy_pass wurde an diese Adresse proxy_pass .

Hier ist eine Beispielkarte, mit der wir die Domain analysieren und den Upstream aus der Liste auswählen:

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

Und so sieht " snippet.d/upstreams_map " aus:
 “one” “http://one.domain.net”; “two” “https://two.domain.org”; 

Hier haben wir 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; } } 

Alles ist cool, alles funktioniert. Es ist möglich, diesen Artikel zu beenden, wenn nicht für eine Nuance.

Bei Verwendung von proxy_pass wird die Anforderung in der Regel unter Verwendung des HTTP / 1.0-Protokolls ohne keepalive direkt an die gewünschte Adresse gesendet und sofort nach Abschluss der Antwort geschlossen. Selbst wenn wir proxy_http_version 1.1 nichts ohne Upstream ( proxy_http_version ).

Was zu tun ist? Der erste Gedanke ist, alle Lieferanten in den Upstream zu bringen, wobei der Server die Adresse des Lieferanten ist, den wir benötigen, und in der map "tag" "upstream_name" behalten.

Fügen Sie eine weitere map um das Schema zu analysieren:

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

Und erstellen Sie upstreams mit Tag-Namen:
  upstream one { keepalive 64; server one.domain.com; } upstream two { keepalive 64; server two.domain.net; } 

Der Server selbst wurde geringfügig geändert, um das Schema zu berücksichtigen und anstelle der Adresse den Namen des Upstreams zu verwenden:

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

Großartig. Die Lösung funktioniert, fügen Sie die keepalive Direktive in jedem Upstream hinzu, setzen Sie proxy_http_version 1.1 , - jetzt haben wir einen Verbindungspool und alles funktioniert wie es sollte.

Dieses Mal können Sie den Artikel definitiv beenden und Tee trinken gehen. Oder nicht?

Während wir Tee trinken, kann einer der Lieferanten die IP-Adresse oder die Adressgruppe unter derselben Domain (hi, Amazon) ändern, wodurch einer der Lieferanten auf dem Höhepunkt unserer Teeparty abfällt.

Was tun? Nginx hat eine interessante Nuance: Während des Neuladens kann es die Server im upstream an neue Adressen nüchtern machen und Datenverkehr auf sie übertragen. Im Allgemeinen auch eine Lösung. Werfen Sie cron reload nginx alle 5 Minuten und trinken Sie weiterhin Tee.

Trotzdem schien es mir eine mittelmäßige Entscheidung zu sein, und so begann ich, Haproxy schief anzusehen.

Haproxy kann DNS- dns resolvers angeben und den dns cache konfigurieren. Daher aktualisiert Haproxy den dns cache wenn die darin enthaltenen Einträge abgelaufen sind, und ersetzt Adressen für Upstream, wenn sie sich geändert haben.

Großartig! Jetzt liegt es an den Einstellungen.

Hier ist ein kurzes Konfigurationsbeispiel für 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) 

Diesmal scheint alles so zu funktionieren, wie es sollte. Das einzige, was ich an Haproxy nicht mag, ist die Komplexität der Konfigurationsbeschreibung. Sie müssen viel Text erstellen, um einen vorgelagerten Text hinzuzufügen. Aber Faulheit ist der Motor des Fortschritts: Wenn Sie nicht dasselbe schreiben möchten, schreiben Sie einen Generator.

Ich hatte bereits eine Karte von Nginx mit dem Format "tag" "upstream" , daher habe ich beschlossen, sie als Grundlage zu verwenden, ein Haproxy-Backend zu analysieren und basierend auf diesen Werten zu generieren.

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

Jetzt müssen wir nur noch einen neuen Host in nginx_map hinzufügen, den Generator starten und die fertige Haproxy-Konfiguration erhalten.

Das ist wahrscheinlich alles für heute. Dieser Artikel bezieht sich eher auf den einleitenden Artikel und widmete sich dem Problem der Auswahl einer Lösung und ihrer Integration in die aktuelle Umgebung.

Im nächsten Artikel werde ich ausführlicher darauf eingehen, auf welche Fallstricke wir bei der Verwendung von Haproxy gestoßen sind, welche Metriken sich für die Überwachung als nützlich erwiesen haben und welche genau im System optimiert werden sollten, um die maximale Leistung der Server zu erzielen.

Vielen Dank für Ihre Aufmerksamkeit, wir sehen uns!

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


All Articles