Le monitoring de flux par l'exemple

Après avoir décrit le monitoring de flux et les bonnes pratiques pour le mettre en place, passons à un cas pratique. Nous allons détailler un exemple complet et documenté de monitoring d’un mini-système d’information combinant appels de services et envois de messages, avec des modèles de code.

Les composants applicatifs présentés dans cet article ont des noms génériques (frontend, middleend et backend) afin que chacun puisse facilement les adapter à son propre contexte. Nous aurions pu, par exemple, imaginer le frontend comme une application mobile ou une application web monopage javascript, le middle-end comme une couche de services REST (API), et enfin le backend comme un système de persistance ou encore d’analyse des transactions.

À propos du code

Le code proposé dans cet article a été écrit dans un but illustratif. Il est donc le plus simple possible et ne comprend par exemple ni gestion des erreurs ni optimisation. À vous de vous en inspirer et de l’adapter (il est sous licence open source Apache) mais surtout ne le réutilisez pas tel quel.

Les étapes permettant de tester le code vous-même sont décrites dans la documentation du projet.

Pour que l’article ne soit pas trop long, nous avons utilisé des liens plutôt que du code source inclus dans le texte.

Si des portions du code vous semblent peu claires ou que vous avez des suggestions, merci de créer un ticket ; si vous avez des idées d’améliorations les pull requests sont les bienvenues.

Le système

Le premier article expliquait que les services métier ont deux caractéristiques:

  • ils peuvent combiner plusieurs appels de services et/ou d’envois de messages
  • ils peuvent combiner des technologies hétérogènes.

Un système d’information qui combine ces deux aspects nécessite d’avoir des outils de monitoring flexibles et qui reposent sur des technologies interopérables.

L’architecture applicative présentée ici prend en compte ces éléments:

Architecture

Côté métier :

  • Un serveur frontend qui expose un service et un site web avec un formulaire qui l’utilise.
  • Un serveur middle-end expose deux services pour le frontend.
  • Un serveur backend traite de manière asynchrone des messages publiés par le middle-end sur un bus.

Côté monitoring :

  • un module de monitoring dans chaque brique applicative;
  • un bus dédié recevant des messages de chacun de ces modules;
  • un serveur de CEP traitant ces messages pour lever des alertes et produire des KPIs;
  • une base de donnée indexant les messages et les informations générées par le CEP;
  • un outil de dashboarding pour présenter les informations de monitoring.

Format des messages de monitoring

Il est important de bien définir le format des messages de monitoring, même si le système de traitement doit être conçu pour pouvoir s’adapter facilement à différents cas. L’utilisation d’un gabarit commun permet de simplifier l’exploitation des messages dans le CEP et les dashboard.

Dans notre cas, nous avons choisi d’utiliser des messages au format JSON:


{
    "correlation_id": "octo.local_MonitoringBase_24389_2015-01-30 11:05:29 UTC_36cddd01-7bcd-4ced-8024-919ff1dbe6ca",  // l'id de correlation
    "timestamp": "2015-01-30T12:05:29.230+01:00", // horodatage du message

    "module_type": "FrontendApp", // nom du module qui envoie le message
    "module_id": "FrontendApp_octo.local_001", // identifiant du module qui envoie le message
    "endpoint": "GET /messages", // nom du service
    "message_type": "Send message to backend", // type de message

    "begin_timestamp": "2015-02-19T22:11:15.939+01:00", // optionnel: horodatage du début de l'action en cours
    "end_timestamp": "2015-02-19T22:11:15.959+01:00", // optionnel: horodatage de la fin de l'action en cours
    "elapsed_time": 0.020169, // optionnel: temps mis pour l'action en cours

    "service_params": {
        // optionnel: paramètres d'appel du service
    },

    "headers": {
        // optionnel: en-têtes d'appel du service (en-têtes http par exemple)
    }

    "result": {
        // optionnel: résultat de l'appel du service
    }
}

Détail de chaque composant

Technologies

À propos du monitoring

Pour fournir les données nécessaires, chaque brique embarque du code permettant de récupérer les informations utiles et de les poster dans un bus. Pour pénaliser au minimum les performances du code applicatif, l’envoi des messages s’effectue systématiquement dans un thread dédié, et une gestion d’exception isole le code de monitoring du code métier.

Conformément aux recommandations du premier article, c’est un bus ZeroMQ qui a été choisi pour ses très bonnes performances.

Frontend

Le serveur frontend est en Ruby et utilise le framework web Sinatra qui est parfait pour comme ici exposer facilement des services web.

Frontend

  • app_base paramètre le socle de l’application, et fournit une méthode pour appeler des services du serveur middle-end.
  • le répertoire static contient le site web
  • frontend_app expose le service métier qu’appelle le site web et appelle deux services du middle-end l’un après l’autre.

Monitoring

Le code de monitoring est situé dans la classe monitoring_base.rb

Le framework Sinatra fournit les points d’entrées nécessaires pour le monitoring sous forme de méthodes before et after où toutes les informations de la requête en cours sont accessibles. Pour pouvoir stocker des informations pendant l’exécution de la requête, comme l’heure du début de son exécution, un champ est ajouté à la classe Request.

La méthode permettant d’appeler des services est surchargée pour faire deux choses :

  • envoyer des copies de l’appel au système de monitoring;
  • ajouter des en-têtes http dans l’appel de service pour propager l’identifiant de corrélation ainsi que l’heure de l’appel

Les données sont postées dans une queue et consommées dans un thread séparé.

Middle-end

Le serveur middle-end utilisé Spring, Spring Boot permet de configurer facilement une application et Spring MVC d’exposer des services REST.

Monitoring

Du fait du choix de la technologie Spring, la mise en place de monitoring demande quelques acrobaties :

  • Un HandlerInterceptor fournit un point d’entrée au début et à la fin de l’exécution de chaque requête http qui permet de créer les messages qui seront envoyés au monitoring.
  • Il est nécessaire de sous-classer le HttpServletRequest pour pouvoir stocker des informations pendant l’exécution de la requête, comme l’heure du début de son exécution.
  • Finalement HttpServletRequest qui représente la requête et HttpServletResponse la réponse ne donnent pas d’accès au contenu de la requête ou de la réponse car leur envoi est streamé. Il est donc nécessaire de wrapper les deux classes pour enregistrer les contenus pendant la tranmission, et pouvoir ainsi les relire ensuite.

Le résultat se trouve réparti dans 5 classes :

Le code en charge de l’envoi des messages est situé dans un projet partagé car il est utilisé par le middle-end et le backend. L’essentiel du code est situé dans le MonitoringMessageSender qui utilise un thread dédié à l’envoi des messages et alimenté par une queue.

RedisProvider a été modifié pour transmettre l’identifiant de correlation dans les messages envoyés au backend.

Le bus applicatif

Il s’agit d’un serveur Redis : il est principalement utilisé comme un cache clé-valeur mais son API lui permet également de servir de bus de messages. Ses principaux avantages sont sa facilité de mise en œuvre et sa vitesse de traitement.

Le backend

Nous avons simulé une application de traitement de messages à l’aide d’un pool de threads :

  • ApplicationBase fournit le socle applicatif qui consomme les messages depuis Redis et les fait traiter par un pool de thread Java.
  • Backend traite les messages.

Monitoring

Comme le code de réception est spécifique à l’application, le monitoring est complètement intégré au socle applicatif. Pour l’envoi des messages, il s’appuie sur le même projet partagé que le middle-end.

Le Complex Event Processing

Principes

Le composant de Complex Event Processing dépile les messages en provenance des différents modules (frondend, middle-end, backend). Il réalise alors en parallèle l’insertion dans la base de données et la mise à jour d’un état en mémoire. L’évolution de cet état peut générer des alertes qui seront à leur tour persistées dans la base.

Il est implémenté en Java à l’aide du framework d’intégration Apache Camel et se présente comme une application autonome.

  • Les messages sont dépilés depuis ZeroMQ à l’aide d’un Connecteur qu’il nous a été nécessaire de réécrire en utilisant la librairie jeroMQ. Le composant existant fonctionnait en effet avec des bindings scala non applicables.
  • L’état en mémoire ainsi que le déclenchement d’alertes sont implémentés en utilisant le framework Esper. Camel fournit le connecteur qui permet de s’y interfacer. Les règles sont écrites avec le DSL interne nommé EPL (Event Processing Language) dans le fichier de configuration Camel.
  • Les messages et alertes sont persistés dans un cluster Elasticsearch à l’aide d’un connecteur maison utilisant la bibliothèque Jest (il n’est donc pas conçu pour monter en charge). Le connecteur par défaut s’enregistre comme un nœud du cluster Elasticsearch et cela rendait les tests locaux plus compliqués.

CEP

Interprétation des évènements

Le flux d’évènements est interprété pour construire des indicateurs en temps réels grâce au language d’analyse Esper Process Language (EPL). Ces statuts sont requêtés à intervalles réguliers afin de détecter des comportements anormaux comme des dépassements de seuils.

Nous avons cherché ici à remonter trois types d’alerte :

  • Un temps de traitement trop important pour l’un des composants à l’aide de l’attribut elapsed_time des messages.
  • Un temps de traitement trop important pour un flux sur l’ensemble de la chaîne, en utilisant l’identifiant de correlation.
  • Throttling : Un nombre d’appels supérieur à un seuil fixé dans une unité de temps fixe (ici une moyenne de plus de 3 appels sur 10 secondes).

Traitements CEP

La base de donnée du monitoring

Il s’agit d’une base Elasticsearch qui indexe automatiquement les données à leur insertion. Pour que les données soient indexées au mieux, il suffit de créer un index à l’avance pour que les champs soient indexés de la bonne manière.

Le dashboard

Avec les données déjà structurées et stockées dans Elasticsearch, Kibana est la solution naturelle pour les dashboard : des assistants permettent de facilement créer les différents graphiques en fonctions des données présentes dans la base.

Voici par exemple un dashboard des percentiles des appels sur les différents serveurs (la configuration de ce dashboard est disponible dans les sources) :

Kibana

Les composants difficiles à monitorer

Les cas présentés ici sont favorables car les composants ne sont pas trop compliqués à monitorer, même si le middle-end demande un peu de gymnastique. Malheureusement dans tout SI d’une certaine taille, il existe toujours au moins une brique "boite noire", type portail ou plateforme e-commerce, qu’il est difficile d’outiller convenablement. Pour ces composants deux choix sont généralement possibles :

Utiliser les points d’extensions fournis par l’outil

Cette solution est la plus conforme. Cependant ces API sont souvent d’une qualité inférieure au reste et avant de vous lancer il y a trois choses à vérifier :

  • La documentation est-elle suffisamment détaillée, particulièrement quand des objets internes au composant sont exposés ?
  • Les API sont elles stables ? Comme ces API sont assez proches du moteur des outils, elles sont plus susceptibles de ne pas être compatibles d’une version à l’autre.
  • Toutes les informations dont vous avez besoin sont-elles exposées ?

En fonction des réponses à ces trois questions, la deuxième solution sera peut-être préférable.

Utiliser un proxy

Si les échanges avec le composant se font par http, l’autre solution est de mettre en place un proxy comme nginx pour générer les messages de monitoring. En configurant les logs vous devriez pouvoir obtenir les informations dont vous avez besoin, et un composant custom est nécessaire pour les pousser vers le serveur de CEP.

Cette solution a le désavantage d’ajouter une couche supplémentaire d’infrastructure, mais évite d’avoir à développer du code trop spécifique à un outil.