Fragen? Antworten! Siehe auch: Alternativlos
Ihr denkt euch: Hey, das hätte doch nie funktionieren dürfen?
Und dann, ab der Sekunde, hört es auch tatsächlich zu funktionieren auf?
Das passiert mir häufiger. Bisher fand ich das immer ganz lustig, aber wenn es meinen Webserver betrifft, dann ist das schon unschön.
gatling basiert auf libowfat, das abstrahiert das Event-Polling-Interface weg. In meinem Fall auf blog.fefe.de ist es epoll, das wegabstrahiert wird. Früher hatten wir poll() und select() für Netzwerk-Events unter Unix, und das war Scheiße. Der Kernel und das Userland mussten pro Iteration durch alle beobachteten Deskriptoren durchgehen und alle einmal angucken. Völlig sinnlose Verschwendung von CPU-Zeit und RAM-Bandbreite, denn normalerweise hat man als Webserver sowas wie 1000 Verbindungen offen (je nach dem, was für Timeouts man da eingestellt hat), und davon ist auf drei oder vier was los.
Die nächste Iteration von Event-API hieß SIGIO, und da hat man dann das Ansagen, an welchen Deskriptoren und Event-Arten man interessiert ist, und das Rückmelden der tatsächlich eingetretenen Events voneinander getrennt. Leider konnte SIGIO jeweils nur ein Event zur Zeit zurückliefern. Das sah damals wie ein krasser Nachteil aus, wie eine Ineffizienz. Syscalls sind nicht kostenlos, also will man pro Syscall die Arbeit maximieren. Daher war der nächste Ansatz, epoll, so, dass wieder Anmelden und Abholen getrennt war, aber das Abholen geht in Gruppen, d.h. da kriegt man sowas wie 100 Events auf einmal. Das ist deutlich effizienter, also mache ich das natürlich so.
Nun unterhalte ich mich mit meinem Kumpel Erdgeist seit längerem darüber, wie wir libowfat mal ordentlich multithreading-fähig kriegen. Der Punkt dabei ist, dass das aktuelle Programmiermodell von libowfat single-threaded ist, und das ist nicht bloß ein Problem mit Locking und Datenstrukturen. Das Problem ist folgendes.
Nehmen wir mal an, man arbeitet auf zwei Sockets zur Zeit, die zusammengehören. Bei gatling kommt das zum Beispiel für Proxy- oder CGI vor, d.h. beim Blog. Ein Socket geht zum Webbrowser des Users, ein Deskriptor geht zum CGI von dem Blog. Die gehören jetzt zusammen. Schließe ich einen, schließe ich auch den anderen.
Nehmen wir jetzt mal an, wir haben die internen Datenstrukturen soweit multithreading-fähig gemacht, und es liegen zwei Events an, einer bei dem Socket zum Webbrowser und einer beim Socket zum CGI. Aber die landen bei verschiedenen Threads. Dann kommen sich die Threads möglicherweise ins Gehege. Die Strategie für effizientes Multithreading ist, dass man die Daten so weit wie möglich trennt. Am effizientesten ist "shared nothing". Daher ist mein Ansatz, dass ich sage: Das lohnt sich erst, wenn der Thread, der einen Socket bearbeitet, da nicht groß rumlocken muss. Aber selbst wenn wir sagen, der lockt herum, dann ist das immer noch nicht trivial. Die Sockets sind ja gleichberechtigt. Wenn als Thread 1 den 1. lockt und dann den anderen, und Thread 2 lockt seinen 1. (den 2. aus Sicht des 1. Threads) und dann den anderen, dann gibt es einen Deadlock. Am liebsten will man also, dass die Anwendung nichts mehr locken muss.
Nun fiel mir vor zwei Tagen auf, dass durch dieses Event-Batch-Verfahren das ja auch passieren kann, dass du ein Event für beide Sockets reinkriegst. Das Event auf dem 1. Socket ist sagen wir mal "Connection reset by peer", du rufst auf beide Deskriptoren ein close() auf, … und an der Stelle schmeißt der Kernel weitere, noch herumliegende Events für den Deskriptor weg und meldet die nicht mehr. Alles gut? Nein! Denn wir haben ja 100 Events auf einmal abgeholt. Wir haben also potentiell noch ein Event für den Deskriptor in der Tasche, den wir gerade geschlossen haben!
OK, denkt ihr euch jetzt vielleicht, aber den habe ich ja geschlossen. Wenn das Event ankommt und ich mache ein read() auf den Deskriptor, gibt es sofort eine Fehlermeldung von read() und alles ist gut. Das ist halt gerade nicht so klar. Nehmen wir mal an, in der Mitte ist noch ein anderes Event, für das öffnen wir beispielsweise eine Datei oder einen Proxy-Socket oder ein CGI. Der Kernel wird dann versuchen, den kleinstmöglichen Zahlenwert für den Deskriptor zu liefern, und der kleinste freie Deskriptor ist der, den wir gerade geschlossen haben. D.h. wenn das Event für den 2. Socket reinkommt, ist der Deskriptor schon für was anderes belegt, und es passieren eben merkwürdige Dinge.
Wir guckten uns virtuell an, und Erdgeist meinte nur so: Wenn jemand close macht, schmeißst du die Events aber schon weg, die da noch rumgammeln?
Nein, tat ich nicht. An die Möglichkeit hatte ich nicht gedacht. Darauf war meine Datenstruktur auch nicht ausgelegt, dass man aus der Mitte der Queue was löschen wollen würde. Ich könnte da jetzt immer irgendwelche Warteschleifen durchlaufen, aber die Datenstrukturen waren ja absichtlich so ausgelegt, dass alle Zugriffe O(1) sein sollten. Nix iterieren.
Also ist die bessere Lösung, da nichts zu löschen, sondern wenn jemand close auf einen Deskriptor macht, und ich habe noch ein Event für den in der Queue (das kann ich ohne Iterieren sehen), dann verzögere ich das tatsächliche Schließen des Deskriptors, bis alle Events abgearbeitet sind. Das hat natürlich auch Nachteile, denn jetzt gibt es mehr System-Last bei der Anzahl der offenen Deskriptoren. Es gammeln ja immer ein paar noch rum, die eigentlich schon geschlossen sein sollten. Ist also auch keine schöne Lösung, aber scheint gerade erstmal zu tun. Ich mache jetzt gleich einen Mike Drop für heute und guck morgen nachmittag nochmal vorbei, ob noch alles läuft. Wenn nicht, dann wisst ihr ja :-)
Oh, Auto-Abschuss und Auto-Neustart gibt es bei mir nicht. Meine Philosophie ist, dass man Fehler sofort brachial merken und hocheskalieren muss, nicht verstecken oder verbergen, sonst gibt es nie genug Leidensdruck, die schnell zu fixen.