29 juillet 2021

Monitoring et métriques applicatives

Il est important aujourd’hui que les applications produisent des métriques pertinentes permettant entre autre de savoir si l’application fonctionne correctement ou non. Je décrirai dans cet article les métriques essentielles à exposer selon moi, en prenant l’exemple d’une application web.

Les métriques applicatives

Les métriques que l’on voit le plus souvent dans les infrastructures de monitoring sont les métriques systèmes (CPU, RAM, occupation des disques, métriques sur les process…​). Ces métriques sont importantes mais je ne les aborderai pas dans cet article car généralement ces métriques sont disponibles (c’est le premier truc que les ops remontent, via des agents comme CollectD ou Telegraf par exemple).

Depuis maintenant assez longtemps, il est commun que les applications produisent elles mêmes des métriques. Des méthodes comme RED (Rate, Error, Duration) ont émergées pour essayer de définir les métriques essentielles à récupérer.

Pourtant, je trouve qu’il manque d’exemples simples sur les métriques à exposer, car ce n’est au final pas un sujet aussi évident que ça. En effet, ces métriques doivent être pertinentes et doivent permettre de détecter rapidement des problèmes divers. Il y a aussi quelques pièges à éviter.
Je ne parlerai ici pas d’outils de monitoring ou de format des métriques, car cela n’est qu’un détail d’implémentation qui varie selon la solution de monitoring choisie pour récupérer et stocker ces métriques.

L’exemple d’une application web

Prenons un exemple qui parle à tout le monde: une application web. La question du langage de programmation utilisé pour cet application est également sans importance.

Cette application a donc plusieurs composants:

  • Un serveur HTTP recevant les requêtes

  • Un client communiquant avec une base de données

  • Un client communiquant avec un second service, en HTTP

Que devons-nous exposer comme métriques en priorité pour cet applications ?

Le serveur HTTP

Métriques internes

La première chose à regarder sont les métriques exposées par votre serveur HTTP. La plupart des serveurs aujourd’hui utilisent plusieurs threads (appelé souvent un threadpool). Ces serveurs ont également parfois des queues internes (c’est le cas de Netty par exemple). Récupérer les métriques de ces composants est un bon début.

Par exemple, savoir combien de threads sont utilisés ou au repos peut être intéressant. Si le serveur HTTP utilise une queue interne, connaître la taille de cette queue set très important: si elle est au dessus de zéro pendant une certaine durée, voir augmente, c’est probablement que votre serveur HTTP n’arrive pas à traiter assez rapidement l’ensemble des requêtes reçues.
Bref, ces métriques permettent d’avoir une information globale sur la santé de votre serveur HTTP.

Requêtes HTTP

Il est important d’exposer également des métriques sur les requêtes HTTP reçues par votre serveur. On veut généralement savoir deux choses:

  • Le nombre de requêtes HTTP reçues

  • Le temps d’exécution d’une requête

Parfois, certains outils de monitoring vous permettent d’obtenir ces deux métriques en une seule fois. Commençons par le nombre de requêtes HTTP reçues.

Nombre de requêtes

Nous appelerons cette métrique http_request_count. La valeur de cette métrique est donc le nombre de requêtes reçues par votre serveur. Mais reçues par quoi ? Nous voulons compter le nombre de requêtes:

  • Par path/uri (par exemple /foo, /bar, /foo/bar…​)

  • Par méthode HTTP (get, post…​)

En effet, je veux savoir précisément comment mon serveur web est utilisé. Si les compteurs de requêtes étaient mélangés, il serait impossible de savoir si un endpoint est plus utilisé qu’un autre par exemple.

Cela permet aussi de voir précisément les fonctionnalités de votre produit qui sont utilisées (ou non).
Notre métrique aura donc deux labels: path et method. On pourra donc avoir des statistiques pour chaque combinaison de labels, et des statistiques globales en les additionant par exemple.

Notre métrique finale ressemblera à quelque chose comme (au format Prometheus par exemple) http_request_count{path="/foo", method="get"} 1000, 1000 étant un compteur (et donc en croissance permanente). Cette métrique peut se traduire "A cet instant, mon application a reçu 1000 requêtes sur le path /foo en `get`". Comme vous le voyez, les labels permettent d’être précis.

La majorité (totalité ?) des produits de monitoring savent convertir un compteur en débit. Par exemple, si à temps = 10 j’ai ma métrique qui vaut 1000, et à temps = 20 ma métrique vaut 2000, j’ai reçu en 10 secondes 1000 requêtes. J’ai donc à peu près 100 requêtes par seconde sur ce endpoint.
Travailler avec des compteurs n’est donc pas un problème.

Mais il y a quand même ici quelques pièges, dans lesquels je tombe d’ailleurs régulièrement.

404 not found

Un exemple vécu: votre application a des métriques aux petits oignons. Chaque path de votre API a sa métrique, parfait.

Sauf qu’un petit malin se met à spam votre API sur des url inexistantes, du genre /azeija ou azijazrijaija. Chaque appel crée une nouvelle métrique. Cela cause plusieurs problèmes:

  • Vous générez des métriques peu utiles, et si vous exposez vos métriques via HTTP (comme Prometheus le fait), la taille des données à exposer explose.

  • Vous poussez plein de nouvelles métriques dans votre base de données stockant vos métriques, où généralement chaque nouvelle url créera une nouvelle métrique. Cela peut causer de gros problèmes de performances selon l’outil.

En gros, quelqu’un peut DDOS votre outil de monitoring voir votre application en vous envoyant des tonnes de requêtes sur des chemins aléatoires.

La solution est de détecter les 404, pourquoi pas log l’url non trouvée, et générer une métrique comptant le nombre de 404 au niveau global et non par path. De cette façon, vous ne créez pas de métriques inutiles.

Variables dans les url

Problème équivalent mais légèrement différent.

Vous avez une API REST permettant de récupérer une ressource par ID, par exemple via un GET /my-resource/ID. Voulez vous vraiment générer une métrique par ressource ou globalement pour ce endpoint ? Vous voulez 99 % du temps la seconde option pour vos métriques (je n’aborderai pas le tracing ici).

Si vous ne faites pas attention, vous aurez des métriques avec path="my-resource/3a189cc2-ef21-11eb-abf6-0bf0708be395,path="my-resource/42b6662a-ef21-11eb-bf8c-37a36554f35c…​ Bref, il vaudrait mieux grouper tout cela dans une seule métrique ayant path="my-resource/ID" non ?

Donc pareil, selon les langages c’est quelque chose de facile ou non à paramétrer, mais c’est vraiment quelque chose à ne pas oublier.

Temps d’exécution des requêtes

Maintenant qu’on mesure correctement l’utilisation de notre API, nous voulons savoir le temps d’exécution des requêtes. Appelons cette métrique http_request_duration.
Nous aurons pour commencer les mêmes labels que notre métrique précédente (path et method), ce qui nous permet là aussi d’avoir une métrique unique pour chaque combinaison de labels. Les mêmes pièges seront à éviter pour ces labels, en particulier le path.

Les librairies de monitoring exposent généralement les temps d’exécution de deux manières.

La première consiste à rajouter un label appelé par exemple percentile à chaque métrique. Nous pourrions avoir par exemple pour un path (/foo) et une méthode (get) les métriques suivantes exposées par l’application:

  • http_request_duration{path="/foo", method="get", percentile="99"} 2000 : 99 % des requêtes s’exécutent en dessous de 2000 millisecondes.

  • http_request_duration{path="/foo", method="get", percentile="75"} 1500 : 75 % des requêtes s’exécutent en dessous de 1500 millisecondes.

  • http_request_duration{path="/foo", method="get", percentile="50"} 1000 : 50 % des requêtes s’exécutent en dessous de 1000 millisecondes.

Comme vous le voyez, le label percentile rajoute une indication sur le pourcentage de requêtes s’exécutant en un temps inférieur à la valeur de la métrique (la valeur de la métrique étant donc un temps d’exécution). Vous pouvez généralement configurer les percentiles voulus (99, 98, 75, 50 …​), et détecter des problèmes de performances en vous basant sur ces métriques.

La seconde manière, utilisée généralement par des outils comme Prometheus, est la suivante:

  • http_request_duration{path="/foo", method="get", le=100} 200: 200 requêtes ont mis 100 millisecondes ou moins à s’exécuter. La valeur de la métrique est donc un compteur.

  • http_request_duration{path="/foo", method="get", le=500} 300: 300 requêtes ont mis 500 millisecondes ou moins à s’exécuter (et donc, vu qu’on sait que 200 ont mis moins de 100 millisecondes, on sait que seulement 100 sont dans l’intervalle 100-500)

  • http_request_duration{path="/foo", method="get", le=1000} 600: 600 requêtes ont mis 1000 millisecondes ou moins à s’exécuter

Vous avez compris le principe: le représente une durée, et on compte pour chaque valeur de le le nombre de requêtes s’exécutant dans un temps inférieur à cette durée. Cette façon de faire est intéressante, car elle permet de facilement additionner ces métriques si vous avez plusieurs instances de votre application (ce qui n’est pas possible avec les percentiles). La difficulté est par contre de définir des intervalles pertinents.

Il y a quand même ici une autre question à se poser: on veut mesurer une durée, mais quand la mesure doit-elle commencer (et se terminer) dans votre application ? Une application web est généralement composée de plusieurs "étages" (ou middlewares, interceptors…​), chaque étage étant responsable d’un traitement sur la requête. On peut imaginer par exemple:

serveur webdéserialisationroutingauthentificationaction à réaliser

Vous devez essayer de mesurer le plus de choses possibles, et donc commencer la mesure le plus tôt possible. En effet, si vous commencez la mesure à la fin de la chaine, une lenteur sur l’étage précédent ne sera pas détectée.
Mais vous allez à un moment arriver à la limite de votre framework ou outil, et il y aura toujours une partie qui ne sera pas mesurée. C’est pour cela qu’il est également très intéressant d’avoir le temps d’exécution des appels remonté également par un load balancer devant votre application par exemple, ou par vos applications clientes quand cela est possible. Ces clients de l’application pourront mesurer le temps réel que met votre service à répondre.

labels supplémentaires

Vous aurez sur vos métriques en réalité d’autres labels, comme par exemple host pour l’hostname où l’application tourne, environment dont la valeur peut être prod, preprod…​ C’est à vous de voir. L’idée est d’avoir des labels pertinents pour que vous puissez ensuite facilement exécuter des requête sur vos métriques, et identifier précisément quelle instance d’une application pose problème.

Réponses HTTP

Cet article contient déjà beaucoup de choses, et nous n’avons vu que deux métriques. Une métrique indispensable à exposer est également le nombre de réponses HTTP, par path, method, et code HTTP. Par exemple:

  • http_response_count{path="/foo", method="get", status="200"} 150: 150 réponses HTTP ont un status code de 200 pour GET /foo.

  • http_response_count{path="/foo", method="get", status="500"} 200: 200 réponses HTTP ont un status code de 500 pour GET /foo.

  • http_response_count{path="/bar", method="post", status="200"} 25: 25 réponses HTTP ont un status code de 200 pour POST /bar.

On compte ici le nombre de réponses HTTP, et il devient facile de détecter une augmentation d’erreurs grâce au label status.

Et voilà pour la partie HTTP !

Base de données et client HTTP

Votre application exécute également des requêtes à une base de données. On trouve généralement dans les clients des bases de données un threadpool, donc la partie threadpool du serveur HTTP s’applique également ici: on veut connaître l’utilisation de ces threads.

Votre application envoie aussi des requêtes HTTP à une autre application. Ici aussi, on a souvent des threadpools dans les clients HTTP donc il faut pouvoir monitorer l’utilisation de ce threadpool.

Ces deux actions (envoyer des requêtes à une base de données et envoyer des requetes HTTP à une autre application) ont une chose en commun: elles interagissent avec un système extérieur. On parle ici d’I/O (notre serveur HTTP étant lui même un composant faisant de l’I/O, sauf que dans ce cas là c’est un composant recevant des requêtes).

Il y a selon moi une règle simple à appliquer sur les I/O: chaque I/O (que ce soit intéragir avec une base de données, une autre application, écrire dans un fichier…​) doit avoir des métriques pertinentes (donc avec des labels correctement définis) mesurant:

  • Le nombre d’appels (donc, des compteurs)

  • Le temps d’exécution des actions (une durée, soit via des percentiles soit via des intervalles)

  • Le nombre d’erreurs (là aussi des compteurs)

On retrouve exactement les mêmes métriques que pour nos requêtes HTTP dans la première partie de cet article, mais appliquées à des composants faisant des requêtes vers l’extérieur.
Par exemple, notre client HTTP pourrait lui aussi exposer des métriques de type http_client_request_count, http_client_request_duration, http_client_response_count, avec les mêmes labels (path, method, status…​) que pour les métriques serveur !

De même, je veux connaître dans le cas de ma base de données le nombre de requêtes envoyées, le temps d’exécution de ces requêtes, et compteur le nombre d’erreurs.

Avec ces métriques, il devient facile de voir qu’on a une dégradation de service (requêtes lentes, erreurs en augmentation).

Autres métriques

On voit au final qu’il est très important d’avoir des métriques sur ce qui entre dans l’application, et ce qui sort de l’application. Chaque I/O doit avoir ses métriques. Si vous consommez des messages d’un bus de message ou d’une queue (Kafka, RabbitMQ…​), c’est la même chose: vous voulez connaître le nombre de messages consommés par seconde, le temps d’exécution de l’action à réaliser une fois le message consommé, et le nombre d’erreurs produites.

Selon la façon dont est construite votre application, vous aurez également d’autres métriques à exposer.

Si votre application a des queues (ou channels, buffers…​) internes (en mémoire), vous devez par exemple savoir combien de messages sont injectés dans la queue, consommés de la queue, et la taille de la queue elle même. Vous pourrez comme cela détecter un problème de performance.

Si votre application exécute des tâches périodiquement (un peu comme un cronjob), il est là aussi intéressant de compter le nombre d’exécution de ces tâches ainsi que de calculer si possible la durée de la tâche.

Conclusion

Essayez toujours de réfléchir aux endroits qui peuvent poser problème dans votre application: I/O, threadpools, queues…​ et demandez vous "comment je détecte un problème sur ce composant" ? Comme dit précédemment, le nombre d’appels, le nombre d’erreurs, et les temps d’exécution sont un bon début.

Mettez des labels pertinents sur vos métriques. Des métriques sans labels sont inutiles.

j’espère que cet article vous aura été utile. Un autre article (et peut être même une vidéo) sur la détection d’anomalies sur ces métriques arrivera prochainement.

Tags: devops kubernetes

Add a comment








If you have a bug/issue with the commenting system, please send me an email (my email is in the "About" section).

Top of page