Aller au contenu principal

7 articles tagués avec « Performance »

Débit, latence et mise à l'échelle dans les systèmes distribués.

Voir tous les tags

Beaucoup de clés, peu d'allers-retours : comment Zaris éventaille une opération en masse

· 12 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Éventailler une opération en masse vers ses nœuds propriétaires

Demandez une clé à un store distribué et le coût est dominé par un seul nombre : l'aller-retour vers le nœud qui la possède. La recherche elle-même est de l'ordre de la nanoseconde ; le réseau est de l'ordre de la microseconde. Demandez maintenant un millier de clés. Si vous bouclez et appelez Get un millier de fois, vous payez cet aller-retour un millier de fois, en série, et le store passe presque tout le temps d'horloge à rester inactif en attendant des paquets. Les clés pourraient être réparties sur quatre nœuds qui auraient pu répondre en parallèle — mais une boucle naïve ne leur en donne jamais la chance.

Zaris a trois API en masse — GetManyAsync, PutManyAsync, DeleteManyAsync — et un ExecuteBatchAsync mixte en dessous, et toute leur raison d'exister est de transformer « un millier de clés » en « une requête par nœud propriétaire, émise en parallèle ». Cet article est le chemin de l'éventail de bout en bout : comment le client regroupe les clés par propriétaire, pourquoi les quatre API se réduisent à un seul moteur, comment un champ Index garde les réponses dans l'ordre à travers deux couches de concurrence, et pourquoi — contrairement à une transaction multi-clés — une opération en masse est par item et le succès partiel est normal.

Un saut vers le propriétaire : comment le client Zaris route une clé

· 13 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Comment le client Zaris route une clé vers son propriétaire

Il y a deux façons honnêtes de construire un client de cache distribué. La première est de choisir n'importe quel nœud, de lui envoyer la requête, et de laisser le cluster la relayer en interne vers celui qui possède la clé — simple côté client, mais chaque requête qui atterrit sur le mauvais nœud paie un saut réseau supplémentaire à l'intérieur du cluster. La seconde est de rendre le client assez intelligent pour savoir quel nœud possède la clé avant d'envoyer quoi que ce soit, de sorte que la première socket qu'il touche soit celle qui peut réellement répondre. Zaris prend la seconde voie. Un GET quitte votre processus déjà adressé au nœud qui détient la clé — pas de proxy, pas de saut de relais, pas de coordinateur au milieu.

Cet article est la vue de l'intérieur de son fonctionnement : la fonction qui transforme une clé en propriétaire, la table de correspondance que le client garde en mémoire, et — la partie facile à rater — comment le client reste correct quand la possession bouge pendant que votre trafic est en vol.

Le plancher de latence : une microseconde de travail dans dix de plomberie

· 12 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Une microseconde de travail dans dix microsecondes de plomberie

Voici un nombre qui recadre la façon dont vous pensez un store clé/valeur. Quand nous isolons le store en mémoire de Zaris et lui envoyons des opérations sans rien d'autre sur le chemin — pas de socket, pas de sérialisation, pas de routage — un seul thread sert les lectures à plus d'un million par seconde, et monte proprement à plus de huit millions sur huit threads. Le store, autrement dit, fait le travail réel d'un GET en moins de deux microsecondes, et il n'est emphatiquement pas le goulet d'étranglement.

Et pourtant un GET complet sur une vraie connexion TCP, sur la même machine, plafonne à quelques centaines de milliers par seconde. Le store peut faire le travail cinq fois plus vite que le serveur ne peut le délivrer. La question de performance intéressante au sujet de Zaris n'est donc pas « à quelle vitesse va le store ? ». C'est l'inverse : où part tout le temps entre la socket et le store ? Cet article en est la réponse, tracée opération par opération, car l'écart entre 1,7 microseconde de logique et 10 à 13 microsecondes de CPU serveur par opération n'est pas du bruit — c'est le plancher de latence, et il est presque entièrement de la plomberie.

Le plafond du worker pool : quand un nœud plafonne à 25 % de CPU

· 9 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Un nœud bloqué près de 25 % de CPU avec un débit plat parce que le worker pool de dispatch était le plafond

Voici un résultat de benchmark qui ressemble à une contradiction. Un nœud Zaris servant une charge majoritairement en lecture plafonne à un peu moins d'un demi-million d'ops/s et refuse de grimper — ajoutez plus de connexions client, plus de générateurs de charge, et le nombre ne bouge pas. Le diagnostic évident est « il est saturé ». Sauf que le graphe de CPU dit le contraire : le nœud est posé à environ un quart de son CPU disponible. Trois cœurs sur quatre sont inactifs, et le débit est plat.

Ce n'est pas un nœud limité par les cœurs, et — si vous l'avez écarté avec soin — ce n'est pas non plus un benchmark limité par le client. C'est une troisième chose, et c'est un classique : le worker pool de dispatch des requêtes était plus étroit que le travail ne le voulait, donc les requêtes faisaient la queue pendant que les cœurs se tournaient les pouces.

Chemins chauds sans allocation : comment le client .NET de Zaris utilise ValueTask

· 7 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Le client .NET de Zaris utilise ValueTask sur ses opérations de chemin chaud

La plupart du temps, Task<T> est exactement le bon type de retour pour une méthode asynchrone, et vous ne devriez pas y réfléchir à deux fois. Mais il y a un endroit précis où il vous coûte discrètement : un chemin chaud qui se termine de façon synchrone, appelé des millions de fois.

Voici le mécanisme. Task<T> est un type référence. Chaque appel qui en renvoie un alloue un objet Task sur le tas — même lorsque l'opération se termine de façon synchrone. Un succès de cache servi directement depuis un tampon local, un awaitable déjà terminé : rien de tout cela ne requiert une vraie asynchronie, mais vous payez quand même un objet sur le tas pour porter le résultat. Une allocation n'est rien. Au rythme d'appel d'un client à haut débit — une boucle serrée effectuant des millions de petits Get et Put — c'est un flux régulier de petites allocations sur le tas, et ce flux alimente le ramasse-miettes. Plus de collectes gen-0, plus de CPU dépensé en ramasse-miettes au lieu de votre travail, plus de gigue dans vos latences de queue.

Le client .NET de Zaris s'attaque à cela exactement là où ça compte et nulle part ailleurs. Cet article porte sur cette décision : quelles méthodes ont changé, pourquoi, les règles que vous devez suivre pour les utiliser correctement, et — délibérément — quelles méthodes nous avons laissées intactes.

Grandes valeurs, petites pauses : le découpage des gros objets dans Zaris

· 7 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Le découpage des gros objets maintient les grandes valeurs hors du LOH

Il y a un nombre que tout ingénieur performance .NET finit par mémoriser : 85 000 octets. Toute allocation unique au-delà d'environ cette taille ne va pas sur le tas normal des petits objets — elle va sur le Large Object Heap (LOH). Le LOH est une autre bête. Il n'est collecté que lors d'un ramasse-miettes gen-2 (complet), et par défaut il n'est pas compacté. Cette combinaison convient à un gros tampon que vous allouez une fois et conservez. Elle ne convient pas à un store en mémoire à faible latence qui écrit et réécrit à répétition de grandes valeurs.

Imaginez la charge de travail : gros blobs sérialisés, grands documents JSON, images, charges utiles de protocole — stockés, mis à jour, remplacés, encore et encore. Chacune de ces grandes écritures dépose un objet neuf sur le LOH. Comme le LOH n'est pas compacté, les emplacements libérés ne fusionnent pas en espace contigu ; ils se fragmentent. Et comme en récupérer la moindre partie implique un ramasse-miettes gen-2 complet, les pauses s'allongent et surviennent plus souvent précisément à mesure que l'ensemble de travail grossit. Pour un store dont tout l'argumentaire est la faible latence, c'est le mauvais type de surprise.

Zaris propose une fonctionnalité optionnelle pour contourner entièrement le LOH pour ces valeurs. Elle est désactivée par défaut, et cet article porte sur ce qu'elle fait, comment elle fonctionne, et — tout aussi important — quand vous devriez et ne devriez pas l'activer.

Des lectures qui montent en charge avec vos cœurs

· 6 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Le débit GET de Zaris monte en charge avec les cœurs

Redis est rapide. Il est aussi, par conception, mono-thread sur le chemin de données : une boucle d'événements, un cœur, une commande à la fois. C'est une conception réellement bonne — elle rend Redis simple à appréhender et élimine toute une catégorie de bugs de concurrence. Mais elle comporte un plafond qu'aucune somme d'argent ne peut lever. Donnez à un moteur mono-boucle d'événements une machine à 4 cœurs, à 8 cœurs, à 32 cœurs, et le débit en lecture retombe au même endroit. Les cœurs supplémentaires restent inactifs.

Zaris emprunte l'autre voie. Les clés sont partitionnées et chaque partition a un propriétaire, de sorte que les lectures de clés différentes atterrissent sur des cœurs différents et s'exécutent en même temps. Résultat : un débit qui bouge lorsque vous ajoutez des cœurs — une pente plutôt qu'une ligne plate.