IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Pratiques de conception de logiciels fiables
Par Chris

Le , par Chris

9PARTAGES

5  0 
Pratiques de conception de logiciels fiables, par Chris

J'ai été victime d'un « nerd-snip ». À l'improviste, un ami m'a posé la question suivante :

Si tu devais construire un cache dans la mémoire, comment ferais-tu ?
Il doit avoir de bonnes performances et être capable de contenir de nombreuses entrées. Les lectures sont plus fréquentes que les écritures. Je sais déjà comment je m'y prendrais, mais je suis curieux de connaître ton approche.
Je ne pouvais pas ne pas mordre à l'hameçon.

En répondant à la question et en écrivant le code, j'ai découvert qu'il s'était passé beaucoup de choses dans mes processus de pensée, qui sont le fruit de l'expérience. Ce sont des choses qui facilitent le génie logiciel, mais que je sais que je n'aurais pas envisagées lorsque j'étais moins expérimenté.

J'ai commencé à écrire un article long et tentaculaire sur le sujet, mais il n'a pas tout à fait touché les bonnes notes, donc ceci est une version très abrégée. Si le temps le permet, je développerai peut-être certaines de ces pratiques dans des articles distincts, plus ciblés et plus concis.

Les pratiques

Voici les huit pratiques que j'ai adoptées avec l'expérience dont j'ai eu besoin pendant l'exercice d'écriture d'un petit cache rapide en mémoire.

1. Utiliser des produits prêts à l'emploi

Notre première réponse à la question devrait être quelque chose comme : pouvons-nous utiliser Redis ?

Tant que nous n'avons pas affaire à un composant très coûteux ou compliqué, ou à une partie du logiciel qui génère la majeure partie de sa valeur, nous devrions opter pour des solutions prêtes à l'emploi. J'ai déjà écrit sur les raisons pour lesquelles nous voulons construire des choses coûteuses et compliquées.

2. Le coût et la fiabilité avant les fonctionnalités

Si nous découvrons que nous ne pouvons pas utiliser une solution prête à l'emploi, nous devons construire quelque chose de bon marché et de fiable. Cela signifie généralement ne pas disposer de toutes les fonctionnalités, mais c'est un compromis qui vaut la peine d'être fait. L'une de mes formulations préférées est la suivante

Il est beaucoup plus facile d'ajouter des fonctionnalités à un logiciel fiable que d'ajouter de la fiabilité à un logiciel riche en fonctionnalités.
En outre, il est très facile de penser accidentellement que l'on a besoin de fonctionnalités dont on n'a pas besoin en réalité.

Je ne suis pas un grand partisan de la phase de conception. On en a parfois besoin, mais je pense que si vous devez concevoir votre logiciel dès le départ, son développement vous coûtera plus cher et prendra plus de temps. D'un autre côté, une petite analyse préalable permet parfois d'éliminer de grandes parties de l'espace de conception avant même d'écrire une seule ligne de code.

Dans le cas de ce cache, nous pouvons poser des questions sur les exigences de durabilité des éléments, les taux de requête, les tailles, les exigences d'éviction, etc. Lorsque nous le ferons, nous découvrirons dans ce cas que nous pouvons nous en sortir avec un seul thread accédant à une seule structure de données, et sans processus d'éviction actif. C'est une grande victoire ! Cela simplifie considérablement la conception.

3. De l'idée à la production rapidement

Une partie du raisonnement de la pratique précédente était qu'il est facile de penser que l'on a besoin de fonctionnalités dont on n'a pas besoin. Comment savoir alors de quelles fonctionnalités vous avez besoin ? Dans la plupart des cas, le moyen le moins coûteux et le plus fiable de le savoir est la production.

Si nous déployons le minimum de fonctionnalités, nous découvrirons très rapidement quelles sont les fonctionnalités supplémentaires les plus demandées, et ce ne seront pas celles que nous pensons. Et même si c'est le cas, il y a généralement d'autres exigences que nous n'aurions pas perçues à l'avance.

Cette pratique est étroitement liée à la précédente : utiliser l'analyse pour réduire les exigences au strict minimum, puis écrire le code minimum pour les prendre en charge, et le mettre en production pour en savoir plus sur le problème que nous essayons de résoudre.

4. Structures de données simples

Les structures de données compliquées sont souvent tentantes. Surtout lorsqu'il existe des bibliothèques qui les gèrent pour nous. Le problème des structures de données compliquées est qu'il est facile de les utiliser à mauvais escient par manque de compréhension, ce qui entraîne des problèmes de performance et des bogues.

Dans le cas de ce cache spécifique, le nombre d'entrées devant être stockées (nous pouvons utiliser la théorie des files d'attente pour le calculer à partir du TTL et du taux d'écriture) serait confortablement traité par 19,9 bits d'information - en d'autres termes, nous pouvons simplement utiliser un tableau simple pour stocker chaque élément, et hacher la clé pour l'adresser.

Remarque : Nous avons découvert plus tôt que nous n'avions pas besoin d'expulser les éléments expirés. Si nous l'avions fait, nous aurions pu conserver un index séparé pour l'élément le plus proche de l'expiration en utilisant un min-heap. C'est moins cher et plus simple qu'un tri complet, qu'il serait tentant d'utiliser autrement. Au lieu d'un processus séparé, nous pouvons nous appuyer sur le taux élevé de requêtes pour déclencher l'éviction des éléments, ce qui permet de gagner un peu de temps de latence et d'éviter d'avoir à concevoir d'emblée un système concurrentiel. Une autre solution consiste à fournir une API distincte qui déclenche l'éviction de l'entrée expirée la plus ancienne, en laissant l'appelant décider du temps à consacrer à l'éviction. Cela signifie que l'appelant peut faire varier ce temps pour réaliser des compromis dynamiques entre latence et besoins en ressources.

5. Réserver les ressources à l'avance

Une autre décision potentiellement controversée que j'ai prise pour le cache que j'ai conçu est qu'il alloue d'emblée l'ensemble du tableau d'index. Cela semble être un gaspillage, mais nous pouvons dire, à partir d'une analyse à rebours, que la plupart de cet espace serait de toute façon nécessaire en fonctionnement continu, et que le report de l'allocation ne permettrait donc pas d'économiser de l'argent.

L'allocation anticipée permet au logiciel de se bloquer rapidement si les ressources nécessaires ne sont pas disponibles. Il s'agit d'une caractéristique souhaitable, car elle évite à l'opérateur la frustration de ne s'en rendre compte qu'une fois qu'il est allé se coucher et qu'il a reçu un appel de PagerDuty. Il est également plus facile de planifier la capacité et de raisonner sur les performances.

6. Fixer des maximums

J'ai choisi une méthode bâclée d'adressage ouvert avec sondage linéaire pour gérer les collisions de hachage dans les éléments d'adressage. Cette méthode est simple et offre de très bonnes performances dans le cas typique (encore une fois, seule une arithmétique très rudimentaire est nécessaire pour le découvrir dans un cas d'utilisation spécifique), mais peut avoir de très mauvaises performances dans le pire des cas : elle peut finir par itérer à travers le tableau entier de tous les éléments à chaque fois que le cache est manqué.

Comme il s'est avéré qu'un rappel parfait n'était pas nécessaire, il était trivial de fixer une limite maximale d'itération de 500 pas, par exemple. Cela permet de s'assurer que le cas le plus défavorable n'est pas trop éloigné du cas moyen, au prix d'un peu plus d'absences de la mémoire cache. Est-ce que 500 pas optimisent ce compromis ? Non. C'est une chose que je ne sais pas comment calculer à l'avance, donc l'expérience de la production devra informer les changements futurs du nombre maximum d'étapes.

Remarque : Voilà pourquoi il est si important de poser des questions sur les besoins !

L'important n'est généralement pas de savoir quel est le maximum, mais simplement qu'il y ait un certain maximum, de sorte que nous n'attendions pas étonnamment longtemps pour que les choses se terminent.

Compte tenu des exigences en matière d'allocation statique, nous pourrions également vouloir fixer une limite à la quantité de données pouvant être stockées dans le cache à un moment donné, afin de ne pas dépasser accidentellement les limites de mémoire au milieu de la nuit. En général, il faut tout limiter ! Si quelqu'un n'est pas satisfait de la limite, révisez-la - ne la supprimez pas.

7. Faciliter les tests

Pour éviter les régressions et garantir un comportement attendu, j'ai fait en sorte que le cache accepte les commandes sur l'entrée standard. Cela signifie que nous pouvons démarrer le cache et taper "write hello world" dans la fenêtre du terminal pour stocker "world" pour la clé "hello".

Mais cela signifie également que nous pouvons écrire une longue chaîne de commandes dans un fichier et l'envoyer dans le cache. Si nous donnons également au cache une commande qui affirme que la dernière valeur lue est quelque chose de spécifique, nous pouvons rédiger un protocole de test complet dans un fichier texte brut, et l'envoyer au programme pour vérifier sa fonctionnalité. Le fichier d'entrée pourrait ressembler à ceci :


Remarque : Il contient également quelques autres commandes liées aux tests, comme la commande sleep qui le force à croire que le temps s'est écoulé (pour tester l'expiration) et la commande dump qui imprime simplement tout ce qu'il stocke actuellement, et si c'est supprimé, expiré ou actif.

Isolément, chacune de ces choses est très simple (accepter des commandes au CLI, permettre d'affirmer une lecture précédente) mais c'est leur combinaison, avec les outils qui existent déjà à portée de main (redirection des entrées du shell) qui permet des temps de cycle plus rapides et donc des logiciels plus fiables.

8. Intégrer des compteurs de performance

Enfin, nous voudrons savoir comment notre programme passe son temps. Nous pouvons le faire avec le profilage, ou le déduire des journaux, mais la façon la plus simple de le faire est d'intégrer des compteurs de performance. Il s'agit de variables qui accumulent la quantité de quelque chose qui se produit. Il peut s'agir de choses comme :

  • Combien de temps passe-t-on à lire les clés ?
  • Combien de temps passe-t-on à écrire des paires clé-valeur ?
  • Combien de temps est consacré aux entrées/sorties ?
  • Combien d'erreurs de cache ont été commises ?
  • Combien de clés avons-nous dû rechercher linéairement ?
  • Combien de fois la limite d'itération de 500 a-t-elle été atteinte ?

Notez que toutes ces questions sont cumulatives, et que nous ne demandons donc pas "combien d'espace de stockage est actuellement alloué", mais nous posons plutôt les deux questions suivantes :

  • Combien d'espace de stockage avons-nous alloué au total depuis le démarrage jusqu'à aujourd'hui ?
  • Combien d'espace de stockage avons-nous libéré au total depuis le démarrage jusqu'à aujourd'hui ?

En décomposant la mesure en deux totaux dépendant du temps, nous pouvons en apprendre beaucoup plus sur le comportement de notre système.

L'évolution de ces chiffres dans le temps est également un indicateur utile. Par exemple, la valeur de "combien de clés avons-nous dû rechercher linéairement" peut augmenter régulièrement (ce qui indique une distribution relativement homogène des collisions de hachage) ou augmenter considérablement parfois (ce qui indique une explosion soudaine des collisions). Il est utile de pouvoir distinguer ces deux types de données. En combinaison avec "Combien de fois la limite d'itération a-t-elle été atteinte", cela nous en dit long sur les pressions exercées sur le système.

Autres pratiques

Des années de génie logiciel ont sûrement permis d'acquérir bien d'autres connaissances, mais ce sont celles auxquelles j'ai pensé au cours de cet exercice. Je serais heureux que vous me fassiez part d'autres pratiques auxquelles vous pensez ! J'espère que cela m'apprendra à devenir un meilleur ingénieur logiciel.

Source : Practices of Reliable Software Design

Et vous ?

Pensez-vous que ces pratiques sont crédibles ou pertinentes ?
Quel est votre avis sur le sujet ?

Voir aussi :

Développement de logiciels à long terme, par Bert Hubert

Le Manifeste anti-héritage : Écrire du code qui dure, par Mensur Durakovic

Le pouvoir surprenant de la documentation, par Vadim Kravcenko
Vous avez lu gratuitement 3 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !