[GO] [Fortsetzung] Aufgrund der Spezifikationen von tcp ist nicht bekannt, ob das Verbindungsziel die Verbindung schließt, bis das Paket tatsächlich gesendet wird.

Einführung

Zuvor gab es einen Artikel [https://qiita.com/behiron/items/3719430e12cb770980f3], der [aufgrund der Spezifikationen von tcp nicht bekannt ist, bis das Paket tatsächlich gesendet wird, ob das Verbindungsziel geschlossen ist]. Ich war dort, aber der Grund war

Als ich zuvor SQL von der App in die Datenbank geworfen habe, wurde die Fehlermeldung angezeigt, dass die Verbindung ungültig war. Die Ursache selbst ist sehr einfach, nur dass die Zeitüberschreitungseinstellung zum Halten der Verbindung auf der Serverseite (DB-Seite) kürzer war als auf dem Client, aber "Dies ist ein Fehler beim Schreiben in den Socket auf der Clientbibliotheksseite. Gehen Sie also damit um und verwenden Sie die anderen Verbindungen, die Sie im Verbindungspool haben. “

war.

Dies geschah, als ich Go's MySQL-Treiber verwendete und die Person in GitHub dieses Problem letztes Jahr hatte. Ich behebe das Problem und blog wurde mit diesem Thema geschrieben.

Es war eine großartige Lernerfahrung, daher möchte ich sie vorstellen und gleichzeitig die schwierigen Teile ergänzen.

Lesen Sie den Blog

Hintergrund

Drei Fehler im Go MySQL-Treiber.

Der Hintergrund und andere Themen waren ebenfalls sehr interessant, daher werde ich einige Teile vorstellen, die geringfügig vom Hauptpunkt abweichen.

Es scheint, dass GitHubs Service ein Rails-Monolith war, aber in den letzten Jahren wurde er schrittweise mit Golang umgeschrieben, wobei der Schwerpunkt auf den Teilen lag, die Geschwindigkeit und Zuverlässigkeit erfordern.

Einer von ihnen ist ein Dienst namens "authzd", der 2019 gestartet wurde, und es scheint, dass er der erste Dienst war, der mit einer in Go of GitHub geschriebenen Webanwendung eine Verbindung zu MySQL herstellte.

Der Blog stellt die Korrekturen für die damals aufgetretenen Fehler basierend auf den drei von GitHub behobenen PRs vor. Dieses Mal werde ich den Teil von "The Crash" vorstellen, der zuerst eingeführt wurde.

Übrigens heißt es "was zu unserer ersten" 9 "Verfügbarkeit für den Dienst führt", so dass es den Anschein hat, dass die Verfügbarkeit des Dienstes 90% überschritten hat, indem "Der Absturz" behoben wurde.

Ich denke, es gibt Orte, an denen Sie die Ziele für die Verfügbarkeit von Diensten in Ihrem Unternehmen erhöhen können, aber OSS war der Engpass, daher ist es großartig, OSS zu beheben! !!

Übrigens ist der an das Blog angehängte Screenshot wie ein Monitor von Datadog, so dass GitHub anscheinend auch Datadog verwendet (egal).

The crash Wenn ich grob schreibe, was die Geschichte ist, wenn das "Leerlaufzeitlimit" auf der Serverseite von MySQL kürzer als das des Clients ist, wurde die Verbindung auf der Serverseite tatsächlich geschlossen, als versucht wurde, eine Abfrage vom Client zu senden. Dinge können passieren. In diesem Fall tritt beim Client ein erzwungener Fehler auf.

Die einfache Lösung für dieses Problem besteht darin, "(* DB) .SetConnMaxLifetime" kleiner als das "Leerlaufzeitlimit" des Servers zu machen. Da es sich jedoch um "SetConnMaxLifetime" und nicht um "SetIdleConnMaxLifetime" handelt, werden aktive Verbindungen anstelle von Leerlauf unnötig geschlossen, was nicht cool ist. Dies liegt daran, dass nicht alle DB-Serververbindungen das Konzept "Leerlauf" haben. Es scheint also einen Hintergrund zu geben, den die "Datenbank / SQL" -Seite nicht vorbereitet.

Ich habe genau das oben Genannte getan (als Referenz scheint das DB-Leerlaufzeitlimit im Fall von AWS Aurora standardmäßig auf 8 Stunden eingestellt zu sein. GitHub setzt es auf 30 Sekunden. Es scheint. Es ist kurz !!) Und was kann ich zu diesem Zeitpunkt auf der MySQL-Treiberseite tun? Ich dachte, ich hätte einen Artikel über das gemacht, was ich zuvor untersucht habe, aber es scheint, dass er ihn korrigiert hat.

Kommen wir nun zu den Details.

Der Anfang des Artikels ist fast so (aufgrund der Spezifikationen von tcp ist nicht bekannt, bis das Paket tatsächlich einmal gesendet wird, ob das Verbindungsziel die Verbindung schließt) (https://qiita.com/behiron/items/3719430e12cb770980f3). Das gleiche wird mit dem TCP-Übergangsdiagramm geschrieben.

Selbst wenn der Server ein FIN-Paket sendet, bedeutet dies aufgrund der TCP-Spezifikationen nur, dass die Serverseite nicht schreibt. Es ist möglich, dass der Client auf den Server schreibt und der Server liest und verarbeitet. Und es gibt keine sichere Möglichkeit im TCP-Protokoll, dem Client mitzuteilen, dass der Server nichts zum Schreiben oder Lesen tut (z. B. den Socket schließen).

Ich werde unten zitieren, weil es leicht zu verstehen ist, aber die oben genannten Eigenschaften von TCP scheinen für die meisten Protokolle kein Problem zu sein, aber das MySQL-Protokoll hat einen Fluss, den "der Client sendet und der Server darauf reagiert". Der Client scheint nicht zu "lesen", bis er "schreibt".

In most network protocols on top of TCP, this isn’t an issue. The client is performing reads from the server, and as soon as it receives a [SYN, ACK], the next read returns an EOF error, because the Kernel knows that the server won’t write more data to this connection. However, as discussed earlier, once a MySQL connection is in its Command Phase, the MySQL protocol is client-managed. The client only reads from the server after it sends a request, because the server only sends data in response to requests from the client.

Übrigens denke ich, dass dieses Merkmal für HTTP / 1.x (ohne Pipelining) dasselbe ist, aber vorher Den Mechanismus zum Abbrechen von http.Request of Go verstehen Wie ich im Artikel items / 9b6975de6ff470c71e06) geschrieben habe, erstellt die Go-Server-Implementierung von Go eine Go-Routine, die den Socket liest, wenn der Anforderungshauptteil vollständig gelesen ist, und das Schließen auf der Clientseite während der Serververarbeitung bemerkt](https) //github.com/golang/go/blob/f92337422ef2ca27464c198bb3426d2dc4661653/src/net/http/server.go#L675-L727) Dies ist die Geschichte auf der Serverseite.

Einige von Ihnen denken möglicherweise, dass Sie es erneut versuchen sollten, wenn nach dem Anhören der Geschichte ein Fehler auftritt. Tatsächlich wird der Wiederholungsmechanismus in "database / sql" vorbereitet. Wenn Sie "ErrBadConn" zurückgeben, wird maxBadConnRetries (zweimal) erneut versucht. ** Wenn weiterhin ein Fehler auftritt, wird der Verbindungspool nicht verwendet. Erstellen Sie eine neue Verbindung zu ** Implementierung.

Das Folgende ist ein Beispiel für "QueryContext", aber jeder Prozess von "database / sql" hat einen ähnlichen Wiederholungsprozess und die Treiberseite (go mysql driver sql-driver / mysql) scheint auch einen Fall zu haben, in dem database / sql / driver`` importiert und driver.ErrBadConn zurückgegeben wird.

database/sql/driver/driver.go


// ErrBadConn should be returned by a driver to signal to the sql
// package that a driver.Conn is in a bad state (such as the server
// having earlier closed the connection) and the sql package should
// retry on a new connection.
//
// To prevent duplicate operations, ErrBadConn should NOT be returned
// if there's a possibility that the database server might have
// performed the operation. Even if the server sends back an error,
// you shouldn't return ErrBadConn.
var ErrBadConn = errors.New("driver: bad connection")

database/sql/sql.go


// QueryContext executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
	var rows *Rows
	var err error
	for i := 0; i < maxBadConnRetries; i++ {
		rows, err = db.query(ctx, query, args, cachedOrNewConn)
		if err != driver.ErrBadConn {
			break
		}
	}
	if err == driver.ErrBadConn {
		return db.query(ctx, query, args, alwaysNewConn)
	}
	return rows, err
}

Wenn Sie diesmal versuchen, "ErrBadConn" auf die gleiche Weise zurückzugeben, ist dies zunächst kein Problem (denn selbst wenn der Wiederholungsversuch fehlschlägt, wird der Verbindungspool am Ende nicht verwendet), aber der Ort, an dem der Fehler entdeckt wird, ist " Da es sich um "Schreiben" handelt (es sei denn, Sie bereiten einen Mechanismus wie die httpserver-Implementierung von Go vor, werden Sie feststellen, dass der Server beim Schreiben zum ersten Mal geschlossen wird), scheint es eine Situation zu geben, die Sie nicht immer sicher wiederholen können.

Die folgenden Fälle im Blog sind genau die Fälle von "Um doppelte Vorgänge zu verhindern, sollte ErrBadConn NICHT zurückgegeben werden, wenn die Möglichkeit besteht, dass der Datenbankserver den Vorgang ausgeführt hat" in den Kommentaren von "ErrBadConn", also "ErrBadConn" `Kommt nicht zurück.

What would happen if we performed an UPDATE in a perfectly healthy connection, MySQL executed it, and then our network went down before it could reply to us? The Go MySQL driver would also receive an EOF after a valid write. But if it were to return driver.ErrBadConn, database/sql would

Warum dann nicht vor dem "Schreiben" mit nicht blockierendem "Lesen" und "ErrBadConn" verbinden, wenn es sich um EOF handelt?

Sie mögen denken, aber genau das macht PR!

Nein, die Situation ist kompliziert. ..

Lesen Sie PR

Lesen wir tatsächlich Pakete: Überprüfen Sie die Verbindungslebensdauer, bevor Sie eine Abfrage schreiben. Ich bin hungrig, nur um die Revisionspolitik im vorherigen Kapitel zu verstehen, aber trotz der kleinen PR von ungefähr 100 Zeilen habe ich viel gelernt.

Ich möchte drei Punkte vorstellen, die ich gelernt habe.

Beziehen Sie sich bei der Überprüfung auf den Rohdateideskriptor

Alles, was Sie tun müssen, ist, den nicht blockierenden Socket kurz vor dem Schreiben zu "lesen", wie im vorherigen Kapitel beschrieben, und "ErrBadConn" zurückzugeben, wenn der Server bereits geschlossen ist.

Die Netzwerkverarbeitung von [Go bietet jedoch eine synchrone API als API, in Wirklichkeit ist die interne Verarbeitung jedoch nicht blockierend. ](Https://qiita.com/takc923/items/de68671ea889d8df6904#%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82 % AF% E5% 87% A6% E7% 90% 86% E3% 81% 97% E3% 81% 9F% E6% 99% 82)

Kurz gesagt, wenn das Netzwerk mit einem Mechanismus namens netpoller wartet, wird Goroutine von der ursprünglichen Verarbeitung getrennt und ein Ereignis für den Socket wird von einem Systemaufruf wie epoll asynchron ausgeführt. Go's Laufzeit hat einen Mechanismus, um Goroutine zu erfassen und neu zuzuweisen, wenn sie verarbeitbar wird (obwohl ich die Quelle dieses Teils nie gelesen habe).

Ich denke, dies ist ein wirklich netter Mechanismus, aber wenn Sie sicher sind, dass er nicht wie dieser blockiert wird, ist es besser, einen Systemaufruf mit einem Rohdateideskriptor zu verwenden. Aus diesem Grund wird die folgende Implementierung implementiert.

Ich denke, der Grund, warum ich es nicht explizit auf nicht blockierend gesetzt habe, ist, dass der Rohdateideskriptor auf der Go-Laufzeitseite bereits als "O_NONBLOCK" angegeben ist.

conncheck.go


	sconn, ok := c.(syscall.Conn)
	if !ok {
		return nil
	}
	rc, err := sconn.SyscallConn()
	if err != nil {
		return err
	}
	rerr := rc.Read(func(fd uintptr) bool {
		n, err = syscall.Read(int(fd), buff[:])
		return true
	})
	switch {
	case rerr != nil:
		return rerr
	case n == 0 && err == nil:
		return io.EOF
	case n > 0:
		return errUnexpectedRead
	case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:
		return nil
	default:
		return err
	}

Überprüfen Sie so oft wie möglich

"ResetSession" wird in der Schnittstelle auf der "SQL / Treiber" -Seite definiert, und dieser Prozess wird von "SQL / Treiber" aufgerufen, wenn eine verarbeitete Verbindung zum Verbindungspool zurückgegeben wird. Dies gibt dem implementierenden Treiber die Möglichkeit, die Arbeit zu erledigen.

In dieser PR wird das für die Verbindung in dieser Schnittstellenimplementierung bereitgestellte Flag aktiviert, die Prüfung wird nur durchgeführt, wenn dieses Flag beim Schreiben vorhanden ist, und das Flag wird nach der Prüfung ausgeschaltet.

Infolgedessen wird die Prüfung nur durchgeführt, wenn die aus dem Verbindungspool erworbene Verbindung zum ersten Mal kommuniziert. Beeindruckend! !!

database/sql/driver/driver.go


// SessionResetter may be implemented by Conn to allow drivers to reset the
// session state associated with the connection and to signal a bad connection.
type SessionResetter interface {
	// ResetSession is called while a connection is in the connection
	// pool. No queries will run on this connection until this method returns.
	//
	// If the connection is bad this should return driver.ErrBadConn to prevent
	// the connection from being returned to the connection pool. Any other
	// error will be discarded.
	ResetSession(ctx context.Context) error
}

Mach nichts in Fenstern

In PR wurde der Vorgang unter Windows nicht bestätigt, und es gibt kein CI. So überprüfen Sie den Betrieb Ich habe das argumentiert, aber ich habe die Funktion "connCheck" sowohl in den Dateien "conncheck.go" als auch "conncheck_windows.go" implementiert, wobei "// + build! Windows" angegeben ist und "conncheck_windows". Auf der anderen Seite haben wir die Diskussion mit der Technik fortgesetzt, einfach "Null" zurückzugeben. Dies bedeutet, dass die Fensterseite ohne Änderungen repariert wurde.

Beeindruckend! !!

abschließend

Als ich die PR überprüfte, erklärte ich sie ziemlich ausführlich, als ich sie zum ersten Mal gab, und ich fand es erstaunlich, dass die Auswirkungen auf die Leistungsverzögerung usw. ebenfalls überprüft wurden. Wenn ich eine PR mit OSS mache, habe ich das Gefühl, dass ich eher unauffällig bin, aber ich bin zuversichtlich in meine Korrekturen, habe Druck ausgeübt, dass der Fluss langsam ist, und gesagt, dass ein so ernstes Problem weiterhin besteht. Selbst wenn es kaputt ist, ist es schlecht, weil es tatsächlich als [ok, das macht nächste Woche eine PR] zusammengeführt wird (https://github.com/go-sql-driver/mysql/pull/941#issuecomment-478059994)

Der Inhalt war wunderbar und ich dachte, ich sollte mich anstrengen, also stellte ich ihn vor.

Recommended Posts

[Fortsetzung] Aufgrund der Spezifikationen von tcp ist nicht bekannt, ob das Verbindungsziel die Verbindung schließt, bis das Paket tatsächlich gesendet wird.