CérénIT

Le blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)

Web, Ops & Data - Décembre 2020

docker runc rootless swarm cgroups kubernetes cri dockershim vector aws timestream warp10 dashboard ptsm timescale centos rhel podman influxdb timeseries

Containers et orchestration

  • Electro Monkeys #37 – Podman, l’alternative de Redhat à Docker avec Benjamin Vouillaume : je me demandais si podman permettait un management de containers à distance et l’étendue de son écosystème. Le podcast permet de compléter le tour du propriétaire et de se faire une bonne idée de son positionnement.
  • How to deploy on remote Docker hosts with docker-compose : dans la même quête, je me demandais s’il était possible de piloter un noeud docker à distance quand je me suis rappelé qu’il était possible de le faire au travers d’une connexion ssh. En creusant un peu plus, j’ai découvert la notion de contexte qui permet ainsi de cibler un noeud docker, docker swarm ou kubernetes.
  • Don’t Panic: Kubernetes and Docker et Dockershim Deprecation FAQ : A partir de kubernetes 1.20 (fin 2020) et définitivement à partir de la version 1.23 (fin 2021), le retrait du binaire docker comme CRI de Kubernetes est annoncé. Cela ne devrait pas changer grand chose et c’est essentiellement de la plomberie interne. Plutôt que de passer par Dockershim qui implémentait l’interface CRI et qui discutait ensuite avec Docker pour lancer les conteneurs via containerd, l’appel sera directement fait à containerd. Il n’y a que ceux qui montent la socket docker dans les pods qui vont avoir un souci. Si c’est pour builder des images, il y a des alternatives comme img, kaniko, etc. Pour les autres cas, il faudra peut être passer par l’API kubernetes ou trouver les alternatives qui vont bien.
  • What developers need to know about Docker, Docker Engine, and Kubernetes v1.20 et Mirantis to take over support of Kubernetes dockershim : Mirantis et Docker Inc vont assurer le support de cette interface dockershim pour permettre à ceux qui ont en besoin de pouvoir continuer à l’utliiser. La limite étant que si vous êtes sur du service managé et que votre provider ne le fournit pas, vous ne pourrez pas l’utiliser…
  • Kubernetes 1.20: The Raddest Release : voilà, la version 1.20 est sortie et apporte son lot de nouveautés et de stabilisation.
  • Announcing General Availability of HashiCorp Nomad 1.0 : Nomad 1.0 est également disponible.
  • Introducing Docker Engine 20.10 - Docker Blog : Docker 20.10 arrive avec des profondes nouveautés comme le support des cgroupsv2 et un mode rootless, docker logs fonctionne avec tous les drivers de log et non unqiement json & journald et plein d’autres améliorations/harmonisations au niveau de la CLI. Pour ceux sous Fedora qui avaient bidouillé avec firewalld précédemment pour faire fonctionner docker et qui ont un problème lié à l’interface docker0 au démarrage du service docker, allez voir par ici.
  • Docker Engine Release Notes - Version 20.10 : En plus des points précédents, il y a l’arrivée des jobs dans swarm - depuis le temps que je l’attendais 🤩 (même si on peut se toujours se poser la question de la pérénnité de swarm depuis qu’il a été racheté par Mirantis)
  • New features in Docker 20.10 (Yes, it’s alive) : un billet décrivant plus en détail certaines feautres de docker 20.10 comme support Fedora/CentOS, le rootless mode, l’option -mount, les jpbs swarm et une synthèse de l’actualité de l’écosystème docker.
  • Podman Release 2.2.0 : ajout des commandes network (dis)connect, support des alias avec des noms courts, amélioration des commandes play|generate kube et capacité de monter une image OCI dans un container.

Observabilité

  • Vector - Collect, transform, & route all observability data with one simple tool. (via) : vector est un outil en rust qui permet de collecter et manipuler des métriques/logs/événements et de les envoyer vers différentes destinations. De quoi remplacer filebeat/journalbeat voire même telegraf ? ;-)
  • Our new partnership with AWS gives Grafana users more options : AWS vient d’annoncer un service managé pour Prometheus basé sur Cortex et pour Grafana (version Entrepsise). Grafana et Cortex étant des projets édités par Grafana Labs sous licence OSS (et des déclinaisons Cloud et Entreprise). AWS changerait-il sa façon de travailler avec les projets OSS lorsqu’il souhaite en faire des services managés et prendre ainsi une attitude plus positive vis à vis de la communauté OSS ?

OSS

  • Death of an Open Source Business Model : Analyse du passage de Mapbox GL JS v2 sous licence propriétaire, le modèle de l’open core et les menaces des top cloud providers sur le reste de l’économie du logiciel. On peut étendre cela aussi “VC funded OSS company”.

Système

  • CentOS Project shifts focus to CentOS Stream, CentOS Stream: Building an innovative future for enterprise Linux et la FAQ associée : Historiquement CentOS Linux était batie sur les sources de Red Hat Entreprise Linux un fois celle-ci disponible. CentOS Stream renverse un peu la tendance avec un cycle d’intégration plutôt Fedora -> CentOS Stream -> RHEL. L’initiative CentOS Stream avait été annoncée en septembre 2019 et ce changement permet donc aux équipes CentOS de se focoaliser sur une seule version (CentOS Stream) et non plus deux versions (CentOS Stream et CentOS Linux). CentOS Linux 7 sera maintenue jusqu’à la fin du support de RHEL 7 et et CentOS 8 jusqu’à fin 2021 (et non pas 2029 comme prévu). Il n’y aura pas de version 9 de CentOS Linux. A tester pour voir si CentOS Stream est plus stable que Fedora mais moins conservateur que RHEL et pourrait alors s’avérer être un bon compromis.
  • Before You Get Mad About The CentOS Stream Change, Think About… : un billet assez long d’un employé de Red Hat qui exprime son opinion et cherche à remettre les choses en perspective en dépassionnant le débat.

Time Series

  • PTSM Edition #8 - Amazon TimeStream 101 : Edition du Paris time Series Meetup sur AWS Timestream
  • TimescaleDB vs. Amazon Timestream: 6000x higher inserts, 5-175x faster queries, 150x-220x cheaper : avec toutes les réserves habitudelles sur les benchmarks, Timescale a comparé son produit avec AWS Timestream et la conclusion semble clairement en faveur de Timescaledb. A noter que l’update des données est arrivé dans AWS Timestream entre temps.
  • Truly Dynamic Dashboards as Code : les équipes de SenX ont implémenté leur vision du “Dashboards as Code” sous le nom Discovery. Discovery veut aller plus loin que la simple description d’un dashboard comme on peut le voir dans Grafana ou InfluxDB 2.0 en apportant une touche de dynamisme et de génération dynamique des dashboards en fonction des élements obtenus (ex si valeur X > Y alors afficher la procédure AAA de résolution d’incident). J’ai commencé à jouer avec, un billet de blog sur ce sujet devrait bientôt arriver.
  • NeuralProphet : un modèle neuronal orienté série temporelles, inspiré de Facebook Prophet et développé avce PyTorch.
  • Release Notes InfluxDB 2.0.3 et Release Announcement: InfluxDB OSS 2.0.3 : build arm64 en preview, un petit conflit de packaging entre influxdb et influxdb2 à passer pour ceux qui étaient déjà en 2.0 et ceci afin d’éviter que des gens en 1.x passent involontairement en 2.x, le “delete with predicate” a été réactivé, améliorations sur le process d’upgrade, des commandes autour des actions en mode V1, mise à jour de flux, et plein d’autres corrections/améliorations.

Web

  • Web Almanac 2020 - Rapport annuel de HTTP Archive sur l’état du Web : une synthèse de l’état du web d’après HTTP Archive sur une base de 7.5 millions de sites testés, soit 31.3 To de données traitées. De quoi relativisez un peu les biais de notre bulle technologique : non tout le monde ne fait pas du React/Angular/Vue.js par ex mais plutôt du JQuery ! Suivant vos usages, plein d’enseignements et de choses intéressantes à tirer de cette synthèse.

Il ne me reste plus qu’à vous souhaiter de bonnes fêtes de fin d’année et on se retrouve l’année prochaine !

Docker et déploiement distant : tout est histoire de contexte

docker context ssh gitlab gitlab-ci

Kubernetes, c’est bien mais parfois on veut faire des choses plus simples et on n’a pas forcément besoin d’avoir un système distribué ou d’une forte disponibilité. Pour autant, il est agréable avec kubernetes de pouvoir interagit à distance avec l’API au travers de kubectl. Je me suis donc mis en quête d’une alternative. Partir sur k3s avec un déploiement mono-node ? Partir sur docker/docker-compose/docker-swarm ?

Je me souvenais que l’on pouvait exposer la socket docker sur le réseau en TCP et authentification par certificat mais cela ne me plaisait pas beaucoup. Je me suis alors rappelé que l’on pouvait interagir avec un hote docker via une connection ssh. En continuant à creuser, je suis tomber sur les contextes dans Docker et ce billet How to deploy on remote Docker hosts with docker-compose. Et là : 🤩

Alors, certes, pas de secrets, configmaps ou cronjobs mais un déploiement remote que je peux automatiser dans gitlab et/ou avec lequel je peux interagir à distance 😍

Création d’un contexte puis utilisation d’un contexte :

# Création du contexte docker
docker context create mon-serveur ‐‐docker “host=ssh://user@monserveur”

# Lister les contextes docker existant
docker context ls

# Utiliser un contexte
docker context use mon-serveur

# Vérifier le bon fonctionnement de la cli docker
docker ps
[liste de conteneurs tournant sur la machine "mon-serveur"]

# Vérifier le bon fonctionnement de la cli docker-compose
cd /path/to/docker/compose/project
docker-compose ps

Pour que cela fonctionne, en plus des versions récentes de docker et docker-compose coté client et serveur. Il vous faut aussi une version récent de paramiko coté client. Celle présente dans Debian 10 n’est pas assez à jour par ex, il a fallu passer par pip3 install paramiko. Il faut aussi avoir une authentification par clé pour se simplifier la vie.

Pour docker-compose, vous avez deux solutions pour utiliser un contexte distant :

# En deux commandes
docker context use <remote context>
docker-compose <command>

# En une commande
docker-compose --context <remote context> <command>

Dans gitlab, dans le fichier .gitlab-ci.yml, on peut alors profiter de cette intégration de la façon suivante ; je prends l’exemple du déploiement du site du Paris Time Series Meetup.

Tout d’abord, dans mon fichier docker-compose.yml, j’ai mis un place holder IMAGE qui sera remplacé par la référence de mon image docker fabriquée par gitlab-ci :

version: '3.8'
services:
  web:
    image: IMAGE
    labels:
        [...]
    restart: always

Ensuite, dans gitlab-ci :

  • le job “publish” génère la version html du site et le stock dans un artefact gitlab.
  • le job “kaniko” va générer l’image docker contenant la version html du site. Pour passer la version de l’image, je stocke l’information dans un fichier docker.env. Ce fichier sera sauvegardé en fin de job sous la forme d’un artefact disponible pour le job “docker”.
  • le job “docker” recupère ce qui était dans le docker.env sous la forme de variable d’environnement. Il remplace ensuite IMAGE par sa vraie valeur. On initialise le contexte docker pour se connecter au serveur cible et on peut alors réaliser les actions habituelles avec docker-compose.
---
stages:
  - publish
  - image
  - deploy

publish:
  image:  $CI_REGISTRY/nsteinmetz/hugo:latest
  artifacts:
    paths:
      - public
    expire_in: 1 day
  only:
    - master
    - web
  script:
    - hugo
  stage: publish
  tags:
    - hugo

kaniko:
  stage: image
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  variables:
    RELEASE_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA-$CI_PIPELINE_ID-$CI_JOB_ID
  script:
    - echo "IMAGE=${RELEASE_IMAGE}" >> docker.env
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $RELEASE_IMAGE
  only:
    - master
    - web
  when: on_success
  tags:
    - kaniko
  artifacts:
    reports:
      dotenv: docker.env

docker:
  stage: deploy
  before_script:
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
  script:
    - sed -i -e "s|IMAGE|${IMAGE}|g" docker-compose.yml
    - docker context use mon-serveur
    - docker-compose pull
    - docker-compose up -d
  needs:
    - job: kaniko
      artifacts: true
  when: on_success
  only:
    - master
    - web
  tags:
    - shell
  environment:
    name: production
    url: https://www.ptsm.io/

Et voilà ! 😎

Avec ces contextes (que l’on peut utiliser aussi avec swarm et kubernetes), on a donc un moyen simple et efficace pour déployer des conteneurs à distance et de façon automatisée.

Web, Ops & Data - Novembre 2020

vitess mysql kubernetes helm mesos influxdb arrow parquet scp sftp gke gcp observability monitoring dig dns dog rust ovhcloud git

Ce soir, il y a la 8ème édition du Paris Time Series Meetup sur AWS TimeStream.

Cloud

Code

Container et orchestration

SQL

  • Announcing Vitess 8 : Vitess, la base distribuée prévue pour un déploiement sur kubernetes et avec une compatibilité MySQL arrive en version 8 et améliore son support de MySQL et des principales librairies et frameworks dans différents langages.

Système

  • Deprecating scp : qui n’a pas fait un scp file destination:/path/to/file ? La commande scp est victime de nombreuses failles. Du coup, elle va être dépréciée. Néanmoins une initiative vise à maintenir uen commande scp mais se fondant sur sftp et son modèle de sécurité.
  • ogham/dog : dog est une réécriture de dig en rust avec coloration syntaxique et différentes fonctionnalités comme le support de DoH, DoT, etc.
  • k6 : k6 est un outil de test de performance avec lequel on peut définir des scénarios plus ou moins élaborés suivant ses besoins ; je l’avais recommandé à un client pour faire des tests de performance d’API; la version 0.29 vient de sortir.

Timeseries

Astuce du mois

Pour ceux sous Fedora et utilisant podman en alternative au binaire docker, pour se connecter à la registry google (via):

gcloud auth print-access-token | podman login -u oauth2accesstoken --password-stdin gcr.io

InfluxDB 2.0 OSS - Notes de mise à jour

timeseries influxdb flux grafana telegraf

InfluxDB 0SS 2.0 étant sortie, j’ai testé la mise à jour d’une instance 1.8.3 vers 2.0.1 sur une VM Debian 10 à jour.

Mise à jour

La documentation pour une mise à jour 1.x vers 2.x est disponible. La vidéo “Path to InfluxDB 2.0: Seamlessly Migrate 1.x Data” reprend cela et va plus loin en présentant bien tous les points à prendre en compte (y compris pour Telegraf, Chronograf et Kapacitor). Je ne rajouterai donc que mes remarques.

Concernant la commande influxd upgrade :

  • Il est fort probable qu’il faille rajouter la commande sudo pour ne pas avoir de problèmes de permisisons.
  • Par défaut, les données migrées vont être mises dans ~/.influxdbV2. Or je doute que vous vouliez que vos données soient à cet endroit. Je vous invite donc à regarder la documentation de influxd upgrade pour définir les propriétés --engine-path et --bolt-path

Exemple:

mkdir -p /srv/influxdb/influxdb2
influxd upgrade --engine-path /srv/influxdb/influxdb2/engine --bolt-path /srv/influxdb/influxdb2/influxd.bolt
  • A l’issue de la migration, un fichier config.toml est généré dans /etc/influxdb/. Il contient quelques valeurs issues de la migration et des valeurs par défaut. Je l’ai personnalisé de la façon suivante pour tenir compte de mes valeurs :
bolt-path = "/srv/influx/influxdb2/influxd.bolt"
engine-path = "/srv/influx/influxdb2/engine"
http-bind-address = "127.0.0.1:8086"
storage-series-id-set-cache-size = 100
  • Post-migration, le service influxd cherchait à initialiser ses fichiers dans /var/lib/influxdb/.influxdbv2. Ayant noté que le service InfluxDB prennait /etc/default/influxdb comme fichier d’environnement, j’ai ajouté dans ce fichier :
# /etc/default/influxdb
INFLUXD_CONFIG_PATH=/etc/influxdb/config.toml

Dès lors, /etc/influxdb/config.toml était bien pris en compte et InfluxDB démarrait bien avec mes données.

Une fois InfluxDB 2 démarré, j’ai pu noter avec plaisir :

  • que l’ingestion via telegraf continuait à se faire sans interruption,
  • que mes dashboards Grafana continuaient à fonctionner.

Je n’ai donc pas d’urgence à migrer la configuration et le paramétrage de ces derniers. Je vais pouvoir le faire progressivement ces prochains jours.

N’utilisant pas Chronograf et Kapacitor, je n’ai pas eu de données à migrer ou d’ajustements à faire à ce niveau là. La vidéo reprend bien les points d’attention et les éventuelles limitations à prendre en compte dans le cadre de la migration.

Finalement, c’est pas mal qu’ils aient réintégrer les endpoints 1.x dans la version 2.0 à ce niveau là ;-)

La 2.0.2 étant sortie pendant ma mise à jour, j’ai poursuivi la mise à jour. Je suis tombé sur ce bug rendant l’écriture de données impossibles. Cela a mis en évidence un bug sur la migration des “retention policies” et sur le fait que j’avais aussi des très vieilles bases InfluxDB. Je n’aurai a priori pas eu ce bug en faisant la migration 1.8.3 vers 2.0.2. En tous cas, une 2.0.3 devrait donc arriver prochainement avec une amélioration du processus de migration faisant suite à ma séance de troubleshooting.

Migration des configurations

Elle peut se faire très progressivement - si par ex vous utilisez telegraf pour envoyer vos données et Grafana pour la partie dashboarding :

  • Vous pouvez mettre à jour votre configuration telegraf en passant de l’outputs influxdb à l’output influxdb_v2 sans impacter grafana qui continuera à accéder à vos données en InfluxQL
  • Vous pouvez ensuite mettre à jour votre datasource InfluxDB ou plutôt en créer une nouvelle et migrer vos dashboards progressivement sans interruption de service

Créer un accès en InfluxQL à un nouveau bucket

Si vous devez rétablir un accès à vos données via les API 1.x à un bucket nouvellement créé (j’ai profité de la migration pour mettre des buckets clients dans des organisations représentant les clients en question).

# Créer le bucket
influx bucket create --name <BUCKET_NAME> --retention 0  --org <ORGANISATION>
# Récupérer l'ID de bucket via la liste des buckets
influx bucket list
# Créer une DBRP (DataBase Retention Policies) pour le bucket en question - les accès en 1.x se font en mode  SELECT * FROM <db_name>.<retention_policies> ...
influx v1 dbrp create --bucket-id=<BUCKET_ID> --db=<BUCKET_NAME> --rp=autogen --default=true
# Créer un utilsateur sans mot de passe pour le moment
influx v1 auth create --username <USER> --read-bucket <BUCKET_ID> --write-bucket <BUCKET_ID> --org <ORGANISATION> --no-password
# Créer un mot de passe au format V1
influx v1 auth set-password --username <USER>

Les utilisateurs migrés depuis la version 1.x sont visibles via influx v1 auth list.

Intégration InfluxDB 2.0 / Flux et Grafana

Le support de Flux dans Grafan existe depuis la version 7.1 mais il n’est pas aussi aisé que celui dans InfluxDB 2.0 OSS. Il y a certes de la complétion au niveau du code ou le support des variables mais pas de capacité d’introspection sur la partie données.

Pour le moment, je procède donc de la façon suivante :

  • Création de la Requête via le Query Builder dans InfluxDB 0SS
  • Passage en mode “Script editor” pour dynamiser les variables ou ajuster certains comportements
  • Copier/coller dans l’éditeur de Grafana
  • Ajustement des variables pour les mettre au format attendu par Grafana.

Ex coté InfluxDB 2.0 OSS / Flux :

from(bucket: v.bucket)
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "net")
  |> filter(fn: (r) => r["_field"] == "bytes_recv" or r["_field"] == "bytes_sent")
  |> filter(fn: (r) => r["host"] == v.host)
  |> derivative(unit: v.windowPeriod, nonNegative: false)
  |> yield(name: "derivative")

La version dans Grafana :

from(bucket: "${bucket}")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "net")
|> filter(fn: (r) => r["_field"] == "bytes_recv" or r["_field"] == "bytes_sent")
|> filter(fn: (r) => r["host"] == "${host}")
|> derivative(unit: v.windowPeriod, nonNegative: false)
|> yield(name: "derivative")

La différence portant sur la gestion des variables v.host vs "${host}" et v.bucket vs "${bucket}".

Autre bonne nouvelle, les variables sont supportées dans Grafana ; vous pouvez donc définir les variables comme celles vu juste au-dessus :

Variable bucket de type “Query” en prenant InfluxDB/Flux comme datasource :

buckets()
  |> filter(fn: (r) => r.name !~ /^_/)
  |> rename(columns: {name: "_value"})
  |> keep(columns: ["_value"])

Variable host de type “Query” en prenant InfluxDB/Flux comme datasource :

# Provide list of hosts
import "influxdata/influxdb/schema"
schema.tagValues(bucket: v.bucket, tag: "host")

Si votre requête fonctionne dans un dashboard InfluxDB ou en mode explore mais qu’elle est tronquée dans Grafana, il vous faudra ajuster le “Max Data Points” pour récupérer plus de points pour cette requête (cf grafana/grafana#26484).

InfluxDB 2.0 OSS - Grafana - Max Data Points

Calcul de la durée d'un état avec des timeseries

timeseries influxdb flux warp10 warpscript duration

Un client m’a demandé la chose suivante : “Nicolas, je voudrais savoir la durée pendant laquelle mes équipements sont au delà d’un certain seuil ; je n’arrive pas à le faire simplement”.

Souvent, quand on manipule des séries temporelles, la requête est de la forme “Sur la période X, donne moi les valeurs de tel indicateur”. On a moins l’habitude de travailler dans le sens inverse, à savoir : “Donne moi les périodes de temps pour laquelle la valeur est comprise entre X et Y”.

C’est ce que nous allons chercher à trouver.

Influx 1.8 et InfluxQL

Avec l’arrivée imminente d’Influx 2.0, j’avoue ne pas avoir cherché la solution mais je ne pense pas que cela soit faisable purement en InfluxQL.

Influx 1.8 / 2.0 et Flux

Avec Flux, j’ai rapidement trouvé des fonctions comme duration et surtout stateDuration

L’exemple ci-dessous se fait avec une base InfluxDB 1.8.3 pour laquelle Flux a été activé. Le requêtage se fait depuis une instance Chronograf en version 1.8.5.

Pour approcher l’exemple de mon client, j’ai considéré le pourcentage d’inactivité des CPU d’un serveur que l’on obtient de la façon suivante:

from(bucket: "crntbackup/autogen")
  |> range(start: dashboardTime)
  |> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_idle" and r.cpu == "cpu-total")
  |> window(every: autoInterval)
  |> group(columns: ["_time", "_start", "_stop", "_value"], mode: "except")

Cela donne:

flux - duration 1

Ensuite, j’ai besoin d’une fonction qui va me rajouter une colonne avec mon état. Cet état est calculé en fonction de seuils - par souci de lisibilité, je vais extraire cette fonction de la façon suivante et appliquer la fonction à ma requête :

set_level = (tables=<-) =>
  tables
    |> map(fn: (r) => ({
      r with
      level:
        if r._value >= 95 then "fully_idle"
        else if r._value >= 90 and r._value <95 then "something_is_moving"
        else if r._value >= 85 and r._value <90 then "oh_oh"
        else if r._value >= 80 and r._value <85 then "hmm"
        else if r._value < 80 then "i_have_to_work"
        else "overloaded"
      })
    )

from(bucket: "crntbackup/autogen")
  |> range(start: dashboardTime)
  |> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_idle" and r.cpu == "cpu-total")
  |> window(every: autoInterval)
  |> group(columns: ["_time", "_start", "_stop", "_value"], mode: "except")
  |> set_level()

La colonne “level” n’est à ce stade pas persistée en base contrairement aux autres données issue de la base de données.

Cela donne ceci en mode “raw data” - tout à fait à droite

flux - duration 2

Maintenant que j’ai mon état, je peux application la fonction stateDuration() ; elle va calculer la périodes de temps où le seuil est “something_is_moving” par tranche de 1 seconde. Le résulat sera stocké dans une colonne “stateDuration”. Pour les autres états, la valeur est de -1. La valeur se remet à 0 à chaque fois que l’état est atteint puis la durée est comptée :

set_level = (tables=<-) =>
  tables
    |> map(fn: (r) => ({
      r with
      level:
        if r._value >= 95 then "fully_idle"
        else if r._value >= 90 and r._value <95 then "something_is_moving"
        else if r._value >= 85 and r._value <90 then "oh_oh"
        else if r._value >= 80 and r._value <85 then "hmm"
        else if r._value < 80 then "i_have_to_work"
        else "overloaded"
      })
    )

from(bucket: "crntbackup/autogen")
  |> range(start: dashboardTime)
  |> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_idle" and r.cpu == "cpu-total")
  |> window(every: autoInterval)
  |> group(columns: ["_time", "_start", "_stop", "_value"], mode: "except")
  |> set_level()
  |> stateDuration(fn: (r) => r.level == "something_is_moving", column: "stateDuration", unit: 1s)

On voit le rajout de la colonne stateDuration en mode “raw data” ; elle n’ont plus n’est pas persistée dans la base à ce stade :

flux - duration 4

et coté visualisation :

flux - duration 3

Maintenant que j’ai ces périodes, je vais vouloir savoir quelle est la durée totale de ces différentes périodes que nous avons identifée. On peut en effet imaginer un cas où on sait que l’équipement est à remplacer lorsqu’il a atteint un seuil donné pendant plus de X heures.

Pour cela, je vais:

  • filtrer sur un état voulu,
  • calculer le différentiel entre chaque valeur de stateDuration pour n’avoir que les écarts non plus la somme des durées en supprimant les valeurs négatives pour gérer les retours à la valeur 0
  • et faire la somme de l’ensemble.
set_level = (tables=<-) =>
  tables
    |> map(fn: (r) => ({
      r with
      level:
        if r._value >= 95 then "fully_idle"
        else if r._value >= 90 and r._value <95 then "something_is_moving"
        else if r._value >= 85 and r._value <90 then "oh_oh"
        else if r._value >= 80 and r._value <85 then "hmm"
        else if r._value < 80 then "i_have_to_work"
        else "overloaded"
      })
    )

from(bucket: "crntbackup/autogen")
  |> range(start: dashboardTime)
  |> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_idle" and r.cpu == "cpu-total")
  |> window(every: autoInterval)
  |> group(columns: ["_time", "_start", "_stop", "_value"], mode: "except")
  |> set_level()
  |> stateDuration(fn: (r) => r.level == "something_is_moving", column: "stateDuration", unit: 1s)
  |> filter(fn: (r) => r.level == "something_is_moving")
  |> derivative(unit: 10s, nonNegative: true, columns: ["stateDuration"], timeColumn: "_time")
  |> sum(column: "stateDuration")

Ce qui me donne un total de 2230 secondes pour l’heure (3600s) qui vient de s’écouler.

flux - duration 5

C’est un POC rapide pour démontrer la faisabilité de la chose. Le code est surement améliorable/perfectible.

Dans un contexte InfluxDB 2.0, il y a aussi la fonction events.duration qui semble intéressante. Ce billet “TL;DR InfluxDB Tech Tips – How to Monitor States with InfluxDB” montre aussi l’usage de la fonction monitor.stateChanges() qui peut compléter l’approche.

Influx 1.8 / Flux - variante pour les séries irrégulières

La fonction derivative impose d’avoir des durées régulières pour calculer le delta. Dans le cas d’une série irrégulière, cela peut coincer rapidement et fausser les calculs. On peut donc remplacer les deux dernières lignes par la fonction increase. Elle prend la différence entre deux valeurs consécutives (quelque soit leur timestamp) et réalise une somme cumulative. Les différences négatives sont ignorées de la même façon que nous le faisions précédemment.

set_level = (tables=<-) =>
  tables
    |> map(fn: (r) => ({
      r with
      level:
        if r._value >= 95 then "fully_idle"
        else if r._value >= 90 and r._value <95 then "something_is_moving"
        else if r._value >= 85 and r._value <90 then "oh_oh"
        else if r._value >= 80 and r._value <85 then "hmm"
        else if r._value < 80 then "i_have_to_work"
        else "overloaded"
      })
    )

from(bucket: "crntbackup/autogen")
  |> range(start: dashboardTime)
  |> filter(fn: (r) => r._measurement == "cpu" and r._field == "usage_idle" and r.cpu == "cpu-total")
  |> window(every: autoInterval)
  |> group(columns: ["_time", "_start", "_stop", "_value"], mode: "except")
  |> set_level()
  |> stateDuration(fn: (r) => r.level == "something_is_moving", column: "stateDuration", unit: 1s)
  |> filter(fn: (r) => r.level == "something_is_moving")
  |> increase(columns: ["stateDuration"])

La sortie change un peu car au lieu d’un nombre unique, on a l’ensemble des points filtrés et leur somme au fur et à mesure (colonne de droite):

flux - duration increase dataviz

Cela donne des possiblités différentes au niveau dataviz :

flux - duration increase dataviz

Warp 10 / WarpScript

En la même chose en WarpScript avec Warp 10, cela donne quoi ? Regardons cela :

'<readToken>' 'readToken' STORE

// Récupération des données de cpu de type "usage_idle" en ne prenant que le label "cpu-total"
[ $readToken '~crntd10monitoring.cpu.usage_idle' { 'cpu' 'cpu-total' } NOW 1 h ] FETCH
0 GET         // Fetch retourne une liste de GTS, on prend donc la première (et unique) GTS
'cpu' STORE   // Stockage dans une variable cpu

// Utilisation de BUCKETIZE pour créer une série régulière de données séparées par 1 seconde
// Mes données étant espacées d'environ 10s, cela va donc créer 10 entrées de 1 seconde au final
// Pour chaque espace, on utliise la dernière valeur connue de l'espace en question pour garder les valeurs de la GTS de départ
[
  $cpu
  bucketizer.last
  0
  1 s
  0
]
BUCKETIZE
// Les espaces insérés n'ont pas encore de valeurs associées
// On remplit les entrées sans valeurs avec les valeurs ci-dessus
// On utilise FILLPREVIOUS et FILLNEXT pour gérer aussi les premières et dernières valeurs
FILLPREVIOUS
FILLNEXT
// A ce stade, on a donc une GTS avec un point toute les secondes et la valeur associée. Cette valeur était la valeur que l'on avait toutes les 10s précédemment.

// On fait une copie de la GTS pour pouvoir comparer avec la version filtrée par ex
DUP

// On filtre sur les valeurs qui nous intéressent, ici on veut les valeurs >= 90 et < 95
[ SWAP 90.0 mapper.ge 0 0 0 ] MAP
[ SWAP 95.0 mapper.lt 0 0 0 ] MAP
// On renomme la liste (pratique si on affiche par ex l'ancienne et la nouvelle liste dans la partie dataviz - cf capture ci-dessous)
'+:above90below95' RENAME

// On compte le nombre d'élément de la GTS qui est sous la forme d'une liste de GTS à l'issu du MAP
0 GET SIZE

// On multiplie le nombre d'entrées par 1 s
1 s *

// on garde une copie de la valeur en secondes
DUP
// On applique le filtre HUMANDURATION qui transforme ce volume de secondes en une durée compréhensible
HUMANDURATION

warp10 - duration 1

On voit ci-dessous l’usage de DUP avec la valeur humainement lisible, la valeur brute en seconde (puis le reste de la pile):

warp10 - duration 1

Si on ne veut pas de dataviz / ne pas conserver les valeurs intermédiaires et n’avoir que la valeur finale, on peut supprimer les lignes avec DUP et RENAME.

'<readToken>' 'readToken' STORE
[ $readToken '~crntd10monitoring.cpu.usage_idle' { 'cpu' 'cpu-total' } NOW 1 h ] FETCH
0 GET
'cpu' STORE

[
  $cpu
  bucketizer.last
  0
  1 s
  0
]
BUCKETIZE
FILLPREVIOUS
FILLNEXT

[ SWAP 90.0 mapper.ge 0 0 0 ] MAP
[ SWAP 95.0 mapper.lt 0 0 0 ] MAP
0 GET SIZE
1 s *
HUMANDURATION

Et on obtient:

20m20.000000s

Un grand merci à Mathias Herberts pour sa disponiblité, sa patience et son aide face à toutes mes questions pour arriver à produire ce code.

Warp 10 / WarpScript - version agrégée

On peut aussi vouloir avoir une version agrégée de la donnée plutôt que de filter sur un état particulier. Ainsi, on peut avoir la répartition des valeurs que prend un équipement sur un indicateur donnée.

'<readToken>' 'readToken' STORE
// Récupération des métriques comme précédemment
[ $readToken '~crntd10monitoring.cpu.usage_idle' { 'cpu' 'cpu-total' } NOW 1 h ] FETCH
0 GET
'cpu' STORE

// Reformatage des données comme précédemment
[
  $cpu
  bucketizer.last
  0
  1 s
  0
]
BUCKETIZE
FILLPREVIOUS
FILLNEXT

// Utilisation de QUANTIZE
// QUANTIZE a besoin que l'on définisse des sous-ensembles dans un ensemble
// Notre indicateur CPU étant un pourcentage, on prend par ex 10 sous ensemble compris entre 0 et 100
// QUANTIZE gère aussi les cas où l'on est plus petit que la première valeur et plus grand que la derinère valeur de l'ensemble
0 100 10 LBOUNDS
// On a donc 10+2 = 12 sous-ensembles : ]-infini,0],[1, 10],[11, 20],...,[90, 100],[101, inf+[
// Pour chaque valeur que nous allons passer à QUANTIZE, elle va retourer une valeur associée au sous ensemble dans laquelle la valeur va "tomber".
// Ainsi, un valeur de 95% va aller dans gt90.
// Liste des valeurs pour les 12 sous-ensembles :
[ 'neg' 'gt0' 'gt10' 'gt20' 'gt30' 'gt40' 'gt50' 'gt60' 'gt70' 'gt80' 'gt90' 'gt100' ]
QUANTIZE
// A ce stade, notre GTS de départ ne contient plus les valeurs de cpu mais les valeurs associées au tableau de QUANTIZE
// on passe donc de [<timestamp>, 95.45] à [<timestamp>, 'gt90']

// Utilisation de VALUEHISTOGRAM qui va compter le nombre d'occurences de chaque valeur d'une liste de GTS
VALUEHISTOGRAM

On obtient alors :

[{"gt90":3491,"gt80":40,"gt70":40,"gt60":10}]

Et voilà !

7 8 9 10 11