Hallo nochmal!
Beim letzten Mal haben wir darüber gesprochen, in
Ostrovok.ru ein Tool auszuwählen, mit dem das Problem gelöst werden kann, dass eine große Anzahl von Anfragen an externe Dienste weitergeleitet wird, ohne dass gleichzeitig jemand eingestellt wird. Der Artikel endete mit einer Auswahl von
Haproxy . Heute werde ich die Nuancen teilen, mit denen ich bei der Verwendung dieser Lösung konfrontiert war.

Haproxy-Konfiguration
Die erste Schwierigkeit bestand darin, dass die Option
maxconn
je nach Kontext unterschiedlich ist:
Aus Gewohnheit habe ich nur die erste Option (
performance tuning
) eingestellt. In der Dokumentation zu dieser Option heißt es:
Legt die maximale Anzahl gleichzeitiger Verbindungen pro Prozess auf <Nummer> fest. Es
entspricht dem Befehlszeilenargument "-n". Proxies werden nicht mehr akzeptiert
Verbindungen, wenn diese Grenze erreicht ist.
Es scheint, dass das, was benötigt wird. Als ich jedoch auf die Tatsache stieß, dass neue Verbindungen zum Proxy nicht sofort hergestellt wurden, begann ich, die Dokumentation genauer zu lesen, und dort fand ich bereits den zweiten Parameter (
bind options
):
Begrenzt die Sockets auf diese Anzahl gleichzeitiger Verbindungen. Fremd
Verbindungen bleiben im Rückstand des Systems, bis eine Verbindung hergestellt wird
freigegeben. Wenn nicht angegeben, entspricht das Limit dem maxconn des Frontends.
Also,
frontends maxconn
gehen und dann nach
frontends maxconn
suchen
frontends maxconn
:
Legen Sie die maximale Anzahl gleichzeitiger Verbindungen auf einem Frontend fest
...
Standardmäßig ist dieser Wert auf 2000 eingestellt.
Großartig, was du brauchst. Zur Konfiguration hinzufügen:
global daemon maxconn 524288 ... defaults mode http maxconn 524288
Der nächste Knebel war, dass Haproxy Single-Threaded ist. Ich bin sehr an das Modell in Nginx gewöhnt, daher hat mich diese Nuance immer deprimiert. Aber verzweifeln Sie nicht -
Willy Tarreau (
Haproxy-Entwickler ) verstand, was er tat, und fügte die Option hinzu -
nbproc
.
In der Dokumentation heißt es jedoch direkt:
VERWENDUNG MEHRERER PROZESSE
Ist schwerer zu debuggen und ist wirklich entmutigt.
Diese Option kann in Fällen, in denen Sie Folgendes benötigen, wirklich Kopfschmerzen verursachen:
- Begrenzen Sie die Anzahl der Anforderungen / Verbindungen zu Servern (da Sie bereits nicht einen Prozess mit einem Zähler, sondern viele Prozesse haben und jeder seinen eigenen Zähler hat).
- Sammeln Sie Statistiken aus dem Haproxy-Verwaltungssocket
- Backends über die Steuerbuchse aktivieren / deaktivieren;
- ... vielleicht noch etwas. ¯ \ _ (ツ) _ / ¯
Trotzdem haben uns die Götter Multi-Core-Prozessoren gegeben, deshalb möchte ich sie maximal nutzen. In meinem Fall gab es vier Kerne in zwei physischen Kernen. Für Haproxy habe ich den ersten Kern ausgewählt und es sah so aus:
nbproc 4 cpu-map 1 0 cpu-map 2 1 cpu-map 3 2 cpu-map 4 3
Mit
cpu-map binden wir Haproxy-Prozesse an einen bestimmten Kern. Der OS-Scheduler muss nicht mehr darüber nachdenken, wo er Haproxy planen soll, wodurch der
content switch
kühl und der CPU-Cache warm bleibt.
Es gibt viele Puffer, aber in unserem Fall nicht
- tune.bufsize - In unserem Fall war es nicht erforderlich, es auszuführen. Wenn Sie jedoch Fehler mit dem Code
400 (Bad Request)
, ist dies wahrscheinlich Ihr Fall. - tune.http.cookielen - Wenn Sie große Cookies an Benutzer verteilen, kann es sinnvoll sein, diesen Puffer ebenfalls zu erhöhen, um Schäden während der Übertragung über das Netzwerk zu vermeiden.
- tune.http.maxhdr ist eine weitere mögliche Quelle für 400 Antwortcodes, wenn Sie zu viele Header haben.
Betrachten Sie nun das Zeug der unteren Ebene
tune.rcvbuf.client /
tune.rcvbuf.server ,
tune.sndbuf.client /
tune.sndbuf.server - In der Dokumentation heißt es:
Es sollte normalerweise nie festgelegt werden, und die Standardgröße (0) ermöglicht es dem Kernel, diesen Wert abhängig von der Menge des verfügbaren Speichers automatisch abzustimmen.
Aber für mich ist das Offensichtliche besser als das Implizite, deshalb habe ich die Werte dieser Optionen gezwungen, morgen sicher zu sein.
Ein weiterer Parameter, der nicht mit Puffern zusammenhängt, aber sehr wichtig ist, ist
tune.maxaccept .
Legt die maximale Anzahl aufeinanderfolgender Verbindungen fest, die ein Prozess in a akzeptieren darf
Zeile vor dem Wechsel zu anderen Arbeiten. Im Einzelprozessmodus höhere Zahlen
Bessere Leistung bei hohen Verbindungsraten. Jedoch in Multi-Prozess
Modi ist es besser, ein bisschen Fairness zwischen den Prozessen zu halten
Leistung steigern.
In unserem Fall werden ziemlich viele Proxy-Anfragen generiert, daher habe ich diesen Wert erhöht, um mehr Anfragen gleichzeitig zu akzeptieren. Wie in der Dokumentation angegeben, lohnt es sich jedoch zu testen, ob die Last im Multithread-Modus so gleichmäßig wie möglich auf die Prozesse verteilt wird.
Alle Parameter zusammen:
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
Was nie passiert, sind Auszeiten. Was würden wir ohne sie tun?
- Timeout-Verbindung - Zeit zum Herstellen einer Verbindung mit dem Backend. Wenn die Verbindung zum Backend nicht sehr gut ist, ist es besser, sie bis zu diesem Zeitpunkt zu deaktivieren, bis das Netzwerk wieder normal ist.
- Timeout-Client - Zeitüberschreitung für die Übertragung der ersten Datenbytes. Es hilft, diejenigen zu trennen, die Anfragen "in Reserve" stellen.
Kulstory über HTTP-Client in GoGo verfügt über einen regulären HTTP-Client, der einen Pool von Verbindungen zu Servern verwalten kann. So geschah eine interessante Geschichte, an der das oben beschriebene Timeout und der Verbindungspool im HTTP-Client teilnahmen. Einmal beschwerte sich ein Entwickler, dass er regelmäßig 408 Fehler von einem Proxy hat. Wir haben uns den Client-Code angesehen und dort die folgende Logik gesehen:
- Wir versuchen, eine kostenlose Verbindung aus dem Pool herzustellen.
- Wenn dies nicht funktioniert, starten Sie die Installation einer neuen Verbindung in Goroutine.
- Überprüfen Sie den Pool erneut.
- Wenn der Pool frei ist - wir nehmen ihn und legen den neuen in den Pool, wenn nicht - verwenden Sie den neuen.
Schon verstanden, was das Salz ist?
Wenn der Client eine neue Verbindung hergestellt, diese jedoch nicht verwendet hat, schließt der Server sie nach fünf Sekunden und der Fall ist beendet. Der Client fängt dies nur ab, wenn er die Verbindung bereits aus dem Pool erhält und versucht, sie zu verwenden. Es lohnt sich, dies zu berücksichtigen.
- Timeout-Server - Maximale Wartezeit auf eine Antwort vom Server.
- Timeout Client-Fin / Timeout Server-Fin - hier schützen wir uns vor halb geschlossenen Verbindungen, um sie nicht in der Betriebssystemtabelle zu akkumulieren.
- Timeout http-Anfrage ist eines der am besten geeigneten Timeouts. Ermöglicht das Abschneiden langsamer Clients, die in der ihnen zugewiesenen Zeit keine HTTP-Anforderung stellen können.
- Zeitüberschreitung http-keep-alive - speziell in unserem Fall, wenn eine
keep-alive
Verbindung länger als 50 Sekunden ohne Anforderungen hängt, ist höchstwahrscheinlich ein Fehler aufgetreten und die Verbindung kann geschlossen werden, wodurch Speicher für etwas Neues frei wird Licht.
Alle Auszeiten zusammen:
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
Protokollierung Warum ist es so kompliziert?
Wie ich bereits geschrieben habe, verwende ich bei meinen Entscheidungen meistens Nginx. Daher bin ich von seiner Syntax und der Einfachheit des Änderns von Protokollformaten verwöhnt. Ich mochte besonders die Killer-Feature-Format-Protokolle in Form von JSON, um sie dann mit jeder Standardbibliothek zu analysieren.
Was haben wir bei Haproxy? Es gibt eine solche Möglichkeit, nur Sie können ausschließlich in Syslog schreiben, und die Konfigurationssyntax ist etwas umfangreicher.
Ich gebe Ihnen eine Beispielkonfiguration mit Kommentaren:
Besondere Schmerzen werden durch solche Momente verursacht:
- kurze Variablennamen und insbesondere deren Kombinationen wie% HU oder% fp
- Das Format kann nicht in mehrere Zeilen unterteilt werden, daher müssen Sie Fußtücher in eine Zeile schreiben. Es ist schwierig, neue / unnötige Elemente hinzuzufügen / zu entfernen
- Damit einige Variablen funktionieren, müssen sie explizit über den Erfassungsanforderungsheader deklariert werden
Um etwas Interessantes zu bekommen, muss man ein solches Fußtuch haben:
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)]"}'
Nun, es scheint, kleine Dinge, aber schön
Ich habe das Format des Protokolls oben beschrieben, aber nicht so einfach. Um einige Elemente darin abzulegen, wie zum Beispiel:
- http_host
- http_referer,
- http_user_agent,
Zuerst müssen Sie diese Daten aus der Anforderung
erfassen (
erfassen ) und in ein Array von erfassten Werten einfügen.
Hier ist ein Beispiel:
capture request header Host len 32 capture request header Referer len 128 capture request header User-Agent len 128
Infolgedessen können wir jetzt auf folgende Weise auf die benötigten Elemente zugreifen:
%[capture.req.hdr(N)]
, wobei N die Sequenznummer der Capture-Gruppendefinition ist.
Im obigen Beispiel befindet sich der Host-Header auf Nummer 0 und der User-Agent auf Nummer 2.
Haproxy hat eine Besonderheit: Es löst die DNS-Adressen der Backends beim Start auf und, wenn es keine der Adressen auflösen kann, fällt der Tod des Mutigen.
In unserem Fall ist dies nicht sehr praktisch, da es viele Backends gibt, wir sie nicht verwalten und es besser ist, 503 von Haproxy zu erhalten, als der gesamte Proxyserver aufgrund eines Anbieters den Start verweigert. Die folgende Option hilft uns dabei:
init-addr .
Eine Zeile direkt aus der Dokumentation ermöglicht es uns, alle verfügbaren Methoden zum Auflösen einer Adresse durchzugehen und im Falle einer Datei diese Angelegenheit einfach auf später zu verschieben und weiter zu gehen:
default-server init-addr last,libc,none
Und schließlich mein Favorit: Backend-Auswahl.
Die Syntax für die Haproxy-Backend-Auswahlkonfiguration ist allen bekannt:
use_backend <backend1_name> if <condition1> use_backend <backend2_name> if <condition2> default-backend <backend3>
Aber richtig, es ist irgendwie nicht sehr. Ich habe bereits alle Backends automatisiert beschrieben (siehe
vorherigen Artikel ), es wäre auch hier möglich,
use_backend
zu generieren, das schlechte Geschäft ist nicht knifflig, aber ich wollte nicht. Als Ergebnis wurde ein anderer Weg gefunden:
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
Daher haben wir die Namen der Backends und URLs standardisiert, über die Sie zu ihnen gelangen können.
Nun kompilieren Sie aus den obigen Beispielen eine Datei:
Vollversion der Konfiguration 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
Vielen Dank an diejenigen, die bis zum Ende gelesen haben. Dies ist jedoch nicht alles.
Das nächste Mal werden wir uns mit untergeordneten Dingen befassen, die mit der Optimierung des Systems selbst zusammenhängen, in dem Haproxy arbeitet, damit es und sein Betriebssystem sich wohlfühlen und es genug Eisen für alle gibt.
Bis dann!