Cet article explique comment nous avons identifié et corrigé une anomalie sporadique des performances de stockage que nous avons observée dans l'un de nos benchmarks.

Chez Qumulo, nous construisons une plateforme de données de fichiers haute performance et publier en permanence des mises à jour toutes les deux semaines. L'expédition de logiciels d'entreprise nécessite si souvent une suite de tests complète pour garantir que nous avons fabriqué un produit de haute qualité. Notre suite de tests de performances s'exécute en continu sur l'ensemble de nos offres de plateforme et inclut des tests de performance des fichiers exécutés par rapport aux benchmarks standard de l'industrie.

Saisissez l'anomalie des performances de stockage

Sur une période de quelques mois, nous avons observé une variabilité dans nos benchmarks de lecture et d'écriture multi-flux. Ces tests de performance utilisent IOzone pour générer des lectures et des écritures simultanées sur le cluster, et mesurer le débit agrégé sur tous les clients connectés. En particulier, nous avons observé une distribution bimodale où la plupart des exécutions atteignaient un objectif de performance constamment stable tandis qu'un deuxième ensemble de résultats plus petit était sporadiquement plus lent d'environ 200 à 300 Mo / s, ce qui est environ 10% pire. Voici un graphique qui montre les résultats de performance.

tcp-performance

Caractériser le problème

Lors de l'analyse d'une anomalie des performances de stockage, la première étape consiste à supprimer autant de variables que possible. Les résultats sporadiques ont d'abord été identifiés sur des centaines de versions de logiciels sur une période de plusieurs mois. Pour simplifier les choses, nous avons lancé une série d'exécutions du benchmark, le tout sur le même matériel et sur une seule version logicielle. Cette série d'essais a montré la même distribution bimodale, ce qui signifiait que la variabilité ne pouvait pas être expliquée par des différences matérielles ou des régressions spécifiques à la version du logiciel.

Après avoir reproduit la performance bimodale sur une seule version, nous avons ensuite comparé les données de performances détaillées collectées à partir d'une exécution rapide et d'une exécution lente. La première chose qui saute aux yeux est que les latences RPC inter-nœuds sont beaucoup plus élevées pour les mauvaises exécutions que pour les bonnes exécutions. Cela aurait pu être pour un certain nombre de raisons, mais cela faisait allusion à une cause fondamentale liée au réseau.

Exploration des performances des sockets TCP

Dans cet esprit, nous voulions des données plus détaillées sur les performances de notre socket TCP à partir de tests, nous avons donc activé notre profileur de test de performance pour recueillir continuellement des données de ss. Chaque fois que ss s'exécute, il génère des statistiques détaillées pour chaque socket du système:

> ss -tio6 État Recv-Q Send-Q Adresse locale: Port Peer Adresse: Port ESTAB 0 0 fe80 :: f652: 14ff: fe3b: 8f30% bond0: 56252 fe80 :: f652: 14ff: fe3b: 8f60: 42687 sac cubique wscale: 7,7 rto: 204 rtt: 0.046 / 0.01 ato: 40 mss: 8940 cwnd: 10 ssthresh: 87 bytes_acked: 21136738172861 bytes_received: 13315563865457 segs_out: 3021503845 segs_in: 2507786423 send 15547.8Mbps dernier lastrv: 348 lastrvack: 1140 348Mbps retrans: 30844.2/0 rcv_rtt: 1540003 rcv_space: 4 ESTAB 8546640 0 fe0 :: f80: 652ff: fe14b: 3f8% bond30: 0 fe44517 :: f80: 652ff: fe14b: 2: 4030 sac cubique wscale: 45514 rto : 7,7 rtt: 204 / 2.975 ato: 5.791 mss: 40 cwnd: 8940 ssthresh: 10 bytes_acked: 10 bytes_received: 2249367594375 segs_out: 911006516679 segs_in: 667921849 send 671354128 Mbps durées: 240.4 lastrcvans: 348bps dernier retrackrate 1464 rcv_rtt: 348 rcv_space: 288.4…

Chaque socket du système correspond à une entrée dans la sortie.

Comme vous pouvez le voir dans l'exemple de sortie, ss vide ses données d'une manière qui n'est pas très conviviale pour l'analyse. Nous avons pris les données et tracé les différents composants pour donner une vue visuelle des performances du socket TCP à travers le cluster pour un test de performances donné. Avec ce graphique, nous pourrions facilement comparer les tests rapides et les tests lents et rechercher des anomalies.

La plus intéressante de ces parcelles était la taille de la fenêtre d'encombrement (en segments) pendant le test. La fenêtre de congestion (signifiée par cwnd: dans la sortie ci-dessus) est d'une importance cruciale pour les performances TCP, car il contrôle la quantité de données en cours de vol sur la connexion à un moment donné. Plus la valeur est élevée, plus TCP peut envoyer de données sur une connexion en parallèle. Lorsque nous avons examiné les fenêtres de congestion d'un nœud lors d'une exécution à faible performance, nous avons vu deux connexions avec des fenêtres de congestion raisonnablement élevées et une avec une très petite fenêtre.

tcp-performance-investigation

En regardant les latences RPC inter-nœuds, les latences élevées étaient directement corrélées avec le socket avec la petite fenêtre de congestion. Cela a soulevé la question - pourquoi un socket conserverait-il une très petite fenêtre de congestion par rapport aux autres sockets du système?

Après avoir identifié qu'une connexion RPC présentait des performances TCP nettement moins bonnes que les autres, nous sommes retournés en arrière et avons examiné la sortie brute de ss. Nous avons remarqué que cette connexion «lente» avait des options TCP différentes du reste des sockets. En particulier, il avait les options tcp par défaut. Notez que les deux connexions ont des fenêtres de congestion très différentes et que la ligne affichant une fenêtre de congestion plus petite est manquante sack et wscale:7,7.

ESTAB 0 0 :: ffff: 10.120.246.159: 8000 :: ffff: 10.120.246.27: 52312 sac cubique wscale: 7,7 rto: 204 rtt: 0.183 / 0.179 ato: 40 mss: 1460 cwnd: 293 ssthresh: 291 bytes_acked: 140908972 octets_received: 27065 segs_out: 100921 segs_in: 6489 send 18700.8Mbps durend: 37280 lastrcv: 37576 lastack: 37280 pacing_rate 22410.3Mbps rcv_space: 29200 ESTAB 0 0 fe80 :: e61d: 2dff: febb: c960% bond 0ff: fe33610: d80: 652 rto cubique: 14 rtt: 54 / 600 ato: 48673 mss: 204 cwnd: 0.541 ssthresh: 1.002 bytes_acked: 40 bytes_received: 1440 segs_out: 10 segs_in: 21 send 6918189Mbps lastsnd: 7769628 dernière : 10435 pacing_rate 10909 Mbps rcv_rtt: 212.9 rcv_space: 1228

C'était intéressant, mais regarder un seul point de données de socket ne nous a pas donné beaucoup de certitude que le fait d'avoir des options TCP par défaut était fortement corrélé à notre petit problème de fenêtre de congestion. Pour avoir une meilleure idée de ce qui se passait, nous avons rassemblé les données ss de notre série d'exécutions de référence et observé que 100% des sockets sans les options SACK (accusé de réception sélectif) maintenaient une taille de fenêtre de congestion maximale de 90 à 99.5% plus petite que chaque socket avec des options TCP non par défaut. Il y avait clairement une corrélation entre les sockets manquant l'option SACK et les performances de ces sockets TCP, ce qui est logique car SACK et d'autres options sont destinées à augmenter les performances.

Comment les options TCP sont définies

Les options TCP sur une connexion sont définies en passant des valeurs d'options avec des messages contenant des indicateurs SYN. Cela fait partie de la négociation de connexion TCP (SYN, SYN + ACK, ACK) requise pour créer une connexion. Vous trouverez ci-dessous un exemple d'interaction dans laquelle les options MSS (taille de segment maximale), SACK et WS (mise à l'échelle de la fenêtre) sont définies.

Alors, où sont passées nos options TCP?

Bien que nous ayons associé les options manquantes de SACK et de dimensionnement de fenêtre avec des fenêtres de congestion plus petites et des connexions à faible débit, nous n'avions toujours aucune idée de la raison pour laquelle ces options étaient désactivées pour certaines de nos connexions. Après tout, chaque connexion a été créée en utilisant le même code!

Nous avons décidé de nous concentrer sur l'option SACK car c'est un simple drapeau, en espérant que ce serait plus facile à déboguer. Sous Linux, SACK est contrôlé globalement par un sysctl et ne peut pas être contrôlé par connexion. Et nous avons activé SACK sur nos machines:

>sysctl net.ipv4.tcp_sack
net.ipv4.tcp_sack = 1

Nous ne savions pas comment notre programme aurait pu manquer la définition de ces options sur certaines connexions. Nous avons commencé par capturer la poignée de main TCP lors de la configuration de la connexion. Nous avons constaté que le message SYN initial avait les options attendues, mais le SYN + ACK a supprimé SACK et la mise à l'échelle de la fenêtre.

Nous avons ouvert la pile TCP du noyau Linux et avons commencé à chercher comment les options SYN + ACK sont conçues. Nous avons trouvé tcp_make_synack, qui appelle tcp_synack_options:

static unsigned int tcp_synack_options (const struct sock * sk, struct request_sock * req, unsigned int mss, struct sk_buff * skb, struct tcp_out_options * opts, const struct tcp_md5sig_key * md5, struct tcp_fastopen_cookie * focus (probablement) ireq (probablement) -> sack_ok)) {opts-> options | = OPTION_SACK_ADVERTISE; if (peu probable (! ireq-> tstamp_ok)) restant - = TCPOLEN_SACKPERM_ALIGNED; } ... renvoie MAX_TCP_OPTION_SPACE - restant; }

Nous avons vu que l'option SACK est simplement définie selon que l'option SACK est définie dans la demande entrante, ce qui n'était pas très utile. Nous savions que SACK se débarrassait de cette connexion entre SYN et SYN + ACK, et nous devions encore trouver où cela se passait.

Nous avons jeté un coup d'œil à l'analyse de la demande entrante tcp_parse_options:

void tcp_parse_options (const struct net * net, const struct sk_buff * skb, struct tcp_options_received * opt_rx, int estab, struct tcp_fastopen_cookie * foc) {... case TCPOPT_SACK_PERM: if (opsize == TCPOLEN_SACK_PERM & net-> ipv4.sysctl_tcp_sack) {opt_rx-> sack_ok = TCP_SACK_SEEN; tcp_sack_reset (opt_rx); } Pause; ...}

Nous avons vu que, pour analyser positivement une option SACK sur une requête entrante, la requête doit avoir le flag SYN (il l'a fait), la connexion ne doit pas être établie (ce n'était pas le cas), et le sysctl net.ipv4.tcp_sack doit être activé (c'était le cas). Pas de chance ici.

Dans le cadre de notre navigation, nous avons remarqué que lors du traitement des demandes de connexion dans tcp_conn_request, cela efface parfois les options:

int tcp_conn_request (struct request_sock_ops * rsk_ops, const struct tcp_request_sock_ops * af_ops, struct sock * sk, struct sk_buff * skb) {... tcp_parse_options (skb, & tmp_opt, 0, want_cookie? NULL: & foc); if (want_cookie &&! tmp_opt.saw_tstamp) tcp_clear_options (& tmp_opt); ... retourne 0; }

Nous avons rapidement découvert que le want_cookie La variable indique que Linux veut utiliser la fonction de cookies TCP SYN, mais nous n'avions aucune idée de ce que cela signifiait.

Que sont les cookies TCP SYN?

Inondation SYN

Les serveurs TCP ont généralement un espace limité dans la file d'attente SYN pour les connexions qui ne sont pas encore établies. Lorsque cette file d'attente est pleine, le serveur ne peut pas accepter plus de connexions et doit abandonner les demandes SYN entrantes.

Ce comportement conduit à une attaque par déni de service appelée inondation SYN. L'attaquant envoie de nombreuses requêtes SYN à un serveur, mais lorsque le serveur répond avec SYN + ACK, l'attaquant ignore la réponse et n'envoie jamais un ACK pour terminer la configuration de la connexion. Cela oblige le serveur à essayer de renvoyer les messages SYN + ACK avec des minuteries d'interruption croissantes. Si l'attaquant ne répond jamais et continue d'envoyer des requêtes SYN, il peut maintenir la file d'attente SYN du serveur pleine à tout moment, empêchant les clients légitimes d'établir des connexions avec le serveur.

Résister à l'inondation SYN

Les cookies TCP SYN résolvent ce problème en permettant au serveur de répondre avec SYN + ACK et d'établir une connexion même lorsque la file d'attente SYN est pleine. Ce que font les cookies SYN, c'est en fait encoder les options qui seraient normalement stockées dans la file d'attente SYN (plus un hachage cryptographique de l'heure approximative et des adresses IP et des ports source / destination) dans la valeur initiale du numéro de séquence dans SYN + ACK. Le serveur peut alors supprimer l'entrée de la file d'attente SYN et ne pas gaspiller de mémoire sur cette connexion. Lorsque le client (légitime) répond finalement par un message ACK, il contiendra le même numéro de séquence initial. Le serveur peut ensuite décoder le hachage de l'heure et, s'il est valide, décoder les options et terminer la configuration de la connexion sans utiliser d'espace de file d'attente SYN.

Inconvénients des cookies SYN

L'utilisation de cookies SYN pour établir une connexion présente un inconvénient: il n'y a pas assez d'espace dans le numéro de séquence initial pour encoder toutes les options. La pile TCP Linux encode uniquement la taille de segment maximale (une option obligatoire) et envoie un SYN + ACK qui rejette toutes les autres options, y compris les options SACK et de mise à l'échelle de la fenêtre. Ce n'est généralement pas un problème car il n'est utilisé que lorsque le serveur a une file d'attente SYN complète, ce qui est peu probable à moins qu'il ne soit soumis à une attaque SYN flood.

Vous trouverez ci-dessous un exemple d'interaction qui montre comment une connexion serait créée avec les cookies SYN lorsque la file d'attente SYN d'un serveur est pleine.

L'anomalie des performances de stockage: le problème TCP de Qumulo

Après avoir étudié les cookies TCP SYN, nous avons reconnu qu'ils étaient probablement responsables de nos connexions manquant périodiquement les options TCP. Nous pensions sûrement que nos machines de test n'étaient pas soumises à une attaque SYN flood, donc leurs files d'attente SYN n'auraient pas dû être pleines.

Nous sommes retournés à la lecture du noyau Linux et avons découvert que la taille maximale de la file d'attente SYN était définie dans inet_csk_listen_start:

int inet_csk_listen_start (struct sock * sk, int backlog) {... sk-> sk_max_ack_backlog = backlog; sk-> sk_ack_backlog = 0; ...}

À partir de là, nous avons parcouru les appelants pour constater que la valeur du backlog était définie directement dans le écouter syscall. Nous avons récupéré le code de socket de Qumulo et avons rapidement vu que lors de l'écoute des connexions, nous utilisions toujours un backlog de taille 5.

if (listen (fd, 5) == -1) return error_new (system_error, errno, "listen");

Lors de l'initialisation du cluster, nous créions un réseau maillé connecté entre toutes les machines, nous avons donc bien sûr créé plus de 5 connexions à la fois pour tout cluster de taille suffisante. Nous étions SYN inondant notre propre cluster de l'intérieur!

Nous avons rapidement fait un changement pour augmenter la taille du backlog utilisé par Qumulo et tous les mauvais résultats de performance ont disparu: Affaire close!

Note de la rédaction: cet article a été publié en décembre 2020.

Apprendre encore plus

Qumulo équipe d'ingénierie embauche et nous avons plusieurs offres d'emploi - vérifiez-les et apprenez-en plus sur la vie chez Qumulo.

Contactez-nous

Faites un essai routier. Démo Qumulo dans nos nouveaux laboratoires interactifs interactifs, ou demandez une démo ou un essai gratuit.

Abonnez-vous au blog Qumulo pour les témoignages clients, les informations techniques, les tendances du secteur et les actualités sur les produits