Supervision d’applications : Collectd et StatsD

Exemple graphiteDans le premier billet Supervision d’application : installation de Graphite nous avons installé Graphite qui permet d’enregistrer des métriques et de générer des graphes utiles à la supervision d’applications.

Il faut maintenant envoyer des métriques à Graphite de manière automatique. Nous verrons quels sont les protocoles pour communiquer avec Carbon (le composant de Graphite qui gère la réception des métriques).

Puis nous installerons Collectd et StatsD afin de collecter et d’envoyer des données à Carbon. Enfin nous ferons quelques tests.

Protocoles de communication avec Carbon

 

Graphite ne va pas chercher les données, il faut les lui envoyer (http://graphite.readthedocs.org/en/latest/feeding-carbon.html).

Nous pouvons utiliser l’un des protocoles suivants :

Texte brut

A envoyer sur un port spécifique de Carbon avec des éléments de métrique : « chemin de la métrique » « valeur » « timestamp ».

Exemple :

echo "test.viseo.v1.ok.count 5 `date +%s`" | nc -q0 127.0.0.1 2003

Protocole “Pickle”

Il est possible d’envoyer des objets sérialisés en Python (Module Pickle dans Python : https://docs.python.org/2/library/pickle.html). Cela permet d’envoyer plusieurs métriques en une seule fois.

 

Message AMQP

(Advanced Message Queuing Protocol) : protocole d’envoi de messages entre serveurs via des files (https://www.amqp.org/product/overview).

 

Collectd

(https://collectd.org/)

Collectd est un démon qui permet de collecter et d’envoyer des informations système (CPU, mémoire, espace disque, etc.). Il est aussi très utile pour superviser des applications communes (Apache, MySQL, Tomcat, etc.) avec plusieurs plugins pré-configurés.

Installation de Collectd

Il suffit de lancer les commandes suivantes :

sudo apt-get update
sudo apt-get install collectd collectd-utils

Configuration de Collectd

Nous allons maintenant permettre à Collectd de récupérer des métriques sur l’espace disque, sur la mémoire utilisée et sur le statut d’Apache en configurant des plugins dans le fichier collectd.conf :

sudo nano /etc/collectd/collectd.conf

Nous commentons toutes les lignes « LoadPlugin » à part les 3 suivantes :

LoadPlugin apache
LoadPlugin df
LoadPlugin memory
LoadPlugin write_graphite

Nous configurons ensuite le plugin Apache qui permettra de surveiller si le serveur est démarré ou non :

<Plugin apache> 
        <Instance "Graphite">
                URL "http://localhost/server-status?auto"
                Server "apache"
        </Instance>
</Plugin>

Pour avoir le taux de remplissage du disque dur, nous paramètrons le plugin df. (Au préalable, lancer la commande df dans une console pour cibler le device à superviser. Généralement ce sera /dev/sda1.)

<Plugin df>
#	Device "/dev/sda1"
#	Device "192.168.0.2:/mnt/nfs"
#	MountPoint "/home"
#	FSType "ext3"

	# ignore rootfs; else, the root file-system would appear twice, causing
	# one of the updates to fail and spam the log
	FSType rootfs
	# ignore the usual virtual / temporary file-systems
	FSType sysfs
	FSType proc
	FSType devtmpfs
	FSType devpts
	FSType tmpfs
	FSType fusectl
	FSType cgroup
	IgnoreSelected true

#	ReportByDevice false
#	ReportReserved false
#	ReportInodes false

#	ValuesAbsolute true
#	ValuesPercentage false
</Plugin>

Enfin nous configurons la manière dont Collectd va communiquer avec Graphite :

<Plugin write_graphite>
        <Node "graphite">
                Host "localhost"
                Port "2003"
                Protocol "tcp"
                LogSendErrors true
                Prefix "collectd."
                StoreRates true
                AlwaysAppendDS false
                EscapeCharacter "_"
        </Node>
</Plugin>

Nous sauvegardons le fichier.

Configurer Apache pour récupérer son statut

Dans la configuration Apache de Graphite,

sudo nano /etc/apache2/sites-available/apache2-graphite.conf

Nous ajoutons le bloc « server-status » :

Alias /content/ /usr/share/graphite-web/static/
        <Location "/content/">
                SetHandler None
        </Location>

        <Location "/server-status">
                SetHandler server-status
                Require all granted
        </Location>

Nous rechargeons la configuration Apache :

sudo service apache2 reload

Et nous testons http://localhost/server-status :

Statut Apache
Statut Apache

 

Configuration de Carbon pour Collectd

Nous modifions le fichier storage-schemas.conf

sudo nano /etc/carbon/storage-schemas.conf

Les métriques collectées par Collectd commenceront par « collectd. ». Nous ajoutons donc le bloc :

[collectd]
pattern = ^collectd.*
retentions = 10s:1d,1m:7d,10m:1y

 

Tests de Collectd

Nous relançons Carbon puis Collectd :

sudo service carbon-cache stop 
sudo service carbon-cache start
sudo service collectd stop
sudo service collectd start

Puis nous allons sur l’interface de Graphite. Nous pouvons voir qu’il y a de nouvelles métriques :

collectd

Ci-dessous 2 exemples de graphes :

Apache Nombre de requêtes
Apache Nombre de requêtes

 

espace_disque
Espace Disque

 

Conclusion sur Collectd

Comme nous pouvons le constater, Collectd couplé avec Graphite permet de connaître facilement l’ « état » de ses serveurs. Il faut évidemment installer ce démon sur chaque serveur à surveiller.

Parmi les nombreux plugins collectd existants nous pouvons citer :

  • CPU : pour monitorer les processeurs
  • MySQL et PostgreSQL pour superviser des bases de données
  • GenericJMX qui permet d’avoir des informations sur des process java, pour surveiller par exemple l’exécution d’un serveur Tomcat.

StatsD

(https://github.com/etsy/statsd)

StatsD est un démon qui reçoit des paquets UDP de n’importe quelle application. Chaque paquet concerne un point de données.

StatsD agrège ces données et les envoie à Carbon.

Nous l’utiliserons pour superviser 2 webservices REST codés en Java et exécutés dans un serveur Tomcat. Nous simulerons les performances de ces webservices :

  • http://localhost:8080/graphite-statsd-ws-1.0-SNAPSHOT/test/v1 (le service v1 répond « ok » dans 97% des cas et a un temps de réponse compris en 0s et 5s)
  • http://localhost:8080/graphite-statsd-ws-1.0-SNAPSHOT/test/v2 (le service v2 répond « ok » dans 90% des cas et a un temps de réponse compris en 0s et 10s)

Création des webservices et déclaration des métriques

Dans un projet Java Maven, en plus des dépendances utiles aux webservices, nous ajoutons dans le pom.xml l’import d’un client StatsD (il en existe d’autres) :

        <dependency>
            <groupId>com.timgroup</groupId>
            <artifactId>java-statsd-client</artifactId>
            <version>3.0.1</version>
        </dependency>

Nous créons une classe CustomStatsdTimer qui aidera à mesurer le temps d’exécution :

package statsd;

public class CustomStatsdTimer {
    String metricPath;
    long start;

    CustomStatsdTimer(String metricPath) {
        this.metricPath = metricPath;
        start = System.currentTimeMillis();
    }
}

Nous créons ensuite un service qui contient :

  • La référence au client StatsD
  • Le préfixe des métriques à envoyer : « app.graphite-statsd.ws » (en général, une application aura toujours le même préfixe)
  • Les méthodes pour mesurer le temps d’exécution et pour incrémenter le nombre d’appels
package statsd;

import com.timgroup.statsd.StatsDClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CustomStatsdClient {

    @Autowired
    private StatsDClient statsd;

    private String metricApplicationPrefix = "app.graphite-statsd.ws";

    private static Logger logger = LoggerFactory.getLogger(CustomStatsdClient.class);

    public CustomStatsdTimer startTimer(String metricName) {
        final String metricPath = metricApplicationPrefix + "." + metricName;
        logger.debug("startTimer {}", metricPath);
        return new CustomStatsdTimer(metricPath);
    }

    public CustomStatsdClient recordExecutionTime(CustomStatsdTimer timer) {
        logger.debug("recordTimer {}", timer.metricPath);
        int ms = (int) (System.currentTimeMillis() - timer.start);
        statsd.recordExecutionTime(timer.metricPath, ms);
        return this;
    }

    public CustomStatsdClient incrementCounter(String metricName) {
        final String metricPath = metricApplicationPrefix + "." + metricName;
        logger.debug("incrementCounter {}", metricPath);
        statsd.incrementCounter(metricPath);
        return this;
    }
}
  • La configuration Spring pour paramétrer la connexion au démon StatsD (dont nous verrons plus tard l’installation) :
       <bean id="statsdClient" class="com.timgroup.statsd.NonBlockingStatsDClient" destroy-method="stop">
              <constructor-arg index="0" value=""/>
              <constructor-arg index="1" value="localhost"/>
              <constructor-arg index="2" value="8125"/>
       </bean>

Finalement nous ajoutons l’envoi de métriques dans nos 2 webservices pour superviser le fonctionnement suivant :

  • app.graphite-statsd.ws.controller.v1.ok : nombre d’appels au service v1 sans échec
  • app.graphite-statsd.ws.controller.v1.ko : nombre d’appels au service v1 avec échec
  • app.graphite-statsd.ws.controller.v1.time : temps d’exécution de l’appel au service v1
  • app.graphite-statsd.ws.controller.v2.ok, app.graphite-statsd.ws.controller.v2.ko, app.graphite-statsd.ws.controller.v2.time : pour superviser les appels au service v2
package controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import statsd.CustomStatsdClient;
import statsd.CustomStatsdTimer;

import java.util.Random;

@Controller
@RequestMapping("test")
public class GraphiteStatsdController {

    private static final String METRIC_NAME_V1 = "controller.v1";
    private static final String METRIC_NAME_V2 = "controller.v2";
    private static Logger logger = LoggerFactory.getLogger(GraphiteStatsdController.class);

    @Autowired
    private CustomStatsdClient customStatsdClient;

    @RequestMapping(value = "v1", method = RequestMethod.GET)
    @ResponseBody
    public String getV1() {

        final CustomStatsdTimer timer = customStatsdClient.startTimer(METRIC_NAME_V1 + ".time");
        String result = "ok";
        Random randomResult = new Random();
        if (randomResult.nextInt(100) <= 3) {
            result = "ko";
        }

        try {
            //Traitement du webservice entre 0s et 5s
            Random randomExecutionTime = new Random();
            final int executionTime = randomExecutionTime.nextInt(5000);
            logger.debug("Start V1 for result {} and executionTime {} s", result, executionTime / 1000);
            Thread.sleep(executionTime);
        } catch (InterruptedException e) {
            logger.warn("Error sleeping : {}", e.getMessage(), e);
        }

        if ("ok".equals(result)) {
            customStatsdClient.recordExecutionTime(timer);
            customStatsdClient.incrementCounter(METRIC_NAME_V1 + ".ok");
        } else {
            customStatsdClient.incrementCounter(METRIC_NAME_V1 + ".ko");
        }

        return result;
    }

    @RequestMapping(value = "v2", method = RequestMethod.GET)
    @ResponseBody
    public String getV2() {

        final CustomStatsdTimer timer = customStatsdClient.startTimer(METRIC_NAME_V2 + ".time");
        String result = "ok";
        Random randomResult = new Random();
        if (randomResult.nextInt(100) <= 10) {
            result = "ko";
        }

        try {
            //Traitement du webservice entre 0s et 10s
            Random randomExecutionTime = new Random();
            final int executionTime = randomExecutionTime.nextInt(10000);
            logger.debug("Start V2 for result {} and executionTime {} s", result, executionTime / 1000);
            Thread.sleep(executionTime);
        } catch (InterruptedException e) {
            logger.warn("Error sleeping : {}", e.getMessage(), e);
        }

        if ("ok".equals(result)) {
            customStatsdClient.recordExecutionTime(timer);
            customStatsdClient.incrementCounter(METRIC_NAME_V2 + ".ok");
        } else {
            customStatsdClient.incrementCounter(METRIC_NAME_V2 + ".ko");
        }

        return result;
    }
}

La communication entre le client et le démon StatsD se fait avec le protocole UDP pour lequel il n’y a pas de vérification des paquets reçus/non reçus.

Nous pouvons donc exécuter les webservices sans problème même si le démon n’est pas installé. En effet un dysfonctionnement sur la plateforme de supervision ne doit pas bloquer l’exécution des applications.

Installation de StatsD

Sur chaque serveur hébergeant nos webservices, nous devons maintenant installer le démon StatsD. L’installation n’est pas aussi facile que pour Collectd mais la suite de commandes suivantes fonctionne sur un serveur Ubuntu 14 :

sudo apt-get install git nodejs devscripts debhelper
sudo apt-get install npm nodejs-legacy

Nous devons créer, builder et installer un package :

mkdir ~/build

#Nous clonons le projet StatsD
cd ~/build
git clone https://github.com/etsy/statsd.git

#Nous construisons le package (.deb)
cd statsd
dpkg-buildpackage
cd ..

 

Dès que le démon va être installé, il va commencer à envoyer des informations à Carbon. Nous arrêtons donc Carbon pour configurer StatsD :

sudo service carbon-cache stop

Puis nous installons le package :

sudo dpkg -i statsd*.deb

#Arrêt de StatsD 
sudo service statsd stop

#Redémarrage de Carbon
sudo service carbon-cache start

 

Configuration de StatsD

Nous modifions le fichier localConfig.js pour utiliser le nouveau format de structuration de données :

sudo nano /etc/statsd/localConfig.js

la structure est en JSON :

{
  graphitePort: 2003
, graphiteHost: "localhost"
, port: 8125
, graphite: {
    legacyNamespace: false
  }
}

 

Configuration de Carbon pour StatsD

Toujours dans le fichier storage-schemas.conf

sudo nano /etc/carbon/storage-schemas.conf

Puisque les métriques collectées par StatsD commenceront par « stats », on ajoute le bloc :

[statsd]
pattern = ^stats.*
retentions = 10s:1d,1m:7d,10m:1y

Et nous relançons Carbon :

sudo service carbon-cache stop
sudo service carbon-cache start

 

Tests de StatsD

Nous lançons le démon StatsD :

sudo service statsd start

Sur l’interface Graphite, de nouvelles métriques sont présentes, celles propres à StatsD :

Graphite et StatsD
Graphite et StatsD

Avec Apache Bench (sudo apt-get install apache2-utils), nous interrogeons les 2 webservices en lançant 1000 requêtes (jusqu’à 5 en parallèle) :

ab -n 1000 -c 5 http://localhost:8080/graphite-statsd-ws-1.0-SNAPSHOT/test/v1
ab -n 1000 -c 5 http://localhost:8080/graphite-statsd-ws-1.0-SNAPSHOT/test/v2

Dans les logs de Carbon nous constatons la création de nos nouvelles métriques :

07/08/2015 15:24:44 :: new metric stats.counters.app.graphite-statsd.ws.controller.v1.ok.count matched schema statsd
07/08/2015 15:24:44 :: new metric stats.counters.app.graphite-statsd.ws.controller.v1.ok.count matched aggregation schema sum
07/08/2015 15:24:44 :: creating database file /var/lib/graphite/whisper/stats/counters/app/graphite-statsd/ws/controller/v1/ok/count.wsp (archive=[(10, 8640), (60, 10080), (600, 52560)] xff=0.0 agg=sum)
...

 

A la fin du test, nous pouvons générer différents graphiques :

Réponses Ok des webservices
Réponses Ok des webservices

 

Réponses KO des webservices
Réponses KO des webservices

 

Temps de réponse des webservices
Temps de réponse des webservices

 

A noter que pour la génération des 2 derniers graphiques j’ai utilisé une wildcard (*) pour sélectionner les métriques à afficher.

Si j’ajoute un troisième webservice, avec des métriques qui respectent ma convention de nommage (en v3), le graphe affichera aussi ces statistiques.

 

Conclusion sur StatsD

StatsD permet de superviser facilement ses applications. Nous l’avons vu avec des webservices, mais c’est aussi applicable sur des Jobs, des pages webs, etc.

Des clients StatsD existent aussi en PHP (https://github.com/liuggio/statsd-php-client), Ruby (https://github.com/github/statsd-ruby), C# (https://github.com/goncalopereira/statsd-csharp-client), etc.

 

Conclusion

En combinant StatsD et Collectd avec Graphite nous pouvons obtenir des graphes sur l’ « état » de nos applications et de nos serveurs. Ceci en temps réel ou sur du long terme.

Pour parfaire la supervision de tout notre environnement, nous avons besoin d’une vision générale des différentes métriques et surtout d’être averti au moindre problème.

Ce sera l’objet du dernier billet qui portera sur Tattle.

2 réflexions au sujet de « Supervision d’applications : Collectd et StatsD »

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *