Le programmeur Defender plus fort que l'entropie

© Dragon Ball. Goku.

Le programmeur-défenseur à tout moment et n'importe où dans le code s'attend à l'apparition de problèmes potentiels et écrit le code de manière à s'en protéger à l'avance. Et si vous ne pouvez pas vous défendre contre un problème, assurez-vous au moins que ses conséquences et son impact sur les utilisateurs sont minimes.

Je me souviens de l'effet FlashForward des superproductions hollywoodiennes lorsque le protagoniste voit la catastrophe imminente et reste extrêmement calme, car il sait à l'avance que cela arrivera et a une protection contre elle. L'idée derrière la programmation défensive est de se protéger des problèmes difficiles ou impossibles à prévoir. Un programmeur de sécurité s'attend à ce que des erreurs se produisent n'importe où dans le système et à tout moment pour les éviter avant qu'elles ne causent des dommages. Cependant, le but n'est pas de créer un système qui ne plante jamais, c'est quand même impossible. L'objectif est de créer un système qui se bloque gracieusement en cas de problème imprévu.

Comprenons plus en détail ce qui est inclus dans le concept de «tomber gracieusement».

  • Tombez vite. En cas d'erreur inattendue, toutes les opérations doivent être terminées immédiatement, en particulier si les calculs ultérieurs sont difficiles ou peuvent entraîner une corruption des données.
  • Tombez bien. Si une erreur se produit, le programme doit libérer toutes les ressources, supprimer les verrous, supprimer les fichiers temporaires et semi-enregistrés, fermer les connexions. Attendez la fin des opérations critiques, dont l'interruption peut conduire à des résultats imprévisibles. Ou un moyen sûr de planter ces opérations.
  • Tomber clairement et magnifiquement. Si quelque chose est cassé, le message d'erreur doit être simple, concis et contenir des détails importants du contexte du système où l'erreur s'est produite. Cela aidera l'équipe responsable du système à comprendre le problème le plus rapidement possible et à le résoudre.

Mais vous avez peut-être une question.

Pourquoi perdre du temps sur des problèmes qui pourraient survenir à l'avenir? Maintenant qu'ils ne sont pas là, le code fonctionne parfaitement. De plus, les problèmes peuvent ne jamais se produire du tout. Après tout, les professionnels ne font pas d'ingénierie pour le plaisir de l'ingénierie ( YAGNI - Vous n'en aurez pas besoin)!

L'essentiel est le pragmatisme


Andrew Hunt dans le livre "Programmer-pragmatist" donne la définition suivante de la programmation défensive - " paranoïa pragmatique ".

Protégez votre code contre:

  • propres erreurs;
  • les erreurs des autres;
  • erreurs et défaillances dans d'autres systèmes avec lesquels votre est intégré;
  • les erreurs de fer, les environnements et les plates-formes sur lesquels votre application fonctionne.

Discutons de plusieurs méthodes tactiques et stratégiques de programmation défensive, qui permettront de créer un système fiable et prévisible résistant aux défaillances arbitraires.

Certains conseils peuvent sembler «du capitaine», mais dans la pratique, de nombreux développeurs ne les suivent même pas. Mais si vous respectez des pratiques et des approches simples, cela augmentera considérablement la stabilité de votre système.

Ne fais confiance à personne


Les données utilisateur ne sont pas fiables par défaut. Les utilisateurs comprennent souvent mal ce qui nous semble évident (en tant que développeurs de systèmes). Attendez-vous à une entrée incorrecte et vérifiez-la toujours.

Vérifiez également la quantité d'entrée. Il se peut que l'utilisateur en envoie trop. Dans le même temps, du point de vue de la logique métier, c'est le bon scénario. Mais cela peut entraîner un traitement trop long. Que peut-on faire avec ça? Par exemple, exécutez-le de manière asynchrone, si la quantité de données d'entrée dépasse un certain seuil et que les spécificités de l'entreprise vous permettent de traiter les données en arrière-plan.

Les paramètres d'application (par exemple, les fichiers de configuration) sont également soumis à l'apparition de données incorrectes. Souvent, les paramètres du programme sont stockés dans JSON, YAML, XML, INI et d'autres formats. Étant donné que tous ces fichiers sont des fichiers texte, il faut s'attendre à ce que tôt ou tard quelqu'un y change quelque chose et que votre programme ne fonctionne plus correctement. Il peut s'agir d'un utilisateur final ou d'une personne de votre équipe.

Bases de données, fichiers, stockages centralisés de configurations, registre - tous ces endroits sont accessibles à d'autres personnes, et tôt ou tard ils y changeront quelque chose ( la loi de Murphy ).

Entrée poubelle → entrée poubelle


Les entrées qui passent la validation et commencent à être traitées doivent être propres si vous voulez que votre code fasse exactement ce que vous attendez de lui.

Cependant, il est recommandé d'effectuer des vérifications supplémentaires de validation des données, y compris lorsqu'elles ont déjà commencé à être traitées. Dans les endroits critiques (facturation, autorisation, données personnelles et confidentielles, etc.), c'est presque une exigence obligatoire. Ceci est nécessaire pour qu'en cas de bogues dans le code ou de problèmes avec le validateur de données d'entrée, arrêtez le flux d'exécution le plus rapidement possible. Il est difficile de faire une validation de haute qualité en vérifiant tous les scénarios d'erreur possibles, vous pouvez donc utiliser des moyens plus simples pour valider que le programme s'exécute toujours correctement - assertions et exceptions.

La paranoïaie saine est une caractéristique de tous les développeurs professionnels. Mais il est très important de rechercher l'équilibre optimal et de comprendre quand la solution est déjà suffisamment bonne.

Configurations séparées autour des environnements


Une cause fréquente de problèmes est la séparation insuffisante des configurations entre les environnements ou l'absence d'une telle séparation.

Cela peut entraîner de nombreux problèmes, par exemple:

  • l'environnement de test commence à lire et / ou à écrire des données à partir de la production, des bases de données, des files d'attente et d'autres ressources;
  • l'environnement de test utilise des intégrations et des services externes avec un compte de production;
  • mélanger les statistiques, les métriques, les erreurs de différents environnements;
  • violation de la sécurité (développeurs, testeurs et autres membres de l'équipe ont accès aux ressources de production);
  • bogues difficiles à étudier sur la production (par exemple, une partie des messages dans la file d'attente est perdue en raison de l'environnement de test commençant à le lire).

Ce ne sont que des exemples, une liste complète des problèmes pouvant être causés par une séparation insuffisamment responsable des configurations est presque infinie et dépend des spécificités du projet.

Une séparation responsable des données de configuration par environnement peut réduire considérablement la probabilité de voir immédiatement toute une classe de problèmes associés à:

  • la sécurité
  • la fiabilité;
  • support et déploiement (les ingénieurs DevOps vous remercieront).

De plus, il est recommandé de stocker les données secrètes (clés, jetons, mots de passe) dans un endroit séparé spécialement conçu pour stocker et traiter les secrets. Ces systèmes cryptent les données en toute sécurité, disposent de moyens flexibles pour gérer les droits d'accès et vous permettent également de changer rapidement les clés si elles ont été compromises. Dans ce cas, vous n'avez pas besoin de modifier le code et de redéployer l'application. Ceci est particulièrement important pour les systèmes qui fonctionnent avec des transactions financières, des données confidentielles ou personnelles.

Rappelez-vous l'effet en cascade


L'effet en cascade est une cause courante de la chute de systèmes vastes et complexes. La panne ou la dégradation de la fonctionnalité de l'une des parties du système se produit et, un par un, les autres sous-systèmes qui lui sont associés commencent à échouer. En cascade jusqu'à ce que l'ensemble du système devienne complètement inaccessible.

Quelques astuces de protection:

  • utiliser des temporisations progressives (exponentielles) avec un élément aléatoire;
  • définir des valeurs raisonnables pour le délai de connexion et le délai de socket;
  • prévoir un repli à l'avance en cas de défaillance des services individuels. Il est préférable de dégrader temporairement certaines fonctionnalités, de désactiver complètement les services, mais ne risquez pas de casser tout le système. Mais imaginez que dans ce cas, l'utilisateur voit un message compréhensible et non effrayant, et que l'équipe d'assistance et de développement découvre le problème dès que possible.

Signaler rapidement les problèmes


Tous les systèmes échouent. Parfois, quelque chose d'étrange se produit en eux que les créateurs attendent "une fois tous les 10 ans". Les intégrations et les API externes deviennent périodiquement indisponibles ou répondent incorrectement. La solution de secours pour tous ces cas est souvent difficile, longue ou simplement impossible. Anticipez cette situation à l'avance et signalez-la le plus rapidement possible. Connexion au niveau ERREUR ou au système de surveillance - pour acquis. Ajouter une validation supplémentaire à Healthcheck est encore mieux. Pour envoyer un message du code à Slack, Telegram, PagerDuty ou à un autre service qui informera instantanément votre équipe du problème est idéal.

Mais il est important de bien comprendre quand il est judicieux d'envoyer des messages directement. Seulement si une erreur, une situation suspecte ou atypique est associée à des processus métier et qu'il est important qu'une personne ou un groupe de personnes spécifique dans une équipe reçoive une notification dès que possible et puisse répondre.

Tous les autres problèmes et écarts techniques doivent être traités par des moyens standard - surveillance, alerte, journalisation.

Mettre en cache les données fréquemment utilisées et / ou récentes


Les programmes et les utilisateurs ont une chose en commun: ils ont tendance à réutiliser des données souvent utilisées ou récemment rencontrées. Dans les systèmes très chargés, vous devez toujours vous en souvenir et mettre en cache les données dans les endroits les plus chauds du système.

La stratégie de mise en cache dépend fortement des spécificités du projet et des données. Si les données sont modifiables, il est nécessaire d'invalider le cache. Par conséquent, réfléchissez à l'avance à la manière dont vous allez procéder. Et pensez également aux risques qu'il peut y avoir si des données obsolètes apparaissent dans le cache, le cache est en panne, etc.

Remplacez les opérations coûteuses par des opérations bon marché


Travailler avec des chaînes est l'une des opérations les plus courantes de tout programme. Et si cela n'est pas fait de manière optimale, cela peut être une opération coûteuse. Dans différents langages de programmation, les spécificités du travail avec des chaînes peuvent varier, mais vous devez toujours vous en souvenir.

Dans les grandes applications avec une grande base de code, on trouve souvent du code écrit il y a plusieurs années qui fonctionne sans erreur, mais n'est pas optimal en termes de performances. Souvent, un changement banal dans la structure de données d'un tableau / liste vers une table de hachage donne un sérieux coup de pouce (même si ce n'est qu'à un endroit local dans le code).

Parfois, vous pouvez améliorer les performances en réécrivant l'algorithme pour utiliser des opérations au niveau du bit. Mais même dans les rares cas où il est justifié, le code est très complexe. Par conséquent, lorsque vous prenez une décision, tenez compte de la lisibilité du code et du fait qu'il devra être pris en charge. Il en va de même pour d'autres optimisations délicates: presque toujours, un tel code devient difficile à lire et très difficile à maintenir. Si vous décidez toujours des optimisations délicates, n'oubliez pas d'écrire des commentaires décrivant ce que vous voulez que ce code fasse et pourquoi il est écrit de cette façon.

Dans le même temps, l'optimisation doit être traitée avec un pragmatisme sain:

  • si cela vous prend, en tant que développeur, pendant quelques secondes ou minutes - il est logique de le faire immédiatement;
  • si plus, il est raisonnable de le faire immédiatement uniquement lorsque vous êtes sûr à 100% de sa nécessité. Dans tous les autres cas, il est logique de le reporter, d'écrire en code TODO, de collecter plus d'informations, de consulter des collègues, etc.

L'optimisation prématurée est la racine de tout mal (Donald Knuth)

Réécrire dans une langue de niveau inférieur


Il s'agit d'une mesure extrême. Les langues de bas niveau sont presque toujours plus rapides que les langues de niveau supérieur. Mais cette solution a un prix - développer un tel programme est plus long et plus difficile. Parfois, en réécrivant des parties critiques du système dans un langage de bas niveau, vous pouvez augmenter considérablement votre productivité. Mais il y a des effets secondaires - généralement, ces solutions perdent en multiplateforme et leur support est plus cher. Par conséquent, prenez une décision avec soin.

Seul sur le terrain n'est pas un guerrier


Pour terminer, je voudrais noter une autre chose importante, peut-être la plus importante. Les mesures que nous avons envisagées dans les paragraphes précédents ne fonctionneront que si tous les membres de l'équipe y adhèrent et que tout le monde comprend qui est responsable de quoi et ce qui doit être fait en cas de situation critique. Il est important qu'après avoir résolu le problème, tenez une réunion (post mortem) avec toutes les personnes intéressées et découvrez pourquoi ce problème est survenu et ce qui peut être fait pour éviter que ce problème ne se reproduise à l'avenir. Dans de nombreux cas, des modifications techniques et de processus sont nécessaires. Avec chaque nouveau Post Mortem, votre système deviendra plus fiable, l'équipe sera plus expérimentée et cohérente, et l'entropie dans l'univers sera légèrement moins;)

L'article utilise partiellement des documents de Why Defensive Programming is the Best Way for Robust Coding (Ravi Shankar Rajan).

Source: https://habr.com/ru/post/fr472922/


All Articles