Extensibilité
Le terme scalabilité est souvent mal utilisé. Pour cette section, une double définition est fournie :
- La scalabilité est la possibilité d’utiliser entièrement la puissance de traitement disponible sur un système multiprocesseur (2, 4, 8, 32 ou plusieurs processeurs).
- La scalabilité est la possibilité de traiter un grand nombre de clients.
Ces deux définitions connexes sont communément appelées scale-up. La fin de cette rubrique fournit des conseils sur le scale-out.
Cette discussion se concentre exclusivement sur l’écriture de serveurs évolutifs, et non de clients évolutifs, car les serveurs évolutifs sont des exigences plus courantes. Cette section traite également de la scalabilité dans le contexte des serveurs RPC et RPC uniquement. Les meilleures pratiques en matière de scalabilité, telles que la réduction des conflits, l’absence fréquente de cache sur les emplacements de mémoire globale ou la prévention du partage faux, ne sont pas abordées ici.
Modèle de thread rpc
Lorsqu’un appel RPC est reçu par un serveur, la routine du serveur (routine du gestionnaire) est appelée sur un thread fourni par RPC. RPC utilise un pool de threads adaptatif qui augmente et diminue à mesure que la charge de travail fluctue. À compter de Windows 2000, le cœur du pool de threads RPC est un port d’achèvement. Le port d’achèvement et son utilisation par RPC sont réglés pour des routines de serveur de contention nulle à faible contention. Cela signifie que le pool de threads RPC augmente fortement le nombre de threads de maintenance si certains sont bloqués. Il fonctionne sur la présomption que le blocage est rare, et si un thread est bloqué, il s’agit d’une condition temporaire qui est rapidement résolue. Cette approche permet d’améliorer l’efficacité des serveurs à faible contention. Par exemple, un serveur RPC d’appel vide fonctionnant sur un serveur à huit processeurs 550 MHz accessible via un réseau SAN (High Speed System Area Network) traite plus de 30 000 appels void par seconde provenant de plus de 200 clients distants. Cela représente plus de 108 millions d’appels par heure.
Le résultat est que le pool de threads agressif se met en route quand la contention sur le serveur est élevée. Pour illustrer, imaginez un serveur de grande capacité utilisé pour accéder à distance aux fichiers. Supposons que le serveur adopte l’approche la plus simple : il lit/écrit simplement le fichier de manière synchrone sur le thread sur lequel ce RPC appelle la routine du serveur. En outre, supposons que nous avons un serveur à quatre processeurs qui sert de nombreux clients.
Le serveur commence par cinq threads (cela varie en fait, mais cinq threads sont utilisés par souci de simplicité). Une fois que RPC récupère le premier appel RPC, il distribue l’appel à la routine du serveur, et la routine du serveur émet les E/S. Rarement, il manque le cache de fichiers, puis bloque l’attente du résultat. Dès qu’il se bloque, le cinquième thread est libéré pour récupérer une demande, et un sixième thread est créé en tant que serveur de secours. En supposant que chaque dixième opération d’E/S manque le cache et se bloque pendant 100 millisecondes (une valeur de temps arbitraire), et en supposant que le serveur à quatre processeurs traite environ 20 000 appels par seconde (5 000 appels par processeur), une modélisation simpliste prédirait que chaque processeur générera environ 50 threads. Cela suppose qu’un appel qui sera bloqué arrive toutes les 2 millisecondes, et après 100 millisecondes, le premier thread est libéré à nouveau afin que le pool se stabilise à environ 200 threads (50 par processeur).
Le comportement réel est plus complexe, car le nombre élevé de threads entraîne des commutateurs de contexte supplémentaires qui ralentissent le serveur et ralentissent également le taux de création de nouveaux threads, mais l’idée de base est claire. Le nombre de threads monte rapidement lorsque les threads sur le serveur commencent à se bloquer et à attendre quelque chose (qu’il s’agit d’une E/S ou d’un accès à une ressource).
RPC et le port d’achèvement qui contrôle les demandes entrantes essaient de maintenir le nombre de threads RPC utilisables dans le serveur pour qu’ils soient égaux au nombre de processeurs sur l’ordinateur. Cela signifie que sur un serveur à quatre processeurs, une fois qu’un thread revient à RPC, s’il existe au moins quatre threads RPC utilisables, le cinquième thread n’est pas autorisé à récupérer une nouvelle demande et se trouve à la place dans un état de secours à chaud au cas où l’un des threads actuellement utilisables bloque. Si le cinquième thread attend suffisamment longtemps en tant que serveur de secours sans que le nombre de threads RPC utilisables soit inférieur au nombre de processeurs, il sera libéré, c’est-à-dire que le pool de threads diminuera.
Imaginez un serveur avec de nombreux threads. Comme expliqué précédemment, un serveur RPC se retrouve avec de nombreux threads, mais uniquement si les threads bloquent souvent. Sur un serveur où les threads sont souvent bloqués, un thread qui retourne au RPC est bientôt retiré de la liste de secours à chaud, car tous les threads actuellement utilisables bloquent et reçoit une demande de traitement. Lorsqu’un thread bloque, le répartiteur de threads dans le noyau bascule le contexte vers un autre thread. Ce commutateur de contexte consomme lui-même des cycles d’UC. Le thread suivant exécutera un code différent, accédera à différentes structures de données et aura une pile différente, ce qui signifie que le taux d’accès au cache mémoire (caches L1 et L2) sera beaucoup plus faible, ce qui ralentit l’exécution. Les nombreux threads qui s’exécutent simultanément augmentent la contention pour les ressources existantes, telles que le tas, les sections critiques dans le code du serveur, etc. Cela augmente encore la contention à mesure que les convois sur les ressources se forment. Si la mémoire est faible, la pression de la mémoire exercée par le nombre important et croissant de threads entraîne des erreurs de page, ce qui augmente encore la vitesse à laquelle les threads bloquent et entraîne la création d’encore plus de threads. Selon la fréquence à laquelle il bloque et la quantité de mémoire physique disponible, le serveur peut soit se stabiliser à un niveau de performances inférieur avec un taux de commutateur de contexte élevé, soit se dégrader au point d’accéder à plusieurs reprises au disque dur et au changement de contexte sans effectuer de travail réel. Cette situation ne s’affiche pas sous une charge de travail légère, bien sûr, mais une charge de travail lourde met rapidement le problème en surface.
Comment peut-on empêcher cela ? Si les threads sont censés bloquer, déclarez les appels comme asynchrones et une fois que la demande entre dans la routine du serveur, faites-la file d’attente vers un pool de threads de travail qui utilisent les fonctionnalités asynchrones du système d’E/S et/ou RPC. Si le serveur effectue à son tour des appels RPC, rendez-les asynchrones et assurez-vous que la file d’attente ne s’agrandit pas trop. Si la routine du serveur effectue des E/S de fichiers, utilisez les E/S de fichier asynchrones pour mettre en file d’attente plusieurs demandes vers le système d’E/S et n’avoir que quelques threads en file d’attente et récupérer les résultats. Si la routine du serveur effectue des E/S réseau, utilisez à nouveau les fonctionnalités asynchrones du système pour émettre les demandes et récupérer les réponses de manière asynchrone, et utilisez le moins de threads possible. Lorsque l’E/S est terminée ou que l’appel RPC effectué par le serveur est terminé, effectuez l’appel RPC asynchrone qui a remis la demande. Cela permet au serveur de s’exécuter avec le moins de threads possible, ce qui augmente les performances et le nombre de clients qu’un serveur peut traiter.
Scale Out
RPC peut être configuré pour fonctionner avec l’équilibrage de charge réseau (NLB) si l’équilibrage de la charge réseau est configuré de telle sorte que toutes les demandes d’une adresse client donnée soient envoyées au même serveur. Étant donné que chaque client RPC ouvre un pool de connexions (pour plus d’informations, consultez RPC et le réseau), il est essentiel que toutes les connexions du pool du client donné se retrouvent sur le même ordinateur serveur. Tant que cette condition est remplie, un cluster NLB peut être configuré pour fonctionner en tant que serveur RPC de grande taille avec potentiellement une excellente scalabilité.