Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Patterns pour haute dispo et scalabilité d’une appli web – Partie II

Poster un commentaire

Exploiter l’asynchronisme

Un thread est occupé lorsqu’il exécute du code. Cela ne signifie pas qu’il soit réellement utilisé. Il peut être en attente pour une des raisons suivantes:
  1. Le code effectue des manipulations de données en mémoire, il est en attente qu’une CPU devienne disponible.
    On parle de code lié à la CPU (CPU-bound)
  2. Le code est en attente d’une requête vers un composant situé en dehors de son process, il est alors I/O bound.
Ces situations ne sont pas très favorables car si le thread ne travaille pas, il n’est pas pour autant disponible pour d’autres tâches.
Une meilleure situation serait de le recycler en attendant que la cause du blocage se résolve.
Les techniques d’asynchronismes permettent d’organiser ce recyclage. Toutefois, l’asynchronisme a un coût. De plus, dans la mesure où le nombre de CPU est tout de même très limité sur un serveur, la mise en oeuvre de code asynchrone est surtout pertinente pour du code I/O bound.

Quel problème tentons nous de résoudre?

Prenons l’exemple du contrôleur synchrone CoursesController:


public ActionResult Index()
{
   var vm = CourseRepository.GetCourses();
   return View(vm);
}

 

Même si le client réalise un appel asynchrone le contrôleur est synchrone. Il va donc extraire un thread du pool géré (managé) qui a un moment ou un autre sera mis en attente, sans doute dans le repository, le temps d’interroger la base de données. Un thread sera donc bloqué côté serveur, mais pour le client tout est asynchrone.

 

Si une nouvelle requête se présente, un autre thread sera extrait du pool géré et il subira la même schéma. Dans le cas d’un site à faible trafic ce n’est pas très gênant. Les choses sont différentes pour un site marchand le jour des soldes.

Le pool de threads n’offre qu’un nombre limité de threads. Ce nombre dépend du contexte d’exécution, mais il ne peut être exagérément sans stresser fortement le kernel et c’est rarement une bonne idée. Une fois les limites atteintes, les requêtes suivantes n’auront pas d’autres choix que d’être mises en attente ou refusées, mais on risque surtout de déstabiliser le système.

Vous trouverez ici une discussion intéressante (mais en Java) sur le dimensionnement du pool de threads:

https://www.infoq.com/fr/articles/Java-Thread-Pool-Performance-Tuning?utm_source=infoq_en&utm_medium=link_on_en_item&utm_campaign=item_in_other_langs

Asynchronisme, parallélisme, multi-threading

Trois termes à ne pas confondre, j’ai déjà abordé la question ici:

https://amethyste16.wordpress.com/2014/10/02/les-nouveaux-chemins-vers-lasynchronisme-i/

Je souhaite également conseiller la lecture d’un document proposé par Microsoft au sujet des patterns de parallélisme:

https://www.microsoft.com/en-us/download/details.aspx?id=19222

Ecriture d’un service asynchrone

L’idée d’un code asynchrone est de réutiliser les threads mis en attente pour faire autre chose et donc d’essayer de désengorger le site.

Un anti-pattern

.Net et le pattern async/await ont considérablement simplifiés l’écriture de méthodes asynchrones. Mais encore faut t’il faire les choses correctement.

On pourrait être tenté de transformer le code de la façon suivante:

public async Task<ActionResult> Index()
{
   var task = Task<List<Course>>.Run(() =>
   {
      var vm = CourseRepository.GetCourses();
      return vm;
 
   });
 
   return View(await task);
}

 

Techniquement les choses fonctionnent, mais est-ce vraiment une méthode asynchrone?

Ok, le client va peut-être faire une requête asynchrone qui sera traitée par une action asynchrone, mais la ligne 5 reste synchrone quoi qu’il arrive. Task.Run() n’a pas de baguette magique. Un thread sera tout de même extrait du pool, ceux-ci sont toujours en nombre limité et une situation de blocage de threads pourra se produire dans les couches profondes du modèle, on a juste déplacé un peu le problème.

Le passage à un contexte asynchrone n’a de sens que si le code est asynchrone de bout en bout.

 

Il existe quelques repères et en particulier je vous conseille de bien apprendre ces 3 règles, juste 3 :

  1. Pas de Task.Run si le code est limité par le pool de thread
  2. Eviter Task.Run dans une méthode de librairie.
    C’est au code client de faire ce choix. Lui seul connaît le contexte d’utilisation (nombre de CPU, scalabilité, est t’on déjà sur un thread d’arrière plan ou sur un UI thread…) et peut décider si c’est approprié.
  3. Pas de Task.Run sur du code serveur.
    En tout cas pas si vous recherchez la scalabilité.

Il y a des exceptions et aussi des contre exemples, mais avec ces règles on peut déjà aller loin. Lisez la première référence en bibliographie si vous voulez en savoir plus.

 

Dans tous les cas, jouez le jeu. Si votre méthode est suffixée par ‘Async’ et/ou renvoie Task, elle DOIT être réellement asynchrone.

Une meilleure solution

Nous avons tout d’abord besoin d’une repository véritablement asynchrone. C’est un scénario que propose justement Entity Framework.

 

On commence en bas de l’échelle avec le repositoty, puis on remonte la chaîne des appels jusqu’au contrôleur:

public async Task<List<Course>> GetCoursesAsync(int recordsPerPage, int currentPage)
{
   List<Course> courses = await Context.Courses
      .Where(c => c.Date >= DateTime.Now)
      .ToListAsync();
 
   return courses;
}

 

Ce qui nous permet de réécrire le contrôleur:


public async Task<ActionResult> Index()
{
   var vm = await CourseRepository
      .GetCoursesAsync();
   return View(vm);
}

Et cette fois tout est parfaitement asynchrone.

Bien sûr un thread est toujours consommé quelque part et qui sait lui-même est en attente sur un autre serveur ou dans un autre processus. Mais pas dans le pool de votre application et c’est tout ce qui nous importe car nous libérons un thread qui peut travailler au lieu de se mettre en attente.

Vues partielles

MVC Core arrive avec son lot de nouveautés et en particulier une qui nous permet de charger des vues partielles de façon asynchrone:

La pagination est par exemple sous traitée à la vue partielle _Paginate.cshtml que l’on charge ainsi avec Razor:


@Html.Partial("_Paginate")

 

Une nouvelle construction est maintenant disponible:


@await @Html.PartialAsync("_Paginate")

Le gain peut être appréciable pour des composants complexes.

Projet de démo

Le projet se trouve sur mon espace Github:

https://github.com/DeLeneMirouze/EscarGo

Il s’agit de EscarGoAsync

Bibiographie

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s