L’observabilité appliqué au Serverless

Contexte

Aujourd’hui, de nouveaux services font parler d’eux dans le monde de l’IT et principalement lorsque l’on parle de fournisseur de services Cloud, c’est le Serverless.

Vous retrouverez de nombreux articles qui parlent de ce sujet dans sa globalité, comme celui sur “Les Architectures Serverless”.

Aujourd’hui nous nous attaquerons à un sujet bien précis : l’observabilité ; nous verrons ensemble les impacts sur votre conception lorsque l’on applique ces principes aux architectures Serverless.

Petit rappel avant de commencer

Histoire que tout le monde parte avec le même niveau d’information, il me semble important de faire un rappel sur ce qu’est l’observabilité.

L’observabilité est un terme qui possède de nombreuses définitions selon les personnes et les profils. Pour faire simple, cela permet au travers de ses 3 piliers (Traces, Métriques, Journaux d’événements), d’avoir une meilleure visibilité de l’état interne de votre système.

Partons des vérités suivantes:

  • votre système n’est jamais totalement sain,
  • un système distribué est imprévisible,
  • l’échec doit être pris en compte dans chacune de nos phases de développements, de la conception à l’exploitation en passant par les phases de tests et de déploiement,
  • votre capacité d’identifier un problème (debug) est un facteur clé de réussite pour la maintenance et l’évolution de votre système.

Ceci donne une vision assez représentative de ce que l’observabilité doit apporter et des problématiques qu’elle doit permettre de résoudre.

Étudions plus en détail ces 3 piliers :


Si vous voulez en savoir plus sur la définition de l’observabilité, je vous invite à regarder le talk de Charity Majors ou de lire l’article de Cindy Sridharan sur le sujet.

Les journaux d’événements (logs)

Un journal d’événements est un enregistrement immuable et horodaté d'événements qui se sont produits au fil du temps au sein de votre application. ces événements se présentent généralement sous différents formats (texte en clair, JSON, binaire).

Les données des journaux d’événements ne sont pas utilisées exclusivement pour de l’évaluation de performance ou dans le cadre d’un débogage. Elles peuvent aussi faire l’objet d’une source d’informations pour vos analyses business comme de la veille stratégique ou pour connaître l’utilisation de votre application.

Les métriques

Les métriques sont une représentation numérique des données mesurées sur des intervalles de temps. Elles peuvent exploiter la puissance de la modélisation mathématique et de la prédiction pour obtenir des informations sur le comportement d'un système au fil du temps, dans le présent et le futur.

Puisque les nombres sont optimisés pour le stockage, le traitement, la compression et la récupération, ces données peuvent être conservées plus longtemps et exploitées par des requêtes spécialisées (eg. médianes, moyennes, quantiles, dérivées).  Cela les rend aussi parfaitement adaptées à la création de tableaux de bord reflétant les tendances historiques et peuvent être stockées dans des bases de données dédiées de type Time series. Nous voyons même l’émergence d’algorithmes de type prédictif pour prédire leur comportement.

Avantage des métriques par rapport aux journaux d’événements

Globalement, le principal avantage de la surveillance basée sur des métriques par rapport aux journaux est que, contrairement à la génération et au stockage de journaux, le transfert et le stockage de métriques entraînent une charge constante. Contrairement aux journaux, le coût des métriques n'augmente pas selon le trafic utilisateur ou à toute autre activité du système susceptible d'entraîner une nette augmentation des données.

Ainsi, avec les métriques, même si votre système supporte une charge croissante, cela n'entraînera pas d’augmentation de la complexité du traitement, de la vitesse de visualisation et des coûts opérationnels (license, stockage, …), contrairement aux journaux.

Les métriques sont également mieux adaptées pour déclencher des alertes, car exécuter des requêtes sur une base de données Time series en mémoire est beaucoup plus efficace et plus fiable, que d'exécuter une requête sur un système distribué comme Elasticsearch, puis d'agréger les résultats avant de décider si une alerte doit être déclenchée ou non. D’autant plus que si l’on se base sur les journaux d’événements comme source de métriques systèmes, nous pourrions avoir des erreurs d’interprétations, du simple fait que les valeurs sont agrégées sous forme de moyenne et peuvent ainsi masquer des événements.

Les traces

Une trace est définie comme la représentation d'une série d'événements distribués, liés de manière causale, qui permet de suivre le flux de requêtes de bout en bout sur un système.

Une seule trace peut fournir une visibilité à la fois sur le chemin parcouru par une requête ainsi que sur la structure d'une requête. Ce chemin permet aux développeurs et aux opérationnels de comprendre les différents services impliqués et d’identifier les éventuels points de blocage.


Lorsqu'une requête commence, un ID unique global lui est attribué que l’on nomme souvent ID de corrélation. Celui-ci est ensuite propagé dans tout le chemin de la requête afin que chaque instrumentation puisse insérer ou enrichir les informations avant de transmettre l'ID au saut suivant. Chacun des sauts enregistrera son événement et permettra par la suite de reconstituer le chemin global de la requête à partir de l’identifiant fourni sur l’ensemble des messages.

Nos nouveaux challenges avec le Serverless

Avec les technologies Serverless comme AWS Lambda, nous sommes confrontés à un certain nombre de nouveaux défis pour appliquer ces principes, vu la jeunesse de la technologie et l’inadéquation de certains de nos outils historiques.

Le principal impact de ces services “sans serveur” est justement l’impossibilité d’accès à notre infrastructure sous-jacente et ainsi notre incapacité à déployer notre agent de supervision. Nous ne pourrons plus utiliser nos briques standard pour récupérer, compresser et envoyer nos informations sur notre système d'observabilité. Dorénavant ces fonctionnalités seront incluses directement au sein de chacune de nos fonctions et ne pourront être exploitées qu’au travers des services dédiés fournis par le fournisseur de services Cloud.

Un autre aspect à prendre en compte est justement cet aspect d’exécution unitaire. Sur un système traditionnel, nous devions gérer une quantité d’événements en lien avec le volume de requêtes que ce système (instance) pouvait traiter. Aujourd’hui, vu que nous avons une exécution par requête, cette gestion de la concurrence sera nativement embarquée et gérée par le service lui-même. Nous n’aurons plus à nous soucier de ce type de problématique et c’est une bonne nouvelle. En revanche, vous devrez garder en tête que cela risque d’impacter votre système d’observabilité. Au lieu d’avoir un ensemble d’événements envoyé par paquets, nous aurons des événements unitaires envoyés par chacune de nos fonctions et cette nouvelle charge devra être traitée par votre système et engendrera de nouveaux coûts ou du moins une réévaluation de votre infrastructure.

La transmission d’informations de bout en bout

De nos jours les architectures deviennent de plus en plus complexes. L’une des conséquences de cette complexité est le nombre grandissant d'interactions entre les services.

Voici 2 parfaits exemples d’architecture contenant plus de 500 micro services avec Netflix et Amazon :


Lorsque l’on réalise ce type d’architecture, on se heurte forcément à la problématique de la gestion des journaux d’événements et de notre capacité à garantir un suivi de bout en bout lors de l’exécution d’une requête sur plusieurs services. Ce cas est d’autant plus vrai lorsque l’on parle d’architecture Serverless où chacune de nos fonctions correspond à une entité indépendante. Une autre problématique se trouve aussi sur notre capacité à garantir ce lien au travers des briques d’infrastructures managées parfois méconnues.

Certains services proposent nativement ce genre de fonctionnalité à un certain niveau. Nous prendrons l’exemple ici d’AWS X-Ray qui permet simplement d’avoir une représentation des interactions entre nos services :

Toutefois, ce service n’apporte pas encore toutes les fonctionnalités nécessaires surtout lorsque l’on souhaite investiguer et identifier un problème spécifique dans notre chaîne de traitement. Prenons en exemple un cas réel d’utilisation. Pour ce faire nous utiliserons l’architecture suivante comme modèle :



Cette architecture illustre un cas typique de communication entre plusieurs services métiers composés de différentes briques Serverless. Lorsqu’un développeur va vouloir identifier la cause d’une erreur survenue en production il devra :

  • Récupérer les derniers journaux d’événements avec le contexte d’exécution pour analyser les différents entrants et sortants,
  • Connaître la chaîne d’exécution de sa requête et possiblement identifier les points de blocages.

Dans un contexte Serverless, si vous avez prévu de positionner le flag DEBUG au travers des variables d’environnements, vous arriverez rapidement au problème de la prise en compte de ce changement. Rappelons-le, une fonction bénéficiera de cette mise à jour uniquement lors de la création d’une nouvelle instance ce qui induira un Cold start et donc une latence pour vos utilisateurs finaux. L’autre problème est de devoir pousser cette modification sur l’intégralité de votre chaîne, ce qui selon sa complexité, peut être un travail fastidieux.

Si vous souhaitez mettre en place une solution plus pérenne, la meilleure solution est de passer directement un paramètre au sein de votre requête et de le transmettre sur les différentes briques technologiques. Vous serez alors capable de positionner plusieurs flags comme celui du DEBUG lors de l’envoi de votre requête ou de générer un ID de corrélation lors du premier traitement afin d’avoir un suivi sur l’ensemble de votre chaîne d’exécution. Dans le cas de notre architecture, nous aurions le traitement suivant :


Nous observons que la valeur “debug-enabled” initialisée lors du premier appel API du Service A est transmise au travers de l’ensemble du système à l’identique de notre ID de corrélation “x-correlation-id” généré au sein de la Lambda A.

Maintenant, si nous détaillons un peu plus cette transmission par service, nous avons :

  • ApiGateway : comme toute requête HTTP, nous pouvons transmettre ce style de données via les headers de la requête,
  • Lambda : les informations sont récupérées directement depuis l’événement source,
  • Kinesis : malheureusement, ce service ne permet pas d’ajouter des informations de contexte à nos enregistrements. Pour ce faire, il nous faudra surcharger le contenu des données transmises dans l’élément “Data”,
  • SNS : sur ce service, nous pouvons utiliser le champ “MessageAttributes” pour envoyer nos informations.

Il vous suffira désormais de récupérer et de fournir le traitement adéquat pour ce type d’information. Vous aurez ainsi la capacité de déclencher le mode DEBUG sur l’ensemble de votre chaîne uniquement via le positionnement du header “debug-enabled” par le développeur. Attention toutefois à ajouter une couche d'autorisation pour éviter tous les abus possibles et générer des coûts non désirés sur votre système d’observabilité.

L’envoi de métriques personnalisées

Il est important de pouvoir récupérer des informations non standard sur l’exécution de nos fonctions, nous prendrons l’exemple de la consommation mémoire et du temps de facturation de notre fonction.

Ces deux valeurs mises en corrélation vont vous permettre d’optimiser votre fonction en trouvant le meilleur ratio mémoire / performance. Prenons l’exemple d’une fonction de 512Mb qui s’exécute en 42ms. Sachant qu’AWS Lambda fonctionne par bloc de 100ms, vous avez une perte de 58ms. Vous pourrez alors réévaluer votre configuration et descendre la mémoire utilisée à 256Mb ce qui aura un impact direct sur votre facture. Un autre avantage d’avoir ce temps de facturation sous forme d’une métrique, est votre capacité à l’intégrer directement dans votre processus de développement voire même dans votre pipeline d’intégration afin d’identifier les augmentations soudaines de complexité de votre code ou un mauvais algorithme. Rappelons-le, l’optimisation de votre code a un impact direct sur votre facture (exemple d’une fonction qui s’exécute en 300ms au lieu de 500ms vous fera réduire votre coût en fin de mois de 40%).

Pour l’extraction, nous nous baserons sur l'approche de Datadog concernant l'envoi de métriques personnalisées vu sa simplicité de mise en place.

Le format

Si nous reprenons l’explication fournie par Datadog au sein de sa documentation, pour envoyer des métriques personnalisées, il faut envoyer un message avec le format DogStatsD.

MONITORING|unix_epoch_timestamp|metric_value|metric_type|my.metric.name|#tag1:value,tag2

metric_type est soit count, gauge, histogram, ou check.

Vous pouvez également utiliser l'API HTTP de Datadog pour les soumettre à partir de votre fonction Lambda directement.

Extraction des informations

Revenons à nos données. Pour extraire les 2 métriques souhaitées qui sont la mémoire consommée et le temps de facturation, nous allons devoir traiter les journaux d’événements de notre fonction.

Pour rappel, lors de l’exécution d’une fonction AWS Lambda, nous pouvons récupérer la trace suivante :


À la fin de chaque invocation, AWS Lambda publie un message REPORT détaillant la mémoire consommée par votre fonction pendant cette invocation, et votre temps facturé.

Concernant l’extraction, AWS nous permet de créer directement un flux de nos événements et de les traiter par le biais d’une fonction AWS Lambda. Cette fonctionnalité est faite pour agréger nos messages, les traiter et les envoyer à un système d’observabilité.

Et une lambda, une !

Intéressons-nous au code de notre fonction d’extraction. Vous trouverez ci-joint un exemple du code Python pour le traitement de ce rapport, l’extraction des informations et l’envoi sur votre serveur Datadog.

from __future__ import print_function

import base64
import gzip
import json
import os
import re
import boto3

# Imported through Lambda Layer
from datadog import datadog_lambda_wrapper, lambda_metric

# Pass custom tags as environment variable, ensure comma separated, no trailing comma in envvar!
DD_TAGS = os.environ.get("DD_TAGS", "")

class metric:
    def __init__(self):
        self.name = ""
        self.value = 0

@datadog_lambda_wrapper
def lambda_handler(event, context):
    try:
        metrics = awslogs_handler(event, context)
        for metric in metrics:
            for key in metric:
                lambda_metric(convert_snake_case(key.replace('-', '_')), metric[key], tags=[DD_TAGS])
    except Exception as e:
        print("Unexpected exception: {} for event {}".format(str(e), event))

# Handle CloudWatch logs
def awslogs_handler(event, context):
    # Get logs
    logs = json.loads(gzip.decompress(base64.b64decode(event["awslogs"]["data"])))

    # For Lambda logs we want to extract the function name,
    # then use it to generate custom metric name.
    # Start by splitting the log group to get the function name
    log_group_parts = logs["logGroup"].split("/lambda/")
    if len(log_group_parts) > 0:
        function_name = log_group_parts[1].lower()

    # Extract structured custom metric
    for log in logs["logEvents"]:
        if log['message'] and log['message'].startswith('REPORT RequestId:'):
            # Split message based on tabultations
            parts = log['message'].split('\t',5)
            metric = {
                function_name + "_billed_duration": re.findall("Billed Duration: (.*) ms", parts[2])[0],
                function_name + "_memory_used": re.findall("Max Memory Used: (.*) MB", parts[4])[0],
                function_name + "_memory_size": re.findall("Memory Size: (.*) MB", parts[3])[0]
            }
            yield metric

# Convert a name to snake case
def convert_snake_case(name):
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

Vous retrouverez le code ainsi que les scripts pour le déploiement de cette fonction sur mon répertoire Github (https://github.com/stevehouel/datadog-serverless-functions-metrics).

Attention toutefois à vos limites

Je dois vous mettre en garde sur un point : la limite d’exécutions concurrentes. Comme vous devez sûrement le savoir, AWS Lambda est soumis à une soft limit (https://docs.aws.amazon.com/fr_fr/lambda/latest/dg/concurrent-executions.html) qui est de 1000 exécutions simultanées pour l’ensemble de vos fonctions. Selon votre volume de messages et de fonctions, vous pourrez être amené à rapidement l’atteindre. Cependant, l’une des fonctionnalités de ce service vous permet de fixer cette limite par fonction :

Vous pourrez alors maîtriser vos exécutions et ne pas vous retrouver dans le cas d’une panne en cascade de votre système.

Conclusion

Nous l’avons vu, l’observabilité ne peut pas être comparée au simple fait d’ajouter une couche de monitoring à son système. Cela va au-delà, elle doit donner la capacité, au travers de différents types d’informations, d’avoir la connaissance nécessaire à la compréhension de son système ainsi que d’identifier et résoudre les problèmes qui peuvent survenir. Les systèmes basés sur les services Serverless ne dérogent pas à ces règles, la complexité en revanche se trouve dans la jeunesse de son écosystème et de l’intégration des différents outils spécialisés comme Datadog, Espagon, Thundra ou Dashbird avec les fournisseurs de services Cloud.

Toutefois, il y a une réelle mobilisation de la communauté et des entreprises pour rapidement remédier à ces problématiques et apporter toutes les fonctionnalités avancées existantes (Machine Learning, Prédiction de panne, …) et les appliquer aux cas d'utilisations spécifiques du Serverless.

Author image
Architecte Cloud & DevOps chez Ippon Technologies, évangéliste Serverless et contributeur sur des projets Open Source comme JHipster et daSWAG. Il est fier d’être un #GeekEnthusiast.
Lyon LinkedIn