Configuration de WCF
J’ai constaté à de nombreuses reprises que des problèmes de montée en charge revenaient souvent dès lors qu’on utilisait WCF. WCF, pour Windows Communication Foundation, est le protocole lié au framework .Net qui permet la communication distante. Cela est, la plupart du temps, du à une vision « magique » de WCF, qui est censé fonctionner tout seul, sans configuration. Sauf qu’en réalité, cela ne marche pas. Voici donc quelques clefs pour comprendre comment configurer et utiliser WCF.
Configuration
WCF possède plusieurs paramètres de configuration qui déterminent sa manière de gérer les requêtes. Une analogie qui permet de bien comprendre leur rôle est celle de Theme Hospital, un jeu datant d’un certain nombre d’années.
Parcours d’un malade
Dans cette métaphore, un appel WCF est comparé à un malade qui va se faire soigner. Tout d’abord il détermine comment se rendre à l’hôpital, s’il y a plusieurs moyens, puis se présente à l’accueil pour créer un dossier, va ensuite en salle d’attente en attendant que soient libre à la fois une salle de consultation et un médecin pour le recevoir. Il va alors à cette consultation, qui représente le traitement effectif de la demande. Les différents éléments de ce parcours sont guidés par des paramètres de configuration, que nous allons détailler.
MaxConcurrentInstance
Ce paramètre représente le nombre de moyens d’accès à votre hôpital (routes, hélicoptère, ambulance, téléportation,…). Selon la manière dont est configuré un autre paramètre, InstanceContextMode, la stratégie des patients sera différente.
- InstanceContextMode=Single : Tout le monde emprunte la même voie d’accès. Le paramètre MaxConcurrentInstance ne sert donc à rien
- InstanceContextMode=PerSession : Tous les patients venant du même endroit (i.e. toutes les requêtes venant du même client) empruntent la même voie d’accès. Il y a donc besoin d’autant de voie d’accès que de clients différents
- InstanceContextMode=PerCall : Chaque patient emprunte sa voie propre. Il faut donc prévoir un nombre important de voies d’accès, et donc une valeur élevée pour le paramètre MaxConcurrentInstance
MaxConcurrentSessions
Ce paramètre représente le nombre de salles de consultation, et donc le nombre de requêtes qu’il est possible de traiter en parallèle. Attention cependant, une salle de consultation sans médecin est inutile. Ce paramètre doit donc avoir une valeur proche de MaxConcurrentCalls, bien que légèrement inférieure, afin de ne pas avoir de salle vide. Il faut également noter que, les malades étant quelque peu élitistes, ils souhaitent une salle de consultation dédiée à eux et aux malades provenant du même endroit. Une session est donc créée par client se connectant.
MaxConcurrentCalls
Ce paramètre représente le nombre de médecins. Il y en faut un peu plus que de salles de consultation, parce qu’il arrive qu’un médecin fasse une pause. Techniquement, cela signifie qu’une fois la session créée, il faut libérer du temps processeur pour traiter l’appel, et que cela se base sur le nombre de threads allouées par le ThreadPool (sur lequel nous allons revenir). Il faut donc prévoir les temps de switch et de concurrence entre threads qui peuvent conduire à la « pause » prise par les médecins.
MaxConnections
Ce paramètre représente la taille de la salle d’attente. Cela est quelque peu biaisé dans la mesure où sont comptabilisés aussi bien les malades en attente que ceux en consultation. Ainsi, l’augmentation de ce paramètre fournira une plus grande possibilité de mise en tampon des requêtes.
ListenBacklog
Ce paramètre représente le nombre de sièges devant l’accueil. Les patients ne sont pas encore enregistrés auprès de l’hôpital et attendent qu’une charmante secrétaire veuille bien s’occuper d’eux. De la même manière que dans le monde réel, ce paramètre doit être maintenu assez bas. Le patient n’est en effet pas encore dans le système et perdra vite patience. Cela correspond à une connexion TCP en état Half-open et se traduit par un timeout assez court au niveau du système. Augmenter ce paramètre conduit donc simplement à délayer la remontée d’erreur au client, et à changer la nature de l’erreur qui passe de « il n’y a plus de place » à « j’ai attendu trop longtemps », ce qui complique le débogage.
Préconisations
Bien qu’il soit impossible de donner des valeurs idéales pour ces paramètres, puisqu’elles dépendent bien évidemment du contexte applicatif, il est possible de donner des relations entre ces paramètres :
- MaxConcurrentInstance doit être dimensionné en fonction de la stratégie définie par le paramètre InstanceContextMode et du nombre de clients.
- MaxConcurrentSessions doit être dimensionné en fonction du nombre de clients.
- MaxConcurrentCalls doit être légèrement supérieur à MaxConcurrentSessions.
- MaxConnections doit être supérieur aux deux précédents et dimensionné en fonction des timeout positionnés. Il faut en effet éviter que les malades en salle d’attente perdent patience, là encore dans l’optique de remonter au plus vite les erreurs.
- ListenBacklog doit être bas.
À part pour ce dernier, il est à noter que sur dimensionner les paramètres n’a que très peu d’influence sur les performances. En effet, il s’agit de maximums, le système ne créant les ressources qu’à la demande. Il faut cependant tenir compte des pics de communication, afin d’éviter que la création et le maintien des ressources liées à WCF ne prenne tout le temps de processeur disponible.
Autres éléments
ThreadPool
Tout est configuré, les paramètres sont bons, et pourtant, cela ne fonctionne toujours pas ? Normal, vous n’avez pas assez de threads dans votre ThreadPool. Avec là encore, l’effet baguette magique, qui pousse à croire que le ThreadPool va fonctionner pour vous dans sa configuration par défaut. Pour comprendre l’influence du ThreadPool, il faut savoir que d’une part, chaque appel WCF est traité dans un thread d’I/O asynchrone, gérée par le ThreadPool, et d’autre part, que le ThreadPool à un fonctionnement particulier.
En effet, l’objectif ultime du ThreadPool, sa raison d’être, est d’amener ou de ramener le nombre de worker threads et de thread d’I/O asynchrones à son idéal, qui est par défaut égal au nombre de cœurs logiques de la machine. Par ailleurs, il est cadencé à 500ms, et obéit à l’algorithme suivant :
Sur un appel :
Si nombre_de_threads < idéal
Alors créer un thread
Sinon mettre en attente
Sur déclenchement du timer interne (500ms)
Si il y a au moins un demande en attente
Alors créer un thread
Sinon supprimer un thread
Pour illustrer son fonctionnement, prenons un cas d’école. Imaginons que chacune de vos threads peut traiter 10 appels par 500 ms, que vous recevez 50 appels par 500 ms et que vous avez 2 cœurs. On obtient le graphe suivant :
Figure 1 Évolution des traitements WCF dans le temps
On s’aperçoit que le fonctionnement du ThreadPool le fait osciller autour de l’idéal. Dans la pratique, le ThreadPool étant optimisé, on finit par arriver à l’idéal de traitement (dans notre cas, 5 threads), mais on s’aperçoit que cela peut prendre du temps et que, pendant cette période de ‘calibration’, les requêtes entrantes peuvent patienter un temps très long, au point même parfois de déclencher des timeouts.
La solution consiste à régler manuellement l’idéal du ThreadPool. Cela se fait par l’appel à la méthode System.Threading.ThreadPool.SetMinThreads, en dimensionnant les paramètres de l’appel en fonction de la capacité de traitement de vos services WCF. Attention cependant à ne pas trop relever ce paramètre, puisque cela conduit à la création de nombreux threads, ce qui peut être dommageable (temps de switch + taille mémoire de chaque thread)
Dans la pratique, ce nombre est largement supérieur au nombre de cœurs logiques de la machine. Il ne faut donc pas hésiter à monter jusqu’à une cinquantaine de threads par cœurs, sans que cela n’impacte réellement les performances, tout en gardant en tête que le dimensionnement idéal est « au plus juste » et se découvre par essais successifs.
Conclusion
Il n’y a pas de baguette magique ! Même si WCF est plus simple à utiliser que ses ainés, il demeure cependant qu’il faut savoir le configurer correctement afin d’en tirer le plein potentiel.