Comment reproduire des conditions d'erreur et voir ce qui se passe pendant l'exécution de l'application?
Comment visualisez-vous les interactions entre les différentes parties concourantes de l'application?
Sur la base de mon expérience, la réponse à ces deux aspects est la suivante:
Traçage distribué
Le traçage distribué est une technologie qui capture les données de synchronisation de chaque composant simultané de votre système et vous les présente sous forme graphique. Les représentations des exécutions simultanées sont toujours entrelacées, ce qui vous permet de voir ce qui fonctionne en parallèle et ce qui ne l'est pas.
Le traçage distribué doit ses origines (bien sûr) aux systèmes distribués, qui sont par définition asynchrones et hautement concurrents. Un système distribué avec traçage distribué permet aux utilisateurs de:
a) identifier les goulots d'étranglement importants, b) obtenir une représentation visuelle des «exécutions» idéales de votre application, et c) fournir une visibilité sur le comportement simultané exécuté, d) obtenir des données temporelles pouvant être utilisées pour évaluer les différences entre les changements de votre système (extrêmement important si vous avez de forts accords de niveau de service).
Les conséquences du traçage distribué sont toutefois les suivantes:
Il ajoute une surcharge à tous vos processus simultanés, car il se traduit par davantage de code à exécuter et à soumettre éventuellement sur un réseau. Dans certains cas, cette surcharge est très importante - même Google utilise son système de traçage Dapper uniquement sur un petit sous-ensemble de toutes les demandes afin de ne pas gâcher l'expérience utilisateur.
Il existe de nombreux outils différents, qui ne sont pas tous interopérables les uns avec les autres. Ceci est quelque peu amélioré par des normes comme OpenTracing, mais pas complètement résolu.
Il ne vous dit rien sur les ressources partagées et leur statut actuel. Vous pourrez peut-être deviner, en fonction du code de l'application et de ce que le graphique que vous voyez est en train de vous montrer, mais ce n'est pas un outil utile à cet égard.
Les outils actuels supposent que vous avez de la mémoire et du stockage à revendre. L'hébergement d'un serveur timeseries peut ne pas être bon marché, en fonction de vos contraintes.
Logiciel de suivi des erreurs
Je me connecte à Sentry ci-dessus principalement parce que c'est l'outil le plus utilisé et pour une bonne raison - un logiciel de suivi des erreurs comme l'exécution du détournement de Sentry permet de transmettre simultanément une trace de pile des erreurs rencontrées à un serveur central.
L’avantage net de ce logiciel dédié en code concurrent:
- Les erreurs de duplication ne sont pas dupliquées . En d'autres termes, si un ou plusieurs systèmes concurrents rencontrent la même exception, Sentry incrémentera un rapport d'incident, mais n'enverra pas deux copies de l'incident.
Cela signifie que vous pouvez déterminer quel système simultané rencontre quel type d'erreur sans avoir à passer par d'innombrables rapports d'erreur simultanés. Si vous avez déjà été victime de spam par courrier électronique à partir d'un système distribué, vous savez à quoi ressemble un enfer.
Vous pouvez même "baliser" différents aspects de votre système concurrent (bien que cela suppose que votre travail ne soit pas entrelacé sur un seul thread, ce qui techniquement n'est pas simultané puisque le thread saute simplement d'une tâche à une autre, mais doit toujours traiter les gestionnaires d'événements à compléter) et voir une ventilation des erreurs par tag.
- Vous pouvez modifier ce logiciel de gestion des erreurs pour fournir des détails supplémentaires avec vos exceptions d'exécution. Quelles ressources ouvertes le processus avait-il? Y a-t-il une ressource partagée que ce processus tenait? Quel utilisateur a rencontré ce problème?
Ceci, en plus des traces de pile méticuleuses (et des mappes de sources, si vous devez fournir une version réduite de vos fichiers), facilite la détermination de ce qui ne va pas une grande partie du temps.
- (Spécifique à Sentry) Vous pouvez disposer d'un tableau de bord de rapports Sentry distinct pour les tests du système, ce qui vous permet de détecter les erreurs lors des tests.
Les inconvénients d'un tel logiciel incluent:
Comme tout, ils ajoutent du volume. Vous ne voudrez peut-être pas un tel système sur du matériel embarqué, par exemple. Je recommande fortement de faire un essai de ce logiciel, en comparant une exécution simple avec et sans échantillonnage sur quelques centaines d'essais sur une machine inactive.
Toutes les langues ne sont pas prises en charge de la même manière, car bon nombre de ces systèmes reposent sur la capture implicite d'une exception et toutes les langues ne disposent pas d'exceptions robustes. Cela étant dit, il existe des clients pour de nombreux systèmes.
Ils peuvent constituer un risque pour la sécurité, car bon nombre de ces systèmes sont essentiellement à source fermée. Dans ce cas, faites preuve de la diligence requise pour les rechercher ou, si vous préférez, lancez les vôtres.
Ils pourraient ne pas toujours vous donner les informations dont vous avez besoin. C'est un risque avec toutes les tentatives pour ajouter de la visibilité.
La plupart de ces services ont été conçus pour des applications Web hautement concurrentes. Tous les outils ne sont donc peut-être pas parfaitement adaptés à votre cas d'utilisation.
En résumé : la visibilité est la partie la plus cruciale de tout système concurrent. Les deux méthodes que je décris ci-dessus, associées à des tableaux de bord dédiés sur le matériel et les données pour obtenir une image globale du système à tout moment, sont largement utilisées dans l’industrie, précisément pour remédier à ce problème.
Quelques suggestions supplémentaires
J'ai passé plus de temps que prévu à réparer des codes chez des personnes qui ont essayé de résoudre des problèmes concurrents de manière terrible. À chaque fois, j'ai trouvé des cas où les choses suivantes pourraient grandement améliorer l'expérience des développeurs (ce qui est tout aussi important que l'expérience des utilisateurs):
Comptez sur les types . La saisie permet de valider votre code et peut être utilisée au moment de l'exécution en tant que protection supplémentaire. Là où la saisie n'existe pas, utilisez des assertions et un gestionnaire d’erreur approprié pour intercepter les erreurs. Le code simultané nécessite un code défensif, et les types constituent le meilleur type de validation disponible.
- Testez les liens entre les composants de code , pas seulement le composant lui-même. Ne confondez pas cela avec un test d'intégration complet - qui teste chaque lien entre chaque composant, et même dans ce cas, il recherche uniquement une validation globale de l'état final. C'est un moyen terrible d'attraper des erreurs.
Un bon test de liaison vérifie si, lorsqu'un composant parle à un autre composant de manière isolée , le message reçu et le message envoyé sont les mêmes que ceux attendus. Si vous avez deux composants ou plus qui dépendent d'un service partagé pour communiquer, mettez-les en synergie, demandez-leur d'échanger des messages via le service central et voyez s'ils obtiennent tous ce que vous attendez.
Découper des tests impliquant beaucoup de composants en un test des composants eux-mêmes et un test de la communication de chacun des composants vous donne une confiance accrue dans la validité de votre code. Un ensemble de tests aussi rigoureux vous permet de faire respecter les contrats entre les services et de détecter les erreurs inattendues qui se produisent lorsqu'ils sont exécutés simultanément.
- Utilisez les bons algorithmes pour valider l'état de votre application. Je parle de choses simples, comme par exemple lorsqu'un processus maître attend que tous ses employés terminent une tâche et ne souhaitent passer à l'étape suivante que si tous les employés ont terminé leur travail - il s'agit d'un exemple de détection globale. terminaison, pour lesquels il existe des méthodologies connues telles que l'algorithme de Safra.
Certains de ces outils sont livrés avec des langages - Rust, par exemple, garantit que votre code n’aura pas de situation de concurrence critique au moment de la compilation, tandis que Go propose un détecteur de blocage intégré qui fonctionne également au moment de la compilation. Si vous pouvez résoudre les problèmes avant qu'ils n'atteignent la production, c'est toujours une victoire.
Une règle générale: conception en cas d'échec dans les systèmes concurrents . Anticipez que les services communs vont planter ou casser. Cela vaut même pour le code qui n'est pas distribué sur plusieurs ordinateurs: le code simultané sur un seul ordinateur peut s'appuyer sur des dépendances externes (telles qu'un fichier journal partagé, un serveur Redis, un putain de serveur MySQL) qui peuvent disparaître ou être supprimées à tout moment. .
Pour ce faire, la meilleure solution consiste à valider de temps à autre l'état de l'application - effectuez des contrôles de l'état de santé de chaque service et assurez-vous que les clients de ce service sont informés de leur mauvais état de santé. Les outils de conteneur modernes tels que Docker le font très bien et devraient être utilisés pour le traitement en bac à sable.
Comment déterminez-vous ce qui peut être rendu simultané et séquentiel?
L'une des principales leçons que j'ai apprises en travaillant sur un système hautement concurrentiel est la suivante: vous ne pouvez jamais avoir assez de métriques . Les métriques doivent contrôler absolument tout dans votre application - vous n'êtes pas un ingénieur si vous ne mesurez pas tout.
Sans métriques, vous ne pouvez pas faire quelques choses très importantes:
Évaluez la différence apportée par les modifications apportées au système. Si vous ne savez pas si le bouton de réglage A fait monter la métrique B et la métrique C, vous ne savez pas comment réparer votre système lorsque des personnes poussent du code inopinément malin sur votre système (et le feront ensuite sur votre système) .
Comprenez ce que vous devez faire ensuite pour améliorer les choses. Jusqu'à ce que vous sachiez que les applications manquent de mémoire, vous ne pouvez pas déterminer si vous devez obtenir plus de mémoire ou acheter plus de disque pour vos serveurs.
Les métriques sont tellement cruciales et essentielles que j'ai fait un effort conscient pour planifier ce que je veux mesurer avant même de penser à ce qu'un système nécessitera. En fait, les métriques sont tellement cruciales que je crois qu’elles sont la bonne réponse à cette question: vous ne savez que ce qui peut être fait de manière séquentielle ou simultanée lorsque vous mesurez ce que font les bits de votre programme. Une conception appropriée utilise des chiffres et non des conjectures.
Cela étant dit, il existe certainement quelques règles de base:
Séquentielle implique la dépendance. Deux processus doivent être séquentiels si l’un dépend de l’autre. Les processus sans dépendance doivent être simultanés. Cependant, prévoyez un moyen de gérer les échecs en amont qui n'empêche pas les processus en aval d'attendre indéfiniment.
Ne mélangez jamais une tâche liée aux E / S avec une tâche liée au processeur sur le même noyau. N'écrivez pas (par exemple) un robot d'exploration Web qui lance dix demandes simultanées dans le même fil, les récupère dès qu'elles arrivent et s'attend à passer à cinq cents - les demandes d'E / S sont placées dans une file d'attente en parallèle, mais la CPU les passera toujours en série. (Ce modèle événementiel mono-threadé est populaire, mais il est limité à cause de cet aspect - plutôt que de comprendre cela, les gens se tordent simplement la main et disent que Node n'échelle pas, pour vous donner un exemple).
Un seul thread peut faire beaucoup de travail d'E / S. Mais pour utiliser pleinement la simultanéité de votre matériel, utilisez des pools de threads qui occupent tous les cœurs. Dans l'exemple ci-dessus, le lancement de cinq processus Python (chacun pouvant utiliser un cœur sur une machine à six cœurs) juste pour le travail du processeur et un sixième thread Python juste pour le travail d'E / S évolueront beaucoup plus rapidement que vous ne le pensez.
La seule façon de tirer parti de la concurrence du processeur consiste à utiliser un pool de threads dédié. Un seul thread est souvent suffisant pour de nombreux travaux liés aux E / S. C’est pourquoi les serveurs Web événementiels tels que Nginx s’adaptent mieux (qu’ils effectuent un travail purement lié aux E / S) et Apache (qui confond le travail lié aux E / S avec quelque chose nécessitant un processeur et lance un processus par requête), mais pourquoi utiliser Node pour les exécuter des dizaines de milliers de calculs GPU reçus en parallèle est une idée terrible .