Faisant suite à DEVOXX France 2016, le blog INVIVOO consacre son mois de septembre à la « Programmation Reactive » avec une série d’articles qui vous a été présentée précédemment et qui vous proposera donc un retour sur la conférence « Architecture découplée grâce aux Reactive Extensions » présentée par  David Wursteisen et Brice Dutheil.

Démarrons sans plus attendre :

1 Besoin d’optimisation de performance dans une application web Java EE

La session commença par la présentation du cheminement des requêtes HTTP dans une application web Java EE s’exécutant sur un serveur Tomcat. L’aspect important à retenir dans ce processus est l’attribution exclusive d’un thread de travail à une connexion HTTP durant toute la vie de cette connexion. Les threads de travail font partie d’un pool de taille définie et la taille de ce pool détermine le nombre de connexions HTTP simultanées que le serveur est capable de recevoir et traiter.

Cheminement d'une requête HTTP dans une application Tomcat avec le connecteur BIO

Figure 1-1 Cheminement d’une requête HTTP dans une application Tomcat avec le connecteur BIO

L’objectif recherché est d’éviter autant que possible de rejeter des requêtes clientes pour cause d’indisponibilité du serveur et aussi de retourner le plus vite possible les réponses aux requêtes en cours de traitement. Examinons les possibilités dont nous disposons pour cela.

1.1 Augmentation de la quantité de ressources matérielles

L’augmentation du nombre de threads de travail est une solution évidente pour permettre de traiter un plus grand nombre de requêtes en parallèle, à condition bien sûr que le matériel utilisé dispose des ressources nécessaires pour cela. Dans l’absolu, disposer à volonté de ressources matérielles performantes permet de réduire le nombre de connexions à rejeter par le serveur. Cela étant convenu, avant d’en arriver là, il faut voir les possibilités d’optimisation existantes au niveau des serveurs d’application ou des applications exécutées.

1.2 Connecteurs NIO dans Tomcat

L’apparition de l’API NIO (Non-Blocking IO) dans Java 1.4 et ensuite leur support un peu plus tard dans les serveurs d’application à travers les connecteurs NIO apportera une amélioration considérable par rapport au nombre de connexions pouvant être acceptées simultanément par le serveur.  Avec le connecteur NIO, les threads de travail ne sont plus dédiés exclusivement à une connexion durant l’existence de celle-ci, ils sont plutôt partagés entre les connexions et sont libérés et rendus disponibles dès que la requête courante a été traitée. Le serveur devient ainsi capable d’accepter et maintenir un nombre de connexions supérieur au nombre de threads de travail.

Figure 1-2 Cheminement d'une requête HTTP dans une application Tomcat avec le connecteur NIO

Figure 1-2 Cheminement d’une requête HTTP dans une application Tomcat avec le connecteur NIO

1.3 Tuning

Le tuning de Java, des bibliothèques et frameworks utilisés est aussi un moyen de résoudre les problèmes de performance. Malheureusement c’est une solution qui demande une connaissance poussée de Java, et des dits frameworks ou bibliothèques, et qui n’est pas toujours accessible au commun des développeurs. Sachant en plus que les solutions de tuning ne sont pas toujours généralisables.

1.4 Limitations des solutions citées

En faisant impasse sur la solution de tuning et en nous recentrant sur les deux pistes que sont l’usage de ressources matérielles importantes et l’apport des connecteurs NIO, nous constatons que ces solutions répondent surtout au problème de limitation par rapport au nombre de connexions simultanées acceptées par le serveur. Elles n’apportent pas d’optimisation dans le traitement d’une requête consommatrice de temps et qui met le client en attente pendant une durée non négligeable. Lorsqu’un traitement prend du temps, le recours au découpage du traitement en plusieurs parties exécutables en concurrence s’invite comme solution naturelle, lorsque le traitement s’y prête bien entendu.

1.5 Optimisation du temps de traitement d’une requête

Dans le contexte d’une application d’entreprise typique Java, après avoir optimisé notre propre code, il reste toujours des services externes dont nous restons tributaires : bases de données, web services, etc. Effectuons une visite des familles d’outils permettant la parallélisation des traitements d’une application.

1.5.1 Executor et Future de l’API Concurrency

L’API Concurrency de Java met à disposition les Executor pour simplifier la gestion de tâches concurrentes. Un Executor retourne des Future qui permettent une interaction basique avec les tâches soumises. Lorsqu’à un moment dans le code le passage à la prochaine instruction est conditionné par le statut de terminaison de certaines tâches soumises avant, il devient nécessaire de procéder manuellement à la vérification des statuts de ces tâches, interrompant du coup le flux d’exécution, ce qui traduit une perte de performance.  Peut-être que dans des cas simples il est relativement facile de modifier l’ordre des instructions, ou de carrément revoir la logique du traitement pour réduire et simplifier les dépendances à gérer entre les tâches. Mais à terme cette gestion de dépendance entre tâches concurrentes risque de devenir compliquée à gérer et de noyer le code métier.

1.5.2 Middleware Orientés Message (MOM)

Les MOM sont des solutions appropriées pour découpler des applications s’échangeant de l’information. Le couplage asynchrone qu’il établit entre les services leur permet de ne pas rester bloqué en attente les uns des autres. Ils ont cependant un coût, ne serait-ce que le fait de devoir gérer l’installation et les contraintes techniques qu’ils apportent. Ensuite il reste toujours le problème d’orchestration lorsque l’application doit composer les résultats des informations provenant de plusieurs services externes. Donc des solutions auxquelles ne recourir que lorsque cela s’avère être le meilleur compromis.

1.5.3 CompletableFuture dans l’API Concurrency de Java 8

Avec les CompletableFuture introduits dans Java 8, il devient possible d’associer directement les instructions dépendantes de la terminaison d’une tâche à cette dernière. Cela décharge le développeur de la fastidieuse tâche de gestion des dépendances dans un contexte de programmation concurrente.  Lorsque plusieurs instructions dépendent en cascade les unes des autres, le code avec les CompletableFuture pour implémenter cet enchainement peut devenir illisible. Ce phénomène bien connu sous le nom de Callback Hell n’est d’ailleurs pas propre aux CompletableFuture, on peut le retrouver dans la plupart des API fournissant le mécanisme de Callback.

Listing 1-1-1 Illustration du Callback Hell

client.execute(new Callback() {
    @Override
    public void completed(HttpResponse response) {
        client.execute(new Callback() {
            @Override
            public void completed(HttpResponse response) {
                client.execute(new Callback() {
                    @Override
                    public void completed(HttpResponse response) {
                    }
                });
            }
        });            

1.6 Recours aux Reactives Extensions

En conclusion, arriver à paralléliser des traitements interdépendants tout en ayant un code lisible et maintenable est une problématique complexe. Voici pourquoi le recours à la solution dédiée que représentent les Reactive Extensions(noté Rx) est un choix raisonnable.

L’article suivant sera consacré à la présentation de RxJava et des Reactive Extensions.

1.7 Références