Loadbalancing mit HAProxy

HAProxy – Ein wertvolles Werkzeug

HAProxy ist ein inzwischen viele Jahre altes und sehr ausgereiftes OpenSource Projekt. Der Name HAProxy steht übersetzt für “High Availability Proxy”. Wie der Name vermuten lässt, handelt es sich also um eine Software, die dabei unterstützen soll, hochverfügbare Dienste zu betreiben.

Ich bin seit Jahren großer Freund dieser Software und setze sie teilweise sogar da ein, wo ich gar keine Hochverfügbarkeit herstellen möchte. Zum Beispiel kann ich HAProxy nutzen, um SSL zentral zu terminieren und mich nicht auf meinem Webserver darum kümmern zu müssen. Oder, wenn ich meinen Webserver nicht direkt aus dem Internet erreichbar machen möchte, eignet sich HAProxy hervorragend als zusätzlichen Security-Layer. Auch kann ich bequemer einzelne Domains von einem Server auf einen anderen umziehen, wenn ich einen HAProxy vor den Webservern stehen habe.

Kurz um: HAProxy ist zu einem der wichtigsten Werkzeuge für mich geworden. In diesem Artikel möchte ich mit dir gerne ein paar meiner Erfahrungen teilen und dir einen Überblick zu HAProxy geben. Dafür gehe ich auf ein paar grundlegende Begrifflichkeiten ein und werde ein wenig über Loadbalancing-Konzepte schreiben. Du wirst sehen, am Ende ist das alles keine Magic und relativ einfach anzuwenden. Wenn du Anmerkungen oder Verbesserungsvorschläge zu diesem Artikel hast, freue ich mich über eine kurze Info an team@gridscale.io. Wenn du bereits durch einen anderen meiner HAProxy-Artikel hier hingekommen bist, weißt du ja bereits das es weitere spannende Artikel gibt. Wenn nicht, dann such doch in unserer Community nach HAProxy. Falls du deine eigenen Erfahrungen mit HAProxy hier veröffentlichen möchtest, nimm doch kurz Kontakt auf.

Doch nun zu den Begrifflichkeiten. Es gilt drei große Blöcke voneinander zu unterscheiden. ACLs, Backend Services und Frontend Services.

HAProxy ACLs (Access Control Lists)

Mithilfe einer ACL (Access Control List) steuerst du Anfragen auf HAProxy und triffst logische Entscheidungen innerhalb von HAProxy. Zum Beispiel erlaubt dir die folgende ACL alle Anfragen auf http://webseite/blog umzuleiten, auf einen separaten Webserver mit einem installierten WordPress oder dergleichen:

acl blog_url backend_service /blog

Der Aufbau einer solchen ACL sieht wie folgt aus:

acl aclname criterion [flags] [operator] value ...

Eine ACL erlaubt außerordentlich umfangreiche Konfigurationen und Regelwerke. So kannst du zum Beispiel auf Basis eines User-Agents, auf Basis von IP-Adressen, anhand von Auth-Headern und vielen weiteren Kriterien umfangreiche Regeln erstellen.

Willst du beispielsweise bestimmte User-Agenten aussperren, hilft dir folgender Konfigurationsauszug:

tcp-request inspect delay 10s
acl not_wanted hdr_sub(user-agent) –f /etc/haproxy/not_wanted.lst
tcp-request content reject if not_wanted

Der o.s. Konfigurationsauszug hilft dir ohne den Rest der Konfiguration noch nicht weiter, aber ich möchte damit gerne herausstellen, wie mächtig das ACL Regelwerk von HAProxy ist – unabhängig der OSI-Layer auf denen du arbeitest.

Wenn du richtig tief einsteigen willst, empfehle ich dir einen Blick in die Dokumentation von HAProxy. Du findest sie unter cbonte.github.io.

Backend Service in HAProxy

HAProxy versteht unter einem Backend Service erst einmal eine beliebige Anzahl (größer eins) an Services. Für diese Backend Services stellt HAProxy einen eigenen Konfigurationsabschnitt bereit. Wenn du HAProxy aus z.B. den Debian Paketen installiert hast, dann findest du in dem Verzeichnis:

/usr/share/doc/haproxy/examples

ein paar gute Beispiele. Die Datei “url-switching.cfg” ist beispielsweise mal einen Blick wert. In einem Backend Bereich von HAProxy legst du u.a. folgendes fest:

  • Welche Backend Services gibt es?
  • Wie sind sie gruppiert?
  • Welche Loadbalancing-Methoden willst du verwenden?
  • Was für Timeouts sind für dich OK und was passiert im Fehlerfall?
  • Auf welchem Layer (OSI Layer 4 – 7) findet der Transport statt?
  • Nutzt du sogenannte Health-Checks für deine Backend Service?

Du siehst, die Parameter sind sehr zahlreich. Ich werde im Rahmen weiterer Artikel ein paar Konfigurationen für HAProxy erstellen und die mE wichtigsten Zusammenhänge erklären. Wenn dir eine Sektion fehlt oder du Fragen zu anderen Parametern hast, dann ping uns kurz an. Wir ergänzen gerne diesen bzw. andere Artikel um weitere Beispiele.

Frontend Services in HAProxy

Also wenn du es bis hierhin geschafft hast, dann kann dich eigentlich nichts mehr schocken. Frontend ist einfach gesagt eine Kombination aus IP und Port an der HAProxy auf neue Anfragen wartet und diese dann gemäß deinem Wunsch (deinen Wunsch formulierst du u.a. auch mit den ACLs) an die verschiedenen Backend Services weiterreicht.

Einen Frontend Service definierst du mit IP und Port (bspw. 1.2.3.4 auf dem Port 567). Im Frontend definierst du außerdem, was genau mit den Anfragen passiert und auf welchem Layer diese verarbeitet werden. Layer 4 zum Beispiel erlaubt dir Services zu Loadbalancen, ohne dass der Loadbalancer die Protokolle (z.B. HTTP) selber kennen und verstehen muss. Folglich kann der Loadbalancer diese Protokolle auch nicht manipulieren (bspw. durch einen X-Forwarded-For Header im HTTP – mehr dazu später). Dafür ist Layer 4 aber der Mode, indem HAProxy am wenigsten rechnen muss und der für HAProxy am einfachen zu bewältigen ist.

Im Folgenden schreibe ich ein wenig über Loadbalancing-Grundlagen. Wenn du dir unter einem Loadbalancer und den verschiedenen Methoden auf den Layern 4 und 7 etwas vorstellen kannst, überspring den nachfolgenden Abschnitt einfach.

Loadbalancing und OSI-Schichten Modell

Ein Loadbalancer wird dafür verwendet, um einen IT-Service horizontal zu skalieren. Meistens entscheidet man sich für den Einsatz von Loadbalancing aus Gründen der Performance, Leistungsfähigkeit und um die Ausfallsicherheit zu erhöhen. In der Praxis ist es meist ein Mix aus den verschiedenen Anforderungen.

Ein Beispiel aus der Praxis: Stell dir vor, dass ein großes Nachrichtenportal Millionen von Anfragen pro Stunde bearbeiten muss. Damit diese Inhalte schnell und zuverlässig ausgeliefert werden, brauchst du eine Vielzahl an Servern. Meist stehen dann komplexe Rechencluster in großen Rechenzentren, die dafür sorgen, die vielen hunderttausend Benutzer stets zuverlässig mit den angeforderten Inhalten zu versorgen.

Was es dabei mit den verschiedenen OSI-Schichten auf sich hat, erklärt dieser Wikipedia-Artikel ganz gut. Die nachfolgende Grafik dient als Übersicht für diesen Artikel.

Von Deadlyhappen – Eigenes Werk, CC-BY-SA 4.0, mygs.io/1S6JgUw

Loadbalancing im Sinne dieses Artikels kann frühestens auf der sogenannten Transportschicht aufsetzen. Unterhalb der Transportschicht findest du lediglich IP-Datenströme, die HAProxy nicht loadbalancen kann. Für ein Loadbalancing auf den unteren Layern kannst du ja mal nach Cluster iptables suchen, auch ein interessantes Projekt. Allerdings habe ich damit keine positiven Erfahrungen in produktiven Umgebungen gemacht.

OK, zurück zum eigentlichen Artikel. In der Frontend Sektion von HAProxy definierst du einen Listener, der sich aus IP und Port zusammensetzt. Damit bist du automatisch auf der Protokollebene (UDP / TCP) und somit auf dem Layer 4 nach ISO-OSI. Definierst du darüber hinaus noch ein Anwendungs-Protokoll (beispielsweise http), befindest du dich im Layer 5 – 7. Das heißt im Klartext, bis auf wenige Ausnahmen gibt es nur zwei Loadbalancing Ansätze: Layer 4 oder Layer 7.

Ohne Loadbalancer sieht eine Verbindung zu deinem Webserver ungefähr wie folgt aus:

Du greifst über das Internet direkt auf den Webserver zu und dieser liefert die angefragten Inhalte aus. Unter Umständen, wie hier in dem Diagramm, hast du bereits die Datenbank auf einem separaten Server ausgelagert, was aber nicht unbedingt notwendig ist.

Wenn du in diesem Fall nun einen Layer-4 Loadbalancer einsetzen möchtest, dann ist deine erste Aufgabe dafür zu sorgen, dass mehr als ein Webserver mit den selben Inhalten bereitsteht. Randnotiz: Nutzt du die Cloud Server von gridscale, clone deine Cloud Server einfach mithilfe eines Snapshots und skaliere damit horizontal deine Inhalte.

Was sich gegenüber dem direkten Zugriff ändert, illustriert die nachfolgende Grafik:

Der HAProxy ist jetzt in der Lage, Anfragen (bspw. auf Port 80) auf die Backend Server zu verteilen. Das sorgt schon einmal für eine signifikante Steigerung der Leistungsfähigkeit und höhere Ausfallsicherheit deiner Webserver.

Eigentlich ist Layer-4 ja schon ausreichend für die meisten Situationen. Aber es gibt durchaus Konstrukte, in denen du vielleicht mit ein wenig mehr Logik arbeiten möchtest. So könntest du zum Beispiel alle Aufrufe deiner Webseite auf den einen Serverpool weiterleiten, Aufrufe deines WordPress Blogs aber auf einen anderen Pool. Jetzt kommt Layer-7 Loadbalancing ins Spiel. Was sich dadurch ändert veranschaulicht die folgende Grafik:

Es kann viele Gründe dafür geben, sich für ein solches Setup zu entscheiden. Im oben stehenden Beispiel braucht deine Webseite vielleicht keine Datenbank, da es sich um statische HTML Inhalte handelt. Oder du hast einfach vollkommen andere Lastprofile auf den unterschiedlichen Servern und möchtest gerne verhindern, dass sich die unterschiedlichen Inhalte in die Quere kommen. Lass deiner Fantasie einfach freien Lauf, du findest eine gute Anwendung dafür 🙂

Loadbalancing-Algorithmen

Nur der Vollständigkeit halber sei erwähnt, dass du im Zusammenhang mit Loadbalancing immer wieder auf drei sogenannte Algorithmen treffen wirst. Diese Algorithmen dienen der Entscheidung, wie ein konkreter Request gehandhabt werden soll und auf welchen der Backend-Server dieser zur Verarbeitung weitergeleitet wird.

Loadbalancer nach roundrobin

Meistens ist roundrobin die Standardeinstellung (zumindest bei F5, Brocade und HAProxy). Die konfigurierten Backend-Server werden einfach der Reihe nach mit Anfragen versorgt. In den meisten Fällen reicht das auch vollkommen aus.

Loadbalancer nach leastconn

Die Methode leastconn (least connection) ist sehr sinnvoll, um die möglichst schnellste Verbindung ausfindig zu machen. Je nach Hersteller kommen unterschiedliche Methoden zum Einsatz. Einige Loadbalancer prüfen regelmäßig mittels eines Health-Checks (später mehr dazu), wie schnell bspw. ein Webserver antwortet. Anhand der Antwortzeiten wird dann eine Gewichtung errechnet und die Anfragen werden dann über die Gewichtung verteilt. Eine bessere Methode ist es, die ständig eintreffenden Verbindungen statistisch auszuwerten und sich zu merken, welcher Webserver gerade wie schnell antwortet. So bekommt der Loadbalancer unmittelbar mit, wenn sich am Antwortverhalten des Webservers etwas ändert und kann die neuen Anfragen direkt anders gewichten.

Wie auch immer die Implementierung im Detail aussieht, der Effekt sollte sehr ähnlich sein. Es wird vom Loadbalancer immer der Backend-Server genommen, der mit größter Wahrscheinlichkeit die Anfrage am schnellsten beantworten wird.

Loadbalancer nach source IP (ggf. auch Port)

Mit einem Loadbalancer nach source IP (oder auch IP + Client Port) versucht man eine etwas statischere Verteilung von Anfragen zu realisieren. So wird gewährleistet, dass ein Client im Regelfall immer auf dem selben Backend-Server landet. Das kann gerade für temporäre Dateien oder dergleichen wichtig sein. Meine Ansicht nach hat dieser Algorithmus aber keine Berechtigung, da das Balancing nach Source-IP (oder ggf. Source-IP + Source-Port) fehleranfällig ist. Was passiert zum Beispiel, wenn ich gerade in einem CMS einen Artikel bearbeite und während dessen baut sich mein DSL neu auf? Unter Umständen bin ich dann auf einem anderen Webserver mit meiner Session und wundere mich über ein merkwürdiges Verhalten. Aus der Praxis erinnere ich mich an keinen Fall, indem ein Projekt längere Zeit Loadbalancing nach Source-IP beibehalten hat. Es kam immer zu „interessanten Effekten“, mit denen sich die technischen Entscheider dann davon überzeugen ließen, das Loadbalancing doch auf andere Algorithmen umzustellen.

Weitere Loadbalancing-Algorithmen

HAProxy erlaubt neben den Standard-Algorithmen noch weitere Verteilungsschlüssel. Wenn du hier tiefer einsteigen möchtest, hilft dir die Seite cbonte.github.io.

Der Vollständigkeit halber eine kurzer Abstract zu den verschiedenen Modes:

  • static-rr: analog zu roundrobin, allerdings mit einer statischen Gewichtung der einzelnen Server.
  • first: einfach der erste verfügbare Server
  • uri: Anhang der aufgerufenen URI (der Teil hinter der Domain) wird ein Hash erstellt und dieser auf die verfügbaren Backend-Server verteilt.
  • url_param: mit diesem Mode kannst du beispielsweise eine User-ID in dem HTTP-Header verstecken und anhand dieser User-ID eine Entscheidung treffen.
  • hdr(HEADER) – mit hdr kannst du ebenfalls den Header auf bestimmte Attribute analysieren und anhand dieser Attribute (bspw. PHP Sess_ID) dann eine Entscheidung treffen.
  • rdp-cookie: hiermit kannst du deinem Benutzer ein Cookie unterschieben, dass den richtigen Backend-Server identifiziert. Eine effiziente Methode um dafür zu sorgen, dass ein Benutzer nicht ständig auf unterschiedlichen Backend-Servern landet.

Die Konfiguration von HAProxy mit den unterschiedlichen Modes sieht immer identisch aus und wird entweder im Listener oder auf den Backend-Services konfiguriert.

Sticky Sessions

Sticky Sessions sind ein gutes Hilfsmittel, um dafür zu sorgen das ein Benutzer immer wieder auf den selben Backend-Servern landet. Du kannst natürlich mithilfe einiger der oben stehenden Layer-7 Loadbalancing-Methoden etwas ähnliches wie eine Sticky Session herstellen (bspw. mithilfe der hdr(HEADER) oder mithilfe des Cookies). Aber wenn du auf Layer-4 arbeitest, wird es schon etwas schwieriger. Da stehen dir dann noch die source-IP zur Verfügung. Wenn aber viele Benutzer von der selben source-IP zugreifen, dann ist die Last möglicherweise sehr unfair verteilt. Du kannst in diesem Fall einen Speicherbereich reservieren lassen, indem eine sogenannte stick-table angelegt wird, in der die TCP Sitzungen zu Backend-Servern zugewiesen werden. Der Unterschied zwischen Sticky und source-IP ist, dass trotz vieler Anfragen derselben Absender-IP-Adresse gewährleistet wird, dass die Requests gleichmäßig auf die Backend-Server verteilt werden. Dazu folgender Konfigurationsauszug:

listen http_handler
    bind 0.0.0.0:80
    mode tcp
    balance leastconn
    stick match src
    stick-table type ip size 150k expire 15m
    server web1 10.0.0.1:80
    server web2 10.0.0.2:80

So, jetzt fehlt nur noch eine wichtige Sache, Health Checks. Anschließend hast du einen umfassenden Überblick über Loadbalancing erhalten und kannst dich an die erste Konfigruation eines HAProxy Dienstes machen.

Loadbalancer Health Checks

HAProxy unterstützt, wie eigentlich jeder Loadbalancer, sogenannte Health Checks. Diese kleinen Skripte stellen sicher, das im Backend alles richtig funktioniert und Backend-Server, die bspw. nicht mehr erreichbar sind, auch keine weiteren Anfragen zur Bearbeitung mehr erhalten.

Health Checks können aber noch viel mehr sinnvolle Aufgaben übernehmen. Nehmen wir einmal an, du hast sehr schwankende Workloads. Nachts ist nahezu gar kein Traffic auf deiner Webseite wohingegen Mittags der Ansturm groß ist. Jetzt hast du grundsätzlich zwei Möglichkeiten. Entweder du baust eine statische Infrastruktur auf und stellst dir so viele Server in dein Rechenzentrum, damit du stets alle Anfragen bedienen kannst. Oder du setzt auf moderne IaaS Plattformen, die dir erlauben in Bruchteilen einer Sekunde neue Ressourcen in deiner Infrastruktur zu starten.

Im letzteren Fall sieht dann dein Workload über einen Tag vielleicht wie auf der folgenden Grafik aus.

Sobald die Last auf deinen Servern nachlässt, schalten sich die Server einfach ab. Und sobald du neue Ressourcen brauchst, starten die vorbereiteten Webserver einfach wieder.

Mit einem Healthcheck sorgst du nun dafür, dass neu gestartete Server automatisch in deinen Workload eingebunden werden. Vollständig automatisiert, ohne dass du auch nur einen Finger rührst.

Du siehst, es gibt viele Anwendungsbereiche für Health-Checks die dir in Summe dein Leben einfacher machen und deine Infrastruktur sehr skalierbar werden lassen. Der einfachste mögliche Check führt nur eine Überprüfung des TCP Ports auf dem Zielwebserver durch und sieht wie folgt aus:

server web1 10.0.0.1:80 check

Die Checks können aber beliebig komplex werden. Aus meiner Sicht ist es immer sinnvoll zu prüfen, ob der Ziel-Service auch wirklich funktioniert. Ein TCP Port kann beispielsweise geöffnet sein, ohne dass der Webserver dahinter auch Daten ausliefert.

Etwas fortgeschrittener ist daher die folgende Konfiguration:

listen http_handler
    bind 0.0.0.0:80
    mode http
    balance leastconn
    option httpchk HEAD / HTTP/1.0
    server web1 10.0.0.1:80 check fall 1 rise 2
    server web2 10.0.0.2:80 check fall 1 rise 2

Hier wird ein HEAD beim Webserver angefragt und dadurch sichergestellt, dass der Webserver auch antwortet. Noch besser ist es, wenn du ein PHP Skript auf dem Zielserver aufrufst um sicherzustellen das auch deine Datenbankverbindung erfolgreich geöffnet werden kann.

Im obigen Beispiel hast du sicherlich gesehen, dass ich hinter dem Parameter „check“ noch weitere Argumente übergeben habe. „fall 1“ sorgt dafür, dass ein fehlerhafter Webserver umgehend außer Betrieb genommen wird. Die Ziffer steht dabei für die Anzahl der fehlerhaften Requests, nachdem ein Backend-Server außer Betrieb genommen wird. „rise 2“ weist HAProxy hingegen an, einen fehlerhaften Backend-Server erst dann wieder in Betrieb zu nehmen, wenn mindestens zwei Health-Checks erfolgreich ausgeführt worden sind.