Je suis passionné par mon intérêt pour le développement de logiciels, en particulier le puzzle de la création de systèmes logiciels ergonomiques qui résolvent l'ensemble de problèmes le plus large tout en faisant le moins de compromis possible. J'aime aussi me considérer comme un développeur de systèmes, qui, selon la définition d'Andrew Kelley, signifie un développeur intéressé à comprendre complètement les systèmes avec lesquels ils travaillent. Dans ce blog, je partage avec vous, mes idées sur la résolution du problème suivant: construisant une application d'entreprise file et performante en plein essor . Tout un défi, n'est-ce pas? Dans le blog, je me concentre sur la partie "Performant Web Server" - c'est là que je pense que je peux offrir une nouvelle perspective, car le reste est soit bien étendu, soit je n'ai rien à ajouter.
Une mise en garde majeure - il y aura aucun échantillon de code , je n'ai pas vraiment testé cela. Oui, c'est un défaut majeur, mais en fait, la mise en œuvre de cela prendrait beaucoup de temps, ce que je n'ai pas, et entre publier un blog défectueux et ne pas le publier du tout, je suis resté avec le premier. Vous avez été averti.
et de quels pièces allions-nous assembler notre application?
avec nos outils décidés, commençons!
zig n'a pas de prise en charge de niveau de langue pour les coroutines :( et les coroutines est ce avec quoi chaque serveur Web performant est construit. Donc, il n'y a pas d'intérêt d'essayer?
Hold, on, mettons d'abord notre chapeau de programmeur de systèmes. Les coroutines ne sont pas une solution miracle, rien ne l'est. Quels sont les avantages et les inconvénients réels impliqués?
Il est de notoriété publique que les coroutines (threads d'espace utilisateur) sont plus légères et plus rapides. Mais de quelle manière exactement? (Les réponses ici sont en grande partie des spéculations, prenez avec un grain de sel et testez-le vous-même)
GO Runtime, par exemple, multiplexes Goroutines sur les threads OS. Les threads partagent le tableau des pages, ainsi que d'autres ressources appartenant à un processus. Si nous introduisons l'isolement du processeur et l'affinité avec le mélange - les threads s'exécuteront en continu sur leurs noyaux de CPU respectifs, toutes les structures de données du système d'exploitation resteront en mémoire sans avoir besoin d'être échangées, le planificateur d'espace utilisateur alloue le temps du processeur aux goroutines avec précision, car il utilise le modèle COOPERATIVE Multitasking. La concurrence est-elle même possible?
Les victoires de performance sont obtenues en mettant à l'écart l'abstraction au niveau OS d'un fil et en la remplaçant par celle d'un goroutine. Mais rien n'est perdu dans la traduction?
Je vais affirmer que l'abstraction de niveau OS "True" pour une unité d'exécution indépendante n'est même pas un thread - c'est en fait le processus OS. En fait, la distinction ici n'est pas aussi évidente - tout ce qui distingue les threads et les processus est les différentes valeurs PID et TID. Quant aux descripteurs de fichiers, à la mémoire virtuelle, aux gestionnaires de signaux, aux ressources suivis - si celles-ci sont séparées pour l'enfant est spécifiée dans les arguments au système "clone". Ainsi, j'utiliserai le terme "processus" pour signifier un thread d'exécution qui possède ses propres ressources système - principalement le temps du processeur, la mémoire, les descripteurs de fichiers ouverts.
Maintenant, pourquoi est-ce important? Chaque unité d'exécution a ses propres demandes de ressources système. Chaque tâche complexe peut être décomposée en unités, où chacune peut faire ses propres ressources prévisibles pour les ressources - mémoire et temps de processeur. Et plus l'arbre des sous-tâches que vous allez, vers une tâche plus générale - le graphique des ressources du système forme une courbe de cloche avec de longues queues. Et il est de votre responsabilité de vous assurer que les queues ne dépassent pas la limite des ressources système. Mais comment cela est-il fait, et que se passe-t-il si cette limite est en fait dépasse?
Si nous utilisons le modèle d'un seul processus et de nombreuses coroutines pour les tâches indépendantes, lorsqu'un coroutine dépasse la limite de mémoire - car l'utilisation de la mémoire est suivie au niveau du processus, l'ensemble du processus est tué. C'est dans le meilleur cas - si vous utilisez des CGROUP (qui est automatiquement le cas pour les pods dans Kubernetes, qui ont un CGROUP par pod) - l'ensemble du CGROUP est tué. La création d'un système fiable doit être prise en compte. Et qu'en est-il du temps du processeur? Si notre service est touché par de nombreuses demandes à forte intensité de calcul en même temps, elle ne répondra pas. Puis les échéances, les annulations, les tentatives, les redémarrages suivent.
La seule façon réaliste de gérer ces scénarios pour la plupart des piles de logiciels traditionnelles est de laisser la "graisse" dans le système - certaines ressources inutilisées pour la queue de la courbe de cloche - et limiter le nombre de demandes simultanées - qui, encore une fois, conduit à des ressources non utilisées. Et même avec cela, nous serons tués ou ne répondrons pas de temps en temps - y compris pour des demandes "innocentes" qui se trouvent dans le même processus que la valeur aberrante. Ce compromis est acceptable pour beaucoup et sert suffisamment de systèmes logiciels en pratique. Mais pouvons-nous faire mieux?
Étant donné que l'utilisation des ressources est suivie par processus, idéalement, nous engendrerions un nouveau processus pour chaque petite unité d'exécution prévisible. Ensuite, nous définissons l'ulimit pour le temps et la mémoire du processeur - et nous sommes prêts à partir! Ulimit a des limites douces et dures, ce qui permettra au processus de se terminer gracieusement en atteignant la limite souple, et si cela ne se produit pas, peut-être en raison d'un bug - être terminé avec force en atteignant la limite dure. Malheureusement, la création de nouveaux processus sur Linux est lente, le report de nouveau processus par demande n'est pas pris en charge pour de nombreux cadres Web, ainsi que d'autres systèmes tels que Temporal. De plus, la commutation de processus est plus chère - qui est atténuée par la broche de vache et de processeur, mais toujours pas idéale. Les processus de longue date sont une réalité inévitable, malheureusement.
Plus nous allons de plus en plus de l'abstraction propre des processus de courte durée, plus nous aurions besoin de travailler de niveau OS pour prendre soin de nous. Mais il y a aussi des avantages à gagner - comme l'utilisation de IO_URING pour un lots IO entre de nombreux threads d'exécution. En fait, si une grande tâche est constituée de sous-tâches - nous soucions-nous vraiment de leur utilisation individuelle des ressources? Uniquement pour le profilage. Mais si pour la grande tâche, nous pouvions gérer (couper) la queue de la courbe de cloche de ressource, ce serait assez bon. Ainsi, nous pourrions engendrer autant de processus que les demandes que nous souhaitons gérer simultanément, les faire vivre à longue durée de vie et réajuster simplement l'ulimit pour chaque nouvelle demande. Ainsi, lorsqu'une demande dépasse ses contraintes de ressources, il obtient un signal de système d'exploitation et est en mesure de terminer gracieusement, sans affecter d'autres demandes. Ou, si l'utilisation élevée des ressources est intentionnelle, nous pourrions dire au client de payer un quota de ressources plus élevé. Cela me semble plutôt bien.
mais la performance en souffrira toujours, par rapport à une approche coroutine par réflexion. Tout d'abord, la copie autour du tableau de mémoire du processus coûte cher. Étant donné que le tableau contient des références aux pages de mémoire, nous pourrions utiliser des pages énormes, limitant ainsi la taille des données à copier. Ceci n'est directement possible avec les langues de bas niveau, comme le zig. De plus, le multitâche du niveau du système d'exploitation est préemptif et non coopératif, ce qui sera toujours moins efficace. Ou est-ce?
Il y a le syscall sched_yield, qui permet au fil de renoncer au CPU lorsqu'il a terminé sa partie des travaux. Semble assez coopératif. Pourrait-il également y avoir un moyen de demander une tranche de temps d'une taille donnée? En fait, il y a - avec la stratégie de planification sched_deadline. Il s'agit d'une politique en temps réel, ce qui signifie que pour la tranche de temps de processeur demandée, le thread s'exécute sans interruption. Mais si la tranche est dépassée - la préemption entre en jeu et que votre fil est échangé et dépréné. Et si la tranche est sous-rugissable - le thread peut appeler sched_ield pour signaler une finition précoce, permettant à d'autres threads d'exécuter. Cela ressemble au meilleur des deux mondes - un modèle coopératif et préemtive.
Une limitation est le fait qu'un thread sched_deadline ne peut pas déborder. Cela nous laisse deux modèles pour la concurrence - soit un processus par demande, qui se fixe la date limite, et exécute une boucle d'événement pour une IO efficace, ou un processus qui, dès le début, engendre un fil pour chaque micro-tâche, chacun établit sa propre date limite, et utilise des files d'attente pour la communication les uns avec les autres. Le premier est plus discret, mais nécessite une boucle d'événement dans l'espace utilisateur, le second fait davantage usage du noyau.
Les deux stratégies atteignent la même extrémité que le modèle Coroutine - en coopérant avec le noyau, il est possible que les tâches d'application s'exécutent avec des interruptions minimales .
Tout cela est pour le côté des choses à haute performance, à faible latence, de bas niveau, où le zig brille. Mais en ce qui concerne les activités réelles de l'application, la flexibilité est beaucoup plus précieuse que la latence. Si un processus implique de vraies personnes qui s'inscrivent sur des documents - la latence d'un ordinateur est négligeable. De plus, malgré la souffrance de performance, les langues orientées objet donnent au développeur de meilleures primitives pour modéliser le domaine de l'entreprise avec. Et à la fin la plus éloignée de cela, des systèmes comme Flowable et Camunda permettent au personnel de gestion et d'opérations de programmer la logique commerciale avec plus de flexibilité et une barrière d'entrée plus faible. Les langues comme Zig ne vous aideront pas et ne vous soutiendront que votre chemin.
Python, en revanche, est l'une des langues les plus dynamiques qui existe. Classes, objets - Ce sont tous des dictionnaires sous le capot et peuvent être manipulés au moment de l'exécution comme vous le souhaitez. Cela a une pénalité de performance, mais rend la modélisation de l'entreprise avec des classes et des objets et de nombreuses astuces intelligentes pratiques. Le zig est l'opposé de cela - il y a intentionnellement peu de trucs intelligents en zig, vous donnant un contrôle maximal. Pouvons-nous combiner leurs pouvoirs en les faisant interagir?
En effet, nous pouvons, en raison de soutenir les deux C Abi. Nous pouvons faire fonctionner l'interpréteur Python à partir du processus en zig, et non comme un processus distinct, réduisant les frais généraux dans le coût d'exécution et le code de colle. Cela nous permet en outre d'utiliser les allocateurs personnalisés de Zig dans Python - définir une arène pour traiter la demande individuelle, réduisant ainsi sinon éliminer les frais généraux d'un collecteur de déchets et définir un capuchon de mémoire. Une limitation majeure serait les fils de frai d'exécution de CPYTHON pour la collection des ordures et l'EI, mais je n'ai trouvé aucune preuve qu'il le faisait. Nous pourrions accrocher Python dans une boucle d'événement personnalisée en zig, avec suivi de la mémoire par corétine, en utilisant le champ "Context" dans AbstractMemoryloop. Les possibilités sont illimitées.
Nous avons discuté des mérites de la concurrence, du parallélisme et de diverses formes d'intégration avec le noyau OS. L'exploration manque de références et de code, ce que j'espère qu'elle compenserait la qualité des idées offertes. Avez-vous essayé quelque chose de similaire? Que pensez-vous? Commentaires bienvenus:)
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3