Fragen? Antworten! Siehe auch: Alternativlos
Ich habe in gatling vor mehreren Jahren SSL-Support eingebaut, mit OpenSSL damals. Später habe ich auch Unterstützung für PolarSSL nachgerüstet, und als die sich nach mbedtls umbenannt haben, bin ich mitgegangen. Der Code für den SSL-Support war immer ziemlich krude, weil ich a) damit keine Erfahrung hatte und b) die APIs alle furchtbar und kompliziert sind und üblicherweise die Dokumentation eher untauglich ist. So mache ich, was viele in der Situation machen, und greife mir ein Stück Code, das ich irgendwo funktionieren gesehen habe, und übernehme das. Bei PolarSSL war das recht einfach, die liefern ein relativ minimales Beispielprogramm mit. Bei OpenSSL war das die Krätze. Es gibt da im Wesentlichen ein Beispielprogramm, nämlich den Code für s_client in dem OpenSSL-Binary, und der macht lauter unnötige und unklare Sachen. Die beschreiben das sogar irgendwo als Absicht, weil das eher Code zum Testen ist als Code zum als Vorbild nehmen.
Benutzung von SSL ist nicht so schwierig, man macht einfach statt read() SSL_read() und statt write() SSL_write(), mal ganz plump gesprochen. Hacks wie sendfile gehen dann halt nicht. Im Fall von gatling kommt erschwerend hinzu, dass ich da auf non-blocking sockets operiere, und SSL halt ein Protokoll ist, d.h. die Signalisierung ist komplexer. write() sagt unter Unix EAGAIN, um mir zu sagen, dass ich später nochmal probieren muss. Bei SSL kann es aber sein, dass ich SSL_write später nochmal probieren muss, aber das nötige Event ist nicht "kann auf diesem Socket schreiben" sondern "kann von diesem Socket lesen", beispielsweise wegen einer Renegotiation oder so. Daher haben SSL_write() und SSL_read() zwei Rückgabewerte, einen für "komm wieder, wenn ich schreiben kann" und "komm wieder, wenn ich lesen kann". Aber gut, kein Ding.
Haariger ist die Initialisierung der Sockets, das ist echt fummelig und nervig. Und die Dokumentation, die da ist, kommt in Form einer Referenz, nicht in Form eines Tutorials oder einer Einführung. Bei OpenSSL operiert man nicht auf Deskriptoren, sondern auf einer Abstraktion namens SSL*. Es gibt aber auch ein SSL_CTX*, und der Unterschied ist nicht so klar, insbesondere weil man seinen Private Key in das SSL reinlädt, nicht in das SSL_CTX. Jedenfalls war das in dem Code so, den ich damals kannibalisiert habe. Wie sich rausstellt, kann man den Private Key aber auch in den SSL_CTX reinladen, und damit steht der Wiederbenutzung des selben SSL_CTX nichts im Wege. Das habe ich neulich mal eingebaut, und die SSL-Performance hat sich wenig überraschend massiv verbessert. Ist ja auch irgendwie klar. Wieso haben die überhaupt ein API, um den Private Key in das SSL* reinzuladen statt in den SSL_CTX*? Kann ich nicht nachvollziehen, verwirrt bloß.
Aber egal, darum geht es nicht. Was ich erzählen wollte: Der SSL-Code hat zwar seit Jahren funktioniert, aber auf ptrace (meinem Blog-Server) habe ich immer bizarre Probleme gesehen, wenn ich eine größere Datei per SSL runterladen wollte. Größer ist so "ab 100 KB" in dem Kontext. Der genaue Stresspunkt variierte. Das Symptom war, dass die Verbindung in der Mitte abbrach. wget verbindet sich dann neu und probiert nochmal, aber das nervt schon. Und vor allem war völlig unklar, was das Problem war. Normale SSL-Verbindungsabbrüche schicken über die Verbindung vorher ein Alert-Paket, damit die andere Seite sieht, wieso die Verbindung geschlossen wurde. Das ist auch nichts, was ich in gatling programmatisch mache, sondern das macht OpenSSL unter den Abstraktionen. So ein Paket kam bei dem wget nicht an.
Daraus schloss ich, dass ich wahrscheinlich irgendwo versehentlich einen Deskriptor schließe, der zu einer anderen Verbindung gehört, und habe aktiv nach Race Conditions und so gesucht. Zuhause konnte ich das nie nachstellen. Schlimmer noch: Wenn ich einen zweiten gatling auf ptrace gestartet habe, konnte ich das auch nicht nachstellen. Nur auf dem Haupt-Blog-gatling mit der ganzen Last tauchte das Problem auf.
Diese Untersuchungen führten zu diesen Überlegungen. Und als ich den Bug da geschlossen hatte, … brachen die SSL-Downloads immer noch ab. Es war also klar, dass ich da noch tiefer analysieren muss. Ich fing also an, mir das Log mit SSL-Fehlermeldungen zuspammen zu lassen, und kriegte da Fehlermeldungen, die so aussahen, als machte ich beim Ausgeben der Fehlermeldungen was falsch. Ich machte also einen strace auf gatling, während das Problem auftauchte, und sah eine Abfolge von Syscalls in etwa so:
accept() -> 23Hier konnte ich also ganz klar sehen, dass kein Fehler vorlag, aber die Verbindung trotzdem geschlossen wurde.
ein paar kurze read/write auf Socket 23, kein Fehler
ein längerer read auf Socket 23, kein Fehler (offenbar der HTTP-Request)
ein langes write auf Socket 23, kein Fehler
noch ein langes write auf Socket 23, diesmal wurden von den 8k nur 4k geschrieben (der Socket ist non-blocking, das soll also so sein)
SSL-Fehlermeldung im Log
close(23)
Nun ziehe ich mir ja SSL-Fehler nicht aus dem Arsch, sondern die kriege ich vom API gemeldet. Da stimmt also was nicht, und es lag nicht daran, dass meine State Machine irgendwo versehentlich einen Socket zu früh oder von woanders schließt.
Die komische SSL-Fehlermeldung, die ich bekam, war übrigens, dass "Shutdown in der Init-Phase aufgerufen wird" (ich hab mir die genaue Fehlermeldung nicht aufgeschrieben, aus der Erinnerung). SSL_Shutdown wird in gatling in cleanup() aufgerufen, das ist die zentrale "fertig, mach mal alle Ressourcen weg"-Funktion. So und jetzt der Fnord: OpenSSL hat einen Error Stack, nicht bloß eine aktuelle Fehlermeldung. Nach SSL_Shutdown habe ich keine Fehler geprüft, denn ob das protokollmäßig funktioniert oder nicht ist mir egal, ich schließe danach den unterliegenden Socket und gebe das SSL* frei. Aber gut, für das Debugging habe ich da mal ein Stück Code eingefügt, das den Error Stack leert.
Plötzlich waren die Verbindungsabbrüche weg.
Ich reime mir das jetzt so zusammen:
Ich hoffe mal, ich freue mich nicht zu früh, und der Bug tritt jetzt wirklich nicht mehr auf. Der ist nicht so einfach zu reproduzieren.
Wir lernen daraus: Error Stack nicht global sondern pro Context oder noch besser pro State machen.
Update: Hier kommt noch der Hinweis rein, dass einem dieser globale Error-Stack dann bei Multithreading massiv auf den Fuß fallen kann. Das befürchte ich auch. gatling ist im Moment noch nicht multithreaded.
Update: Ah, der Error Stack ist pro Thread. Oh Graus ist das alles widerlich.