Partager via


Scalabilité

Le terme, l’extensibilité, est souvent mal utilisé. Pour cette section, une double définition est fournie :

  • L’extensibilité est la possibilité d’utiliser entièrement la puissance de traitement disponible sur un système multiprocesseur (2, 4, 8, 32 ou plus de processeurs).
  • L’extensibilité est la possibilité de traiter un grand nombre de clients.

Ces deux définitions connexes sont couramment appelées scale-up. La fin de cette rubrique fournit des conseils sur scale-out.

Cette discussion se concentre exclusivement sur l’écriture de serveurs évolutifs, et non sur les clients évolutifs, car les serveurs évolutifs sont des exigences plus courantes. Cette section traite également de l’extensibilité dans le contexte des serveurs RPC et RPC uniquement. Les meilleures pratiques pour l’extensibilité, telles que la réduction de la contention, l’absence fréquente de cache sur les emplacements de mémoire globale ou l’évitement de faux partages, 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 adaptatifs qui augmente et diminue à mesure que la charge de travail varie. À 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 zéro à faible routine de serveur de contention. Cela signifie que le pool de threads RPC augmente de manière agressive le nombre de threads de maintenance si certains deviennent bloqués. Il opère 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 de contention faible. Par exemple, un serveur RPC d’appel void fonctionnant sur un serveur 550 MHz huit processeurs accessible sur un réseau san (High Speed System Area Network) sert plus de 30 000 appels void par seconde à partir 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 obtient réellement la façon dont la contention sur le serveur est élevée. Pour illustrer, imaginez un serveur lourd utilisé pour accéder à distance aux fichiers. Supposons que le serveur adopte l’approche la plus simple : il lit/écrit simplement le fichier de façon synchrone sur le thread sur lequel rpc appelle la routine du serveur. Supposons également que nous disposons d’un serveur à quatre processeurs servant de nombreux clients.

Le serveur commence par cinq threads (cela varie réellement, mais cinq threads sont utilisés pour simplifier). Une fois RPC récupéré 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 bloque, le cinquième thread est libéré pour récupérer une demande, et un sixième thread est créé en tant que veille à chaud. En supposant que chaque dixième opération d’E/S manque le cache et bloque pendant 100 millisecondes (une valeur de temps arbitraire), et en supposant que le serveur à quatre processeurs sert environ 20 000 appels par seconde (5 000 appels par processeur), une modélisation simpliste prédit que chaque processeur génère environ 50 threads. Cela suppose qu’un appel qui va bloquer est fourni toutes les 2 millisecondes, et après 100 millisecondes, le premier thread est libéré de nouveau afin que le pool se stabilise à environ 200 threads (50 par processeur).

Le comportement réel est plus compliqué, 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 au fur et à mesure que les threads sur le serveur démarrent le blocage et attendent 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ôlent les demandes entrantes essaieront de conserver 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 y a quatre threads RPC utilisables ou plus, le cinquième thread n’est pas autorisé à récupérer une nouvelle requête et s’asseoira à la place dans un état de secours chaud au cas où l’un des blocs threads actuellement utilisables. Si le cinquième thread attend suffisamment longtemps en tant que secours chaud sans que le nombre de threads RPC utilisables tombe sous le nombre de processeurs, il sera libéré, autrement dit, le pool de threads diminue.

Imaginez un serveur avec de nombreux threads. Comme expliqué précédemment, un serveur RPC se retrouve avec de nombreux threads, mais seulement si les threads bloquent souvent. Sur un serveur où les threads bloquent souvent, un thread qui revient à RPC est bientôt retiré de la liste de secours à chaud, car tous les threads actuellement utilisables bloquent et reçoivent 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écute un code différent, accédant à différentes structures de données et aura une pile différente, ce qui signifie que le taux d’accès au cache de mémoire (les caches L1 et L2) sera beaucoup plus faible, ce qui entraînera une exécution plus lente. Les nombreux threads en cours d’exécution augmentent simultanément la contention des ressources existantes, telles que le tas, les sections critiques dans le code du serveur, etc. Cela augmente encore la contention à mesure que les convois se forment sur les ressources. Si la mémoire est faible, la sollicitation de la mémoire exercée par le grand nombre de threads entraîne la création d’erreurs de page, ce qui augmente davantage le taux de blocage des threads et entraîne la création d’un nombre encore plus élevé de threads. Selon la fréquence à laquelle il bloque et la quantité de mémoire physique disponible, le serveur peut se stabiliser à un niveau de performances inférieur avec un taux de commutateur de contexte élevé, ou il peut se dégrader jusqu’à ce qu’il accède uniquement à 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 amène rapidement le problème à la surface.

Comment cela peut-il être empêché ? Si les threads sont censés bloquer, déclarer des appels comme asynchrones et une fois que la requête entre dans la routine du serveur, placez-la en 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 effectuent ces appels asynchrones et assurez-vous que la file d’attente ne augmente pas trop grande. Si la routine du serveur effectue des E/S de fichier asynchrones, utilisez les E/S de fichier asynchrones pour mettre en file d’attente plusieurs demandes au système d’E/S et n’avoir que quelques threads en file d’attente et récupérez les résultats. Si la routine du serveur effectue de nouveau 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 autant de threads que 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 requête. Cela permet au serveur de s’exécuter avec autant de threads que 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 charge réseau est configuré afin que toutes les requêtes 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 terminent sur le même ordinateur serveur. Tant que cette condition est remplie, un cluster d’équilibrage de charge réseau peut être configuré pour fonctionner comme un serveur RPC volumineux avec une extensibilité potentiellement excellente.