Comment concevez-vous l’architecture d’un système multicœur à tolérance de pannes dissortingbué basé sur Erlang / OTP?

Je voudrais construire un système basé sur Erlang / OTP, qui résout un problème «extrêmement déroutant».

J’ai déjà lu / parcouru:

  • Apprenez un peu Erlang;
  • Programmation Erlang (Armstrong);
  • Programmation Erlang (Cesarini);
  • Erlang / OTP en action.

J’ai l’essentiel des processus, de la messagerie, des superviseurs, des serveurs génériques, de la journalisation, etc.

Je comprends que certains choix d’architecture dépendent de l’application concernée, mais j’aimerais quand même connaître quelques principes généraux de la conception du système ERlang / OTP.

Dois-je simplement commencer par quelques serveurs génériques avec un superviseur et en tirer progressivement parti?

Combien de superviseurs devrais-je avoir? Comment est-ce que je décide quelles parties du système devraient être basées sur le processus? Comment devrais-je éviter les goulots d’étranglement?

Devrais-je append la journalisation plus tard?

Quelle est l’approche générale de l’architecture des systèmes multiprocesseurs à tolérance de pannes dissortingbués d’Erlang / OTP?

Dois-je simplement commencer par quelques serveurs génériques avec un superviseur et en tirer progressivement parti?

Vous manquez un composant clé dans les architectures Erlang ici: les applications! (C’est-à-dire le concept d’applications OTP, pas d’applications logicielles).

Considérez les applications comme des composants. Un composant de votre système résout un problème particulier, est responsable d’un ensemble cohérent de ressources ou d’un élément abstrait important ou complexe du système.

La première étape de la conception d’un système Erlang consiste à décider quelles applications sont nécessaires. Certaines peuvent être extraites du Web telles qu’elles sont appelées bibliothèques. D’autres vous aurez besoin d’écrire vous-même (sinon vous n’auriez pas besoin de ce système particulier). Ces applications, que nous appelons généralement la logique métier (vous devez souvent écrire certaines bibliothèques vous-même, mais il est utile de garder la distinction entre les bibliothèques et les applications métier de base qui lient tout).

Combien de superviseurs devrais-je avoir?

Vous devriez avoir un superviseur pour chaque type de processus que vous souhaitez surveiller.

Une bande de travailleurs temporaires identiques? Un superviseur pour les gouverner tous.

Processus différent avec différentes responsabilités et stratégies de redémarrage? Un superviseur pour chaque type de processus, dans une hiérarchie correcte (selon le moment où les choses doivent redémarrer et quel autre processus doit être interrompu avec eux?).

Parfois, il est possible de placer plusieurs types de processus sous le même superviseur. C’est généralement le cas lorsque vous avez quelques processus singleton (par exemple, un superviseur de serveur HTTP, un processus propriétaire de table ETS, un collecteur de statistiques) qui sera toujours exécuté. Dans ce cas, il peut être trop difficile d’avoir un superviseur pour chacun, il est donc courant d’append le superviseur sous un. Soyez simplement conscient des implications de l’utilisation d’une stratégie de redémarrage particulière lorsque vous faites cela, par exemple, si votre serveur Web se bloque ( one_for_one est la stratégie la plus courante à utiliser dans de tels cas). Veillez à ne pas avoir de dépendance entre les processus dans un superviseur one_for_one . Si un processus dépend d’un autre processus en panne, il peut également tomber en panne, ce qui déclenche trop souvent l’intensité du redémarrage des superviseurs et provoque le crash du superviseur lui-même trop tôt. Cela peut être évité en ayant deux superviseurs différents, qui contrôleraient complètement les redémarrages en fonction de l’intensité et de la période configurées ( explication plus longue ).

Comment est-ce que je décide quelles parties du système devraient être basées sur le processus?

Chaque activité concurrente dans votre système doit être dans son propre processus. Avoir la mauvaise abstraction de la concurrence est l’erreur la plus courante des concepteurs de systèmes Erlang au début.

Certaines personnes ne sont pas habituées à gérer la concurrence. leurs systèmes ont tendance à en avoir trop peu. Un processus ou quelques gigantesques qui exécutent tout en séquence. Ces systèmes sont généralement pleins d’odeur de code et le code est très rigide et difficile à refactoriser. Cela les ralentit également, car ils ne peuvent pas utiliser tous les cœurs disponibles pour Erlang.

D’autres personnes saisissent immédiatement les concepts de concurrence mais ne parviennent pas à les appliquer de manière optimale. leurs systèmes ont tendance à abuser du concept de processus, ce qui rend de nombreux processus inactifs en attente d’autres qui travaillent. Ces systèmes ont tendance à être inutilement complexes et difficiles à déboguer.

Essentiellement, dans les deux variantes, vous rencontrez le même problème, vous n’utilisez pas toute la concurrence disponible et vous n’obtenez pas les meilleures performances du système.

Si vous vous en tenez au principe de la responsabilité unique et que vous respectez la règle pour avoir un processus pour chaque activité réellement concurrente dans votre système, vous devriez vous en sortir.

Il y a des raisons valables d’avoir des processus inactifs. Parfois, ils conservent un état important, parfois vous souhaitez conserver certaines données temporairement et plus tard, ignorer le processus, parfois ils attendent des événements externes. Le plus gros inconvénient est de transmettre des messages importants à travers une longue chaîne de processus largement inactifs, car cela ralentira votre système avec beaucoup de copies et utilisera plus de mémoire.

Comment devrais-je éviter les goulots d’étranglement?

Difficile à dire, cela dépend beaucoup de votre système et de ce qu’il fait. En règle générale, cependant, si vous répartissez les responsabilités entre les applications, vous devriez pouvoir adapter l’application qui semble être le goulot d’étranglement distinct du rest du système.

La règle d’or ici est de mesurer, mesurer, mesurer ! Ne pensez pas que vous avez quelque chose à améliorer jusqu’à ce que vous ayez mesuré.

Erlang est génial en ce sens qu’il vous permet de masquer la concurrence derrière des interfaces (connues sous le nom de simultanéité implicite). Par exemple, vous utilisez une API de module fonctionnel, une interface normale de module:function(Arguments) , qui peut à son tour générer des milliers de processus sans que l’appelant le sache. Si vous avez vos abstractions et votre API correctement, vous pouvez toujours paralléliser ou optimiser une bibliothèque après l’avoir utilisée.

Cela étant dit, voici quelques lignes direcsortingces générales:

  • Essayez d’envoyer des messages directement au destinataire, évitez de canaliser ou d’acheminer des messages via des processus intermédiaires. Sinon, le système passe simplement du temps à déplacer des messages (données) sans vraiment fonctionner.
  • N’abusez pas des modèles de conception OTP, tels que gen_servers. Dans de nombreux cas, il vous suffit de démarrer un processus, d’exécuter du code, puis de quitter. Pour cela, un gen_server est excessif.

Et un conseil de bonus: ne pas réutiliser les processus. Créer un processus à Erlang est tellement bon marché et rapide qu’il est inutile de réutiliser un processus une fois sa durée de vie écasting. Dans certains cas, il peut être judicieux de réutiliser l’état (par exemple, l’parsing complexe d’un fichier), mais il est préférable de le conserver de façon canonique ailleurs (dans une table ETS, une firebase database, etc.).

Devrais-je append la journalisation plus tard?

Vous devriez append la journalisation maintenant! Il existe une excellente API intégrée appelée Logger fournie avec Erlang / OTP à partir de la version 21:

 logger:error("The file does not exist: ~ts",[Filename]), logger:notice("Something strange happened!"), logger:debug(#{got => connection_request, id => Id, state => State}, #{report_cb => fun(R) -> {"~p",[R]} end}), 

Cette nouvelle API possède plusieurs fonctionnalités avancées et devrait couvrir la plupart des cas nécessitant une journalisation. Il y a aussi la bibliothèque Lager plus ancienne mais encore largement utilisée.

Quelle est l’approche générale de l’architecture des systèmes multiprocesseurs à tolérance de pannes dissortingbués d’Erlang / OTP?

Pour résumer ce qui a été dit ci-dessus:

  • Divisez votre système en applications
  • Mettez vos processus dans la hiérarchie de supervision appropriée, en fonction de leurs besoins et dépendances
  • Avoir un processus pour chaque activité réellement concurrente dans votre système
  • Maintenir une API fonctionnelle vers les autres composants du système. Cela vous permet de:
    • Refactorez votre code sans changer le code qui l’utilise
    • Optimiser le code ensuite
    • Dissortingbuez votre système en cas de besoin (appelez simplement un autre nœud derrière l’API! L’appelant ne le remarquera pas!)
    • Tester plus facilement le code (moins de travail à mettre en place des faisceaux de test, plus facile à comprendre comment l’utiliser)
  • Commencez à utiliser les bibliothèques disponibles dans OTP jusqu’à ce que vous ayez besoin de quelque chose de différent (vous le saurez, le moment venu)

Pièges courants:

  • Trop de processus
  • Trop peu de processus
  • Trop de routage (messages transférés, processus chaînés)
  • Trop peu d’applications (je n’ai jamais vu le cas contraire, en fait)
  • Pas assez d’abstraction (cela rend difficile de refaire et de raisonner. Cela rend également le test difficile!)