5. Grenzen für Dateideskriptoren
Damit alle eingehenden Verbindungen erfolgreich bedient werden können, berechnet HAProxy beim Laden die Gesamtzahl der Dateideskriptoren, die der Prozess während seiner Laufzeit benötigt. Ein gewöhnlicher Unix-Prozess erhält standardmäßig meist 1024 Dateideskriptoren; ein privilegierter Prozess kann diese Grenze selbst erhöhen. Dies ist ein Grund, HAProxy als root zu starten und die Grenze von ihm anpassen zu lassen. Die Standardgrenze von 1024 Dateideskriptoren reicht ungefähr für 500 gleichzeitige Verbindungen. In die Berechnung gehen der globale Parameter maxconn, der die Gesamtzahl der Verbindungen pro Prozess begrenzt, die Anzahl der Listener, die Server mit aktiviertem Health Check, Agent-Checks, Peers, Protokollziele sowie gegebenenfalls weitere technische Anforderungen ein. Als grobe Schätzung lässt sich maxconn verdoppeln und um einige Dutzend ergänzen.
Ursprünglich konnte HAProxy diesen Wert nicht berechnen. Er musste über die Einstellung “ulimit-n” im Abschnitt global vorgegeben werden. Deshalb findet sich diese Einstellung auch heute noch in vielen Konfigurationen. Häufig war sie jedoch falsch berechnet. Beim Annähern an maxconn scheiterten dann Verbindungen, statt dass eingehende Verbindungen bis zur Verfügbarkeit der benötigten Ressourcen gedrosselt wurden. Deshalb ist es wichtig, alle aus sehr alten Versionen übernommenen “ulimit-n”-Einstellungen zu entfernen.
Schon für mäßige Last muss die Anzahl der Dateideskriptoren erhöht werden. Dabei sind einige Besonderheiten des Betriebssystems zu beachten. Das Polling-Verfahren select() ist auf 1024 Dateideskriptoren begrenzt. Unter Linux konnte es früher zwar mehr verarbeiten, doch einige Betriebssysteme liefern übermäßig restriktive SELinux-Richtlinien aus, die select() mit mehr als 1024 Dateideskriptoren verbieten. Um Laufzeitprobleme zu vermeiden, verweigert HAProxy in diesem Fall inzwischen den Start. Auf allen unterstützten Betriebssystemen steht poll() ohne diese Einschränkung zur Verfügung. Es wird automatisch gewählt; für eine funktionierende Konfiguration ist daher nichts zu tun. Mit zunehmender Anzahl von Dateideskriptoren wird poll() jedoch sehr langsam. HAProxy begrenzt die Auswirkungen durch Maßnahmen wie einen internen Dateideskriptor-Cache und stapelweise Verarbeitung so weit wie möglich. Als Faustregel gilt dennoch, dass poll() mit mehr als tausend gleichzeitigen Verbindungen viel CPU-Leistung beansprucht.
Auf Linux-Systemen mit Kernel 2.6 oder neuer wird der Systemaufruf epoll() verwendet. Dieses erheblich besser skalierende Verfahren nutzt Rückrufe im Kernel und gewährleistet eine konstante Aufweckzeit unabhängig von der Anzahl registrierter, überwachter Dateideskriptoren. Es wird bei Erkennung automatisch verwendet, sofern HAProxy für eine Linux-Variante gebaut wurde. Verfügbarkeit und Unterstützung lassen sich mit “haproxy -vv” prüfen.
Auf unterstützten BSD-Systemen steht kqueue() als Alternative zur Verfügung. Es ist wesentlich schneller als poll() und durch die gebündelte Verarbeitung von Änderungen sogar etwas schneller als epoll(). Mindestens FreeBSD und OpenBSD unterstützen es. Wie bei epoll() unter Linux zeigt “haproxy -vv” die Unterstützung und Verfügbarkeit an.
Ein geeignetes Polling-Verfahren allein genügt nicht; der Prozess muss die Grenzwerte auch erreichen können. Beim Start setzt HAProxy sofort die Dateideskriptorgrenzen des neuen Prozesses und prüft den Erfolg. Fehler meldet er vor dem Fork, damit die Administration sie sehen kann. Wird der Prozess als root gestartet, sollte diese Einstellung ohne Weiteres gelingen. Bei einem Start durch einen unprivilegierten Benutzer kann sie jedoch scheitern. Gibt es einen zwingenden Grund, haproxy nicht als root zu starten, etwa weil Endnutzer oder ein eigenes Anwendungskonto ihn starten, kann die Systemadministration die Dateideskriptorgrenze für diesen Benutzer erhöhen. Mit “ulimit -n” in dessen Befehlszeile lässt sich prüfen, ob die Änderung wirksam ist. Die Ausgabe sollte den neuen Grenzwert zeigen.
Warnung: Werden die Grenzwerte im Konto eines unprivilegierten Benutzers geändert, wirken sie häufig erst bei dessen Anmeldung. Manche beim Systemstart ausgeführten Skripte und crontabs berücksichtigen sie überhaupt nicht. Das hängt vollständig vom Betriebssystem ab. Bei dieser Betriebsweise daher vor dem Start von haproxy stets “ulimit -n” prüfen. Generell wird empfohlen, haproxy im Produktionsbetrieb niemals als unprivilegierter Benutzer zu starten. Ein weiterer wichtiger Grund ist, dass haproxy sonst einige Schutzmaßnahmen nicht aktivieren kann.
Auch wenn das System dem haproxy-Prozess die angeforderte Anzahl von Dateideskriptoren erlaubt, können zwei weitere systemspezifische Grenzen erreicht werden. Die erste ist die systemweite Dateideskriptorgrenze: Sie umfasst alle offenen Dateideskriptoren sämtlicher Prozesse. Wird sie erreicht, geben accept() oder socket() gewöhnlich ENFILE zurück. Die zweite ist die harte Dateideskriptorgrenze pro Prozess, über die setrlimit() nicht hinausgehen darf. Beide hängen stark vom Betriebssystem ab. Unter Linux wird die systemweite Grenze beim Start anhand der Speichermenge festgelegt und kann mit dem sysctl-Parameter “fs.file-max” geändert werden. Die harte Grenze pro Prozess beträgt standardmäßig 1048576 und lässt sich mit “fs.nr_open” ändern.
Zu niedrige Dateideskriptorgrenzen lassen sich auch an einem laufenden Prozess erkennen. strace zeigt bei accept() und socket() den Rückgabewert “-1 EMFILE”, wenn die Prozessgrenze erreicht ist. In diesem Fall genügt es, “ulimit-n” zu erhöhen oder die Einstellung zu entfernen. Geben die Systemaufrufe “-1 ENFILE” zurück, sind dagegen Kernel-Grenzen erreicht, und ein systemweiter Parameter muss angepasst werden. Diese Probleme müssen unbedingt behoben werden: Sie verursachen hohe CPU-Auslastung, wenn accept() scheitert, sowie Verbindungsfehler, die für Benutzer meist sichtbar sind. Eine weitere Lösung besteht darin, den globalen Wert maxconn zu senken, um eine sequenzielle Verarbeitung zu erzwingen, und gegebenenfalls HTTP-Keep-Alive zu deaktivieren, damit Verbindungen schneller freigegeben und wiederverwendet werden.