9 Trotz seiner wachsenden Beliebtheit (auf Englisch) als Open-Source-Programmiersprache kann man in Go immer noch ein paar saftige Bugs aufspüren. Genau das passierte, als Benoit Artuso, Software Architect bei Scality, und sein Team an der Entwicklung und am Benchmarking unserer Software arbeiteten. So haben sie den Fehler aufgedeckt (auf Englisch) – und das haben sie aus der Detektivarbeit gelernt. Fangen wir von vorn an: In unserem Code müssen wir einen anderen Service über HTTP aufrufen. So sieht unser Pseudocode aus: function call_service(dataBuffer) result = http.Put(destination, dataBuffer) return result dataBuffer = "some data&" result = call_service(dataBuffer) print result Das ist sauber und einfach genug. Aber Code ist Code, und oft gibt es einen Haken: Diesmal möchte unsere Komponente wissen, wann dataBuffer bereinigt und wiederverwendet werden kann. Statt des obigen Codes führen wir also Folgendes aus: dataBuffer = get_an_existing_buffer("some data") result = call_service(dataBuffer) reuse(dataBuffer) print result Dieser Code funktioniert meistens. Aber nicht immer: Einige Tests schlagen weiterhin fehl. Schlimmer noch: Das Team stellt fest, dass es den Buffer nicht sofort wiederverwenden kann, wenn http.Put() fehlgeschlagen ist. Die Detektivarbeit geht also weiter. Wir suchen nach der Ursache und entwickeln eine erste Korrektur. Statt den Buffer sofort wiederzuverwenden, hängen wir einen Callback an, der den Code benachrichtigt, wenn der Buffer von der Garbage Collection erfasst wird. An diesem Punkt scheint der Übeltäter hinter uns zu liegen. Gut, der Speicherverbrauch in den Tests ist etwas hoch, aber nicht alarmierend. Spulen wir vor zu dem Zeitpunkt, an dem groß angelegte Tests auf einer echten Hochleistungsplattform anstehen. Unter diesen Bedingungen schießt der Speicherverbrauch über akzeptable Werte hinaus, sodass wir einige Tests verschieben müssen. Rückblickend stellt sich heraus, dass die vorherige Notlösung den Speicherverbrauch in die Höhe treibt. Wieder einmal gehen wir zurück ans Reißbrett und suchen einen anderen Ansatz. Diesmal nutzen wir dataBuffer, um die Methode close() bereitzustellen, die http.Put() (laut Dokumentation) aufruft, wenn der Buffer nicht mehr benötigt wird. Neuer Code: dataBuffer = get_an_existing_buffer("some data") dataBuffer.close() = function () { reuse(dataBuffer) } result = call_service(dataBuffer) print result An diesem Punkt sieht alles gut aus und folgt der Dokumentation. Der Speicherverbrauch pendelt sich auf akzeptable Werte ein. Problem gelöst? Von wegen. Plötzlich gibt es in groß angelegten Tests viele Fehler, mehr als zuvor. Schlimmer noch: Die Software scheint allergisch auf diesen Code zu reagieren und stürzt früh ab. Ein Blick in die Absturzlogs zeigt, dass close() offenbar zweimal aufgerufen wird. Der Versuch, die Quelle dieser Fehler einzugrenzen, führt zu einer Verhaltensänderung von dataBuffer, wodurch http.Put() eine Anfrage ohne korrekte Content-Length erstellt. Die Anfrage ist zwar (laut RFC) weiterhin gültig, doch der Server lehnt diese Art von Anfrage ab und antwortet mit einem entsprechenden Fehlercode. Lehren aus der Bug-Jagd in Go Go und seine Standardbibliothek sind sehr gut. Die meisten unserer Probleme lagen daran, dass wir nicht „das Richtige“ getan haben, als wir die Grenzen ausloten wollten – und an den Folgen für den Lebenszyklus des Eingabe-Buffers.Jedes einzelne Wort in der Go-Dokumentation zählt. Überspringen Sie keinen Satz!Tests sind Ihre Freunde – ignorieren Sie sie nichtFühren Sie so oft wie möglich groß angelegte und lang laufende Tests durch. Genau das haben wir in der PipelineLassen Sie nichts unversucht und melden Sie, was Sie finden. Das ist gut für das Go-Ökosystem und kommt langfristig allen zugute. Das stellt sich als einfache Korrektur heraus, und zu unserer Erleichterung hören auch die Abstürze auf. Unser Verdacht: Tritt an einer kritischen Stelle ein Fehler auf, reagiert die Go-Standardbibliothek, indem sie den Buffer zweimal schließt. Um den wahren Schuldigen zu finden, reproduzieren wir das Problem zunächst in kleinerem Maßstab und reduzieren es dann, bis nur noch ein kleiner Reproducer in einer einzigen Datei übrig ist, mit dem wir das Issue im Repository golang/go eröffnen. Dramatische Musik bitte: Der Bug steckt im HTTP-Server der Go-Standardbibliothek (auf Englisch)! Ist die Geschichte damit zu Ende? Nein. Eine endgültige Lösung gibt es noch nicht. Wir hoffen auf einen Fix im Upstream; falls er nicht kommt, erwägen wir eine Umgehungslösung im Code. Bleiben Sie dran!