[GO] [Suite] En raison des spécifications de tcp, on ne sait pas si la destination de la connexion ferme la connexion jusqu'à ce que le paquet soit réellement envoyé.

introduction

Auparavant, il y avait un article [https://qiita.com/behiron/items/3719430e12cb770980f3] qui [en raison des spécifications de tcp, on ne sait pas tant que le paquet n'est pas réellement envoyé si la destination de connexion est fermée]. J'étais là, mais la raison était

Auparavant, lorsque je jetais SQL de l'application vers la base de données, j'avais une erreur indiquant que la connexion n'était pas valide. La cause elle-même est très simple, juste que le paramètre de délai d'expiration pour maintenir la connexion côté serveur (côté DB) était plus court que le client, mais "Ceci est une erreur lors de l'écriture sur le socket du côté bibliothèque client. Alors gérez-le et utilisez les autres connexions que vous avez dans le pool de connexions. »

était.

Cela s'est produit lorsque j'ai utilisé le pilote mysql de go, et la personne dans GitHub a eu ce problème l'année dernière. Je suis en train de le réparer, et blog a été écrit avec ce thème.

Ce fut une excellente expérience d'apprentissage, alors je voudrais la présenter tout en complétant les parties difficiles.

Lire le blog

Contexte

Trois bogues dans le pilote Go MySQL.

Le contexte et d'autres sujets étaient également très intéressants, je vais donc vous présenter certaines parties qui s'écartent légèrement du point principal.

Il semble que le service de GitHub était un monolithe Rails, mais au cours des dernières années, il a été progressivement réécrit avec Golang, en se concentrant sur les parties qui nécessitent vitesse et fiabilité.

L'un d'eux est un service appelé ʻauthzd` qui est entré en service en 2019, et il semble que ce soit le premier service à se connecter à MySQL avec une application Web écrite en Go de GitHub.

Le blog présente les correctifs pour les bogues rencontrés à ce moment-là en fonction des trois PR corrigés par GitHub. Cette fois, je présenterai la partie de The crash qui a été introduite en premier.

Au fait, il dit "résultant de notre premier" 9 "de disponibilité pour le service", il semble donc que la disponibilité du service ait dépassé 90% en corrigeant "le crash".

Je pense qu'il y a des endroits où vous pouvez augmenter les objectifs de disponibilité des services dans votre entreprise, mais OSS était le goulot d'étranglement, donc c'est super de corriger OSS! !!

À propos, la capture d'écran jointe au blog est comme un moniteur de Datadog, il semble donc que GitHub utilise également Datadog (peu importe).

The crash Décrivant grossièrement ce qu'est l'histoire, si le «délai d'inactivité» côté serveur de MySQL est plus court que celui du client, la connexion a en fait été fermée par le serveur lors de la tentative d'envoi d'une requête depuis le client. Des choses peuvent arriver. Dans ce cas, le client rencontrera une erreur forcée.

La solution simple à ce problème est de rendre (* DB) .SetConnMaxLifetime plus petit que le" délai d'inactivité "du serveur. Cependant, comme il s'agit de "SetConnMaxLifetime" et non de "SetIdleConnMaxLifetime", les connexions actives au lieu d'être inactives sont fermées inutilement, ce qui n'est pas cool. Cela semble être l'arrière-plan que le côté base de données / sql ne prépare pas parce que toutes les connexions au serveur DB n'ont pas le concept de ʻidle`.

J'ai fait exactement ce qui précède (pour référence, le «délai d'inactivité» de DB semble être réglé sur «8h» par défaut dans le cas d'AWS Aurora. GitHub le définit sur «30s». Il semble. C'est court !!) Et que puis-je faire du côté du pilote mysql à ce moment-là? Je pensais avoir fait un article sur ce que j'avais enquêté auparavant, mais il semble qu'il l'ait corrigé.

Maintenant, entrons dans les détails.

Le début de l'article est presque aussi en raison des spécifications de tcp, on ne sait pas tant que le paquet n'est pas réellement envoyé une fois si la destination de la connexion ferme la connexion. La même chose est écrite avec le diagramme de transition TCP.

En raison des spécifications TCP, même si le serveur envoie un paquet FIN, cela signifie uniquement que le côté serveur n'écrit pas, et il est possible que le client écrit sur le serveur et que le serveur le lit et le traite. Et il n'y a aucun moyen sûr dans le protocole tcp de dire au client que le serveur ne fait rien pour écrire ou lire (par exemple, fermer le socket).

Je vais citer ci-dessous car il est facile à comprendre, mais les caractéristiques ci-dessus de TCP ne semblent pas être un problème pour la plupart des protocoles, mais le protocole MySQL a un flux que "le client envoie et le serveur y répond". Le client ne semble pas "lire" jusqu'à ce qu'il "écrit".

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.

Au fait, je pense que cette caractéristique est la même pour HTTP / 1.x (hors pipelining), mais avant, [Comprendre le mécanisme d'annulation de http.Request of Go](https://qiita.com/behiron/ Comme je l'ai écrit dans l'article items / 9b6975de6ff470c71e06), L'implémentation du serveur http de Go crée une routine go qui lit le socket lorsque le corps de la requête est complètement lu et remarque la fermeture côté client pendant le traitement du serveur //github.com/golang/go/blob/f92337422ef2ca27464c198bb3426d2dc4661653/src/net/http/server.go#L675-L727) C'est l'histoire du côté serveur.

Certains d'entre vous peuvent penser que vous devriez réessayer s'il y a une erreur après avoir écouté l'histoire jusqu'à présent. En fait, le mécanisme de nouvelle tentative est préparé dans database / sql, et si vous retournez ʻErrBadConn`, maxBadConnRetries (deux fois) sera retenté, ** et si une erreur se produit toujours, le pool de connexions ne sera pas utilisé. Créez une nouvelle connexion à ** Implementation.

Ce qui suit est un exemple de QueryContext, mais chaque processus de database / sql a un processus de nouvelle tentative similaire, et le côté pilote (go mysql driver sql-driver / mysql) aussi) semble avoir un cas où database / sql / driver est ʻimport et driver.ErrBadConn` est retourné.

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
}

Si vous essayez de renvoyer ʻErrBadConnde la même manière cette fois, ce ne sera pas un problème en premier lieu (car même si la nouvelle tentative échoue, le pool de connexions ne sera pas utilisé à la fin), mais l'endroit où l'erreur est découverte est Puisqu'il s'agit d'écrire (sauf si vous préparez un mécanisme comme l'implémentation httpserver de Go, vous remarquerez la fermeture du serveur pour la première fois avec l'écriture), il semble y avoir une situation que vous ne pouvez pas toujours réessayer en toute sécurité.

Les cas suivants présentés sur le blog sont exactement les cas de Pour éviter les opérations en double, ErrBadConn ne doit PAS être retourné s'il y a une possibilité que le serveur de base de données ait pu effectuer l'opération dans les commentaires de ʻErrBadConn`. «Ne revient pas.

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

Alors, avant d'écrire, pourquoi ne pas lire avec non bloquant et «ErrBadConn» si c'est EOF?

Vous pourriez penser, mais c'est exactement ce que fait PR!

Non, la situation est compliquée. ..

Lire PR

Lisons en fait paquets: vérifiez la vivacité de la connexion avant d'écrire la requête. J'ai juste envie de saisir la politique de révision du chapitre précédent, mais malgré le petit PR d'environ 100 lignes, j'ai beaucoup appris.

Je voudrais présenter trois points que j'ai appris.

Reportez-vous au descripteur de fichier brut lors de la vérification

Tout ce que vous avez à faire est de lire la socket avec non-blocage juste avant écrire comme organisé dans le chapitre précédent et de retourner ʻErrBadConn` si le serveur est déjà fermé.

Cependant, le traitement réseau de [Go fournit une API synchrone en tant qu'API, mais en fait, il s'agit d'un traitement non bloquant en interne. ](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)

En bref, lorsque le réseau attend avec un mécanisme appelé netpoller, goroutine est séparé du traitement d'origine et un événement sur le socket est exécuté de manière asynchrone par un appel système tel que epoll. Le runtime de Go dispose d'un mécanisme pour saisir et réaffecter la goroutine lorsqu'elle devient traitable (bien que je n'ai jamais lu la source de cette partie)

Je pense que c'est un mécanisme vraiment sympa, mais si vous êtes sûr qu'il ne sera pas bloqué comme celui-ci, il vaut mieux utiliser un appel système avec un descripteur de fichier brut. C'est pourquoi l'implémentation suivante est implémentée.

Je pense que la raison pour laquelle je ne l'ai pas défini explicitement sur non bloquant est que le descripteur de fichier brut est déjà spécifié comme ʻO_NONBLOCK` sur le côté d'exécution de Go.

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
	}

Vérifiez le moins de fois possible

ResetSession est défini dans l'interface du côté sql / driver, et ce processus est appelé par sql / driver lors du retour d'une connexion traitée au pool de connexions. Cela donne au pilote d'implémentation l'occasion de faire le travail.

Dans ce PR, le drapeau fourni pour la connexion dans cette implémentation d'interface est activé, le contrôle est effectué uniquement lorsque ce drapeau est présent à "écriture", et le drapeau est désactivé après le contrôle.

Par conséquent, la vérification ne sera effectuée que lorsque la connexion acquise à partir du pool de connexions communiquera pour la première fois. Hou la la! !!

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
}

Ne rien faire dans les fenêtres

En PR, l'opération n'a pas été confirmée sur windows, et il n'y a pas de CI. Comment vérifier le fonctionnement J'ai soutenu cela, mais j'ai implémenté la fonction connCheck dans les fichiers conncheck.go et conncheck_windows.go avec // + build! Windows spécifié et` conncheck_windows. Sur le côté «aller», nous poursuivions la discussion en utilisant la technique consistant simplement à renvoyer «nil». Cela signifie que le côté des fenêtres a été corrigé sans aucun changement.

Hou la la! !!

en conclusion

Quand j'ai vérifié le PR, je l'ai expliqué en détail la première fois que je l'ai donné, et j'ai trouvé étonnant que l'impact sur le retard de performance, etc., ait également été vérifié. Quand je donne des relations publiques avec OSS, je sens que j'ai tendance à être discret, mais je suis confiant dans mes corrections, j'ai la pression sur le fait que le flux est lent et j'ai dit qu'un problème aussi grave demeure Même s'il est cassé, il est mauvais car il est en fait fusionné comme ok qui fera un PR la semaine prochaine

Le contenu était merveilleux et j'ai pensé que je devais faire un effort, alors je l'ai présenté.

Recommended Posts

[Suite] En raison des spécifications de tcp, on ne sait pas si la destination de la connexion ferme la connexion jusqu'à ce que le paquet soit réellement envoyé.