A propos des erreurs qui surgissent de nulle part et dans lesquelles il n'y a personne à blâmer: le phénomène du maculage de responsabilité

Mikher multimédia

L'article ne parlera pas d'employés irresponsables, comme le suggère le titre de l'article. Nous discuterons d'un réel danger technique qui peut vous attendre si vous créez des systèmes distribués.

Dans un système d'entreprise, il y avait un composant. Ce composant a collecté des données des utilisateurs sur un certain produit et les a enregistrées dans une banque de données. Et il comprenait trois parties standard: l'interface utilisateur, la logique métier sur le serveur et les tables dans la base de données.

Le composant a bien fonctionné et pendant plusieurs années, personne n'a touché à son code.

Mais une fois, sans raison, des choses étranges ont commencé à arriver au composant.

Lorsque vous travaillez avec certains utilisateurs, un composant au milieu d'une session a soudainement commencé à générer des erreurs. Cela arrivait rarement, mais comme d'habitude, au moment le plus inopportun. Et ce qui est le plus incompréhensible, les premières erreurs sont apparues dans une version stable du système en production. Dans la version dans laquelle pendant plusieurs mois aucun composant n'a été changé du tout.

Nous avons commencé à analyser la situation et vérifié le composant sous forte charge. Ça marche bien. Tests d'intégration répétés assez étendus. Dans les tests d'intégration, notre composant a bien fonctionné.

En un mot, l'erreur est venue pas clair quand et pas clair où.

Ils ont commencé à creuser plus profondément. Une analyse détaillée et une comparaison des fichiers journaux ont montré que la cause des messages d'erreur affichés à l'utilisateur était une violation de contrainte dans la clé primaire du tableau déjà mentionné dans la base de données.

Le composant a écrit des données dans la table à l'aide de Hibernate, et parfois Hibernate, lors de la tentative d'écriture de la ligne suivante, a signalé une violation de contrainte.

Je n'ennuierai pas les lecteurs avec plus de détails techniques et je vous parlerai immédiatement de l'essence de l'erreur. Il s'est avéré que non seulement notre composant écrit dans le tableau ci-dessus, mais parfois (extrêmement rarement) un autre composant. Et elle le fait très simplement, avec une simple instruction SQL INSERT. Un Hibernate fonctionne par défaut lors de l'écriture comme suit. Pour optimiser le processus d'écriture, il interroge l'index de la clé primaire suivante une fois dans l'index, puis écrit plusieurs fois en augmentant simplement la valeur de la clé (10 fois par défaut). Et s'il arrivait qu'après la demande, le deuxième composant soit bloqué dans le processus et écrivait des données dans la table à l'aide de la valeur de clé primaire suivante, la tentative d'écriture suivante à partir d'Hibernate a entraîné une violation de contrainte.
Si vous êtes intéressé par les détails techniques, voyez-les ci-dessous.

Détails techniques
.
Le code de classe a commencé comme ceci:

@Entity @Table(name="PRODUCT_XXX") public class ProductXXX {                               @Id                @Basic(optional=false)                @Column(                                name="PROD_ID",                                columnDefinition="integer not null",                                insertable=true,                                updatable=false)                @SequenceGenerator(                                name="GEN_PROD_ID",                                sequenceName="SEQ_PROD_ID",                                allocationSize=10)                @GeneratedValue(                                strategy=GenerationType.SEQUENCE,                                generator="GEN_PROD_ID")                private long prodId; 

Une discussion d'un problème similaire sur Stackoverflow:
https://stackoverflow.com/questions/12745751/hibernate-sequencegenerator-and-allocationsize

Et il se trouve que pendant plusieurs mois après avoir modifié le deuxième composant et implémenté les entrées dans le tableau, les processus d'écriture des premier et deuxième composants ne se chevauchent jamais dans le temps. Et ils ont commencé à se croiser lorsque, dans l'une des unités utilisant le système, l'horaire de travail a légèrement changé.

Eh bien, les tests d'intégration se sont bien déroulés, car les intervalles de temps pour tester les deux composants à l'intérieur des tests d'intégration ne se sont pas croisés non plus.

D'une certaine manière, nous pouvons dire que personne n'était vraiment responsable de l'erreur.

Ou n'est-ce pas?

Observations et réflexions


Après avoir découvert la véritable cause de l'erreur, elle a été corrigée.

Mais pas avec cette fin heureuse, je voudrais terminer cet article, mais réfléchir à cette erreur en tant que représentant de la vaste catégorie d'erreurs qui ont gagné en popularité après la transition des systèmes monolithiques aux systèmes distribués.

Du point de vue des composants individuels ou des services dans le système d'entreprise décrit, tout a été fait, tout semble aller bien. Tous les composants ou services avaient des cycles de vie indépendants. Et lorsque le besoin s'est fait sentir d'écrire sur la table dans le deuxième composant, en raison de l'insignifiance de l'opération, une décision pragmatique a été prise de l'implémenter directement dans ce composant de la manière la plus simple, et de ne pas toucher le premier composant stable et fonctionnel.

Mais hélas, ce qui arrivait souvent dans les systèmes distribués (et relativement moins souvent dans les systèmes monolithiques) s'est produit: la responsabilité d'effectuer des opérations sur un objet particulier était répartie entre les sous-systèmes. Assurément, si les deux opérations d'écriture étaient implémentées dans le même microservice, une seule technologie serait choisie pour leur implémentation. Et puis l'erreur décrite ne se serait pas produite.

Les systèmes distribués, en particulier le concept de microservices, ont efficacement aidé à résoudre un certain nombre de problèmes inhérents aux systèmes monolithiques. Cependant, paradoxalement, la séparation des responsabilités pour les services individuels provoque l'effet inverse. Les composants "vivent" maintenant aussi indépendamment que possible. Et inévitablement, il y a une tentation, en apportant de grands changements à un composant, de «visser ici» une petite fonctionnalité qui serait mieux implémentée dans un autre composant. Cela atteint rapidement l'effet final, réduit le volume d'approbations et de tests. Ainsi, de changement en changement, les composants sont envahis de fonctionnalités inhabituelles pour eux, les mêmes algorithmes et fonctions internes sont dupliqués, la multivariance de la résolution de problèmes (et parfois leur non-déterminisme) se pose. En d'autres termes, un système distribué se dégrade avec le temps, mais différemment d'un système monolithique.

La responsabilité «tacite» des composants dans les grands systèmes composés de nombreux services est l'un des problèmes typiques et douloureux des systèmes distribués modernes. La situation est encore compliquée et confuse par les sous-systèmes d'optimisation partagés tels que la mise en cache, la prédiction des opérations suivantes (prédiction), ainsi que l'orchestration des services, etc.

Centraliser l'accès à la base de données, au moins au niveau d'une bibliothèque unique, l'exigence est assez évidente. Cependant, de nombreux systèmes distribués modernes se sont historiquement développés autour de bases de données et utilisent les données qui y sont stockées directement (via SQL) plutôt que par le biais de services d'accès.

"Aider" la diffusion des cadres de responsabilité et ORM et des bibliothèques comme Hibernate. En les utilisant, de nombreux développeurs de services d'accès aux bases de données souhaitent involontairement donner le plus haut possible des objets à la suite de la demande. Un exemple typique est la demande de données utilisateur afin de les afficher dans un message d'accueil ou dans le champ avec le résultat de l'authentification. Au lieu de renvoyer le nom d'utilisateur sous la forme de trois variables de texte (prénom, nom intermédiaire, nom de famille), une telle demande renvoie souvent un objet utilisateur à part entière avec des dizaines d'attributs et d'objets connectés, tels que la liste des rôles de l'utilisateur demandé. Cela, à son tour, complique la logique de traitement du résultat de la demande, génère des dépendances inutiles du gestionnaire sur le type de l'objet retourné et ... provoque la répartition des responsabilités en raison de la possibilité de mettre en œuvre la logique associée à l'objet de l'extérieur responsable de cet objet de service.

Que faire? (Recommandations)


Hélas, l'étalement de responsabilité dans certains cas est parfois forcé, et parfois même inévitable et justifié.

Néanmoins, si possible, vous devez essayer de respecter le principe de répartition des responsabilités entre les composants. Un composant est une responsabilité.

Eh bien, s'il est impossible de concentrer les opérations sur certains objets strictement dans un seul système, un tel maculage doit être très soigneusement enregistré dans la documentation à l'échelle du système («supercomposant») comme la dépendance spécifique des composants de l'élément de données, de l'objet de domaine ou les uns des autres.

Il serait intéressant de connaître votre avis à ce sujet ainsi que les cas de pratique confirmant ou réfutant les thèses de cet article.

Merci d'avoir lu l'article jusqu'au bout.

Illustration "Multimedia Mikher" de l'auteur de l'article.

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


All Articles