Aller au contenu

worker-events: Événements Inter-Worker pour NGINX en Lua Pur

Installation

Si vous n'avez pas encore configuré l'abonnement au dépôt RPM, inscrivez-vous. Ensuite, vous pouvez procéder avec les étapes suivantes.

CentOS/RHEL 7 ou Amazon Linux 2

yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-worker-events

CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-worker-events

Pour utiliser cette bibliothèque Lua avec NGINX, assurez-vous que nginx-module-lua est installé.

Ce document décrit lua-resty-worker-events v2.0.1 publié le 28 juin 2021.


lua-resty-worker-events

Événements inter-processus pour les processus de travail Nginx

Synopsis

http {
    # la taille dépend du nombre d'événements à gérer :
    lua_shared_dict process_events 1m;

    init_worker_by_lua_block {
        local ev = require "resty.worker.events"

        local handler = function(data, event, source, pid)
            print("événement reçu ; source=",source,
                  ", événement=",event,
                  ", données=", tostring(data),
                  ", du processus ",pid)
        end

        ev.register(handler)

        local ok, err = ev.configure {
            shm = "process_events", -- défini par "lua_shared_dict"
            timeout = 2,            -- durée de vie des données d'événement uniques dans shm
            interval = 1,           -- intervalle de sondage (secondes)

            wait_interval = 0.010,  -- attendre avant de réessayer de récupérer les données d'événement
            wait_max = 0.5,         -- temps d'attente maximum avant d'abandonner l'événement
            shm_retries = 999,      -- réessais pour la fragmentation shm (pas de mémoire)
        }
        if not ok then
            ngx.log(ngx.ERR, "échec du démarrage du système d'événements : ", err)
            return
        end
    }

    server {
        ...

        # exemple de sondage :
        location = /some/path {

            default_type text/plain;
            content_by_lua_block {
                -- appeler manuellement `poll` pour rester à jour, peut être utilisé à la place,
                -- ou avec l'intervalle du minuteur. Le sondage est efficace,
                -- donc si rester à jour est important, cela est préférable.
                require("resty.worker.events").poll()

                -- faire des choses régulières ici

            }
        }
    }
}

Description

Ce module fournit un moyen d'envoyer des événements aux autres processus de travail dans un serveur Nginx. La communication se fait par le biais d'une zone de mémoire partagée où les données d'événement seront stockées.

L'ordre des événements dans tous les workers est garanti d'être le même.

Le processus de travail mettra en place un minuteur pour vérifier les événements en arrière-plan. Le module suit un modèle singleton et s'exécute donc une fois par worker. Cependant, si rester à jour est important, l'intervalle peut être défini à une fréquence inférieure et un appel à poll à chaque requête reçue garantit que tout est traité dès que possible.

La conception permet 3 cas d'utilisation :

  1. diffuser un événement à tous les processus de travail, voir post. Dans ce cas, l'ordre des événements est garanti d'être le même dans tous les processus de travail. Exemple : un contrôle de santé s'exécutant dans un worker, mais informant tous les workers d'un nœud en amont échoué.
  2. diffuser un événement uniquement au worker local, voir post_local.
  3. regrouper des événements externes en une seule action. Exemple : tous les workers surveillent des événements externes indiquant qu'un cache en mémoire doit être rafraîchi. Lorsqu'ils le reçoivent, ils le publient tous avec un hachage d'événement unique (tous les workers génèrent le même hachage), voir le paramètre unique de post. Maintenant, un seul worker recevra l'événement une seule fois, donc un seul worker touchera la base de données en amont pour rafraîchir les données en mémoire.

Ce module lui-même déclenchera deux événements avec source="resty-worker-events" ; * event="started" lorsque le module est d'abord configuré (note : le gestionnaire d'événements doit être enregistré avant d'appeler configure pour pouvoir attraper l'événement) * event="stopping" lorsque le processus de travail se termine (basé sur un paramètre de minuteur premature)

Voir event_list pour utiliser des événements sans valeurs/chaînes magiques codées en dur.

Dépannage

Pour dimensionner correctement le shm, il est important de comprendre comment il est utilisé. Les données d'événement sont stockées dans le shm pour les transmettre aux autres workers. En tant que tel, il y a 2 types d'entrées dans le shm :

  1. événements qui doivent être exécutés par un seul worker (voir le paramètre unique de la méthode post). Ces entrées obtiennent un ttl dans le shm et expireront donc.
  2. tous les autres événements (sauf les événements locaux qui n'utilisent pas le SHM). Dans ces cas, aucun ttl n'est défini.

Le résultat de ce qui précède est que le SHM sera toujours plein ! donc ce n'est pas une métrique à examiner.

Comment prévenir les problèmes :

  • la taille du SHM doit au moins être un multiple de la charge utile maximale attendue. Elle doit pouvoir accueillir tous les événements qui pourraient être envoyés dans un interval (voir configure).
  • les erreurs no memory ne peuvent pas être résolues en augmentant la taille du SHM. La seule façon de résoudre cela est d'augmenter l'option shm_retries passée à configure (qui a déjà une valeur par défaut élevée). Cela est dû au fait que l'erreur est causée par la fragmentation et non par un manque de mémoire.
  • l'erreur waiting for event data timed out se produit si les données d'événement sont évincées avant que tous les workers aient eu le temps de les traiter. Cela peut se produire s'il y a une rafale d'événements (à charge utile importante). Pour résoudre cela :

    • essayez d'éviter les grandes charges utiles d'événements
    • utilisez un interval plus petit, afin que les workers vérifient (et traitent) les événements plus fréquemment (voir l'option interval passée à configure)
    • augmentez la taille du SHM, de sorte qu'il puisse contenir toutes les données d'événement qui pourraient être envoyées dans 1 intervalle.

Méthodes

configure

syntax: success, err = events.configure(opts)

Initialisera l'écouteur d'événements. Cela devrait généralement être appelé depuis le gestionnaire init_by_lua, car cela garantira que tous les workers commencent avec le premier événement. En cas de rechargement du système (démarrage de nouveaux et arrêt des anciens workers), les événements passés ne seront pas rejoués. Et parce que l'ordre dans lequel les workers se rechargent ne peut pas être garanti, le démarrage de l'événement ne peut également pas être garanti. Donc, si un certain type d'état est dérivé des événements, vous devez gérer cet état séparément.

Le paramètre opts est une table Lua avec des options nommées :

  • shm : (obligatoire) nom de la mémoire partagée à utiliser. Les données d'événement ne périmeront pas, donc le module s'appuie sur le mécanisme lru du shm pour évincer les anciens événements du shm. En tant que tel, le shm ne devrait probablement pas être utilisé à d'autres fins.
  • shm_retries : (optionnel) nombre de réessais lorsque le shm renvoie "no memory" lors de la publication d'un événement, par défaut 999. Chaque fois qu'il y a une tentative d'insertion et qu'aucune mémoire n'est disponible (soit aucun espace n'est disponible, soit la mémoire est disponible mais fragmentée), "jusqu'à des dizaines" d'anciennes entrées sont évincées. Après cela, s'il n'y a toujours pas de mémoire disponible, l'erreur "no memory" est renvoyée. Réessayer l'insertion déclenche la phase d'éviction plusieurs fois, augmentant la mémoire disponible ainsi que la probabilité de trouver un bloc de mémoire contigu suffisamment grand disponible pour les nouvelles données d'événement.
  • interval : (optionnel) intervalle pour sonder les événements (en secondes), par défaut 1. Définir à 0 pour désactiver le sondage.
  • wait_interval : (optionnel) intervalle entre deux tentatives lorsqu'un nouvel eventid est trouvé, mais que les données ne sont pas encore disponibles (en raison du comportement asynchrone des processus de travail)
  • wait_max : (optionnel) temps maximum à attendre pour les données lorsqu'un id d'événement est trouvé, avant d'abandonner l'événement. C'est un paramètre de sécurité au cas où quelque chose se serait mal passé.
  • timeout : (optionnel) délai d'expiration des données d'événement uniques stockées dans le shm (en secondes), par défaut 2. Voir le paramètre unique de la méthode post.

La valeur de retour sera true, ou nil et un message d'erreur.

Cette méthode peut être appelée plusieurs fois pour mettre à jour les paramètres, sauf pour la valeur shm qui ne peut pas être changée après la configuration initiale.

NOTE : le wait_interval est exécuté en utilisant la fonction ngx.sleep. Dans les contextes où cette fonction n'est pas disponible (par exemple, init_worker), elle exécutera une attente active pour exécuter le délai.

configured

syntax: is_already_configured = events.configured()

Le module d'événements fonctionne comme un singleton par processus de travail. La fonction configured permet de vérifier s'il est déjà opérationnel. Une vérification avant de démarrer des dépendances est recommandée ;

local events = require "resty.worker.events"

local initialization_of_my_module = function()
    assert(events.configured(), "Veuillez configurer le module 'lua-resty-worker-events' "..
           "avant d'utiliser my_module")

    -- faire l'initialisation ici
end

event_list

syntax: _M.events = events.event_list(sourcename, event1, event2, ...)

Fonction utilitaire pour générer des listes d'événements et éviter les fautes de frappe dans les chaînes magiques. Accéder à un événement non existant dans la table retournée entraînera une 'erreur d'événement inconnu'. Le premier paramètre sourcename est un nom unique qui identifie la source de l'événement, qui sera disponible comme champ _source. Tous les paramètres suivants sont les événements nommés générés par la source d'événements.

Exemple d'utilisation ;

local ev = require "resty.worker.events"

-- Exemple de source d'événements

local events = ev.event_list(
        "my-module-event-source", -- disponible comme _M.events._source
        "started",                -- disponible comme _M.events.started
        "event2"                  -- disponible comme _M.events.event2
    )

local raise_event = function(event, data)
    return ev.post(events._source, event, data)
end

-- Publier mon propre événement 'started'
raise_event(events.started, nil) -- nil pour plus de clarté, aucune donnée d'événement n'est passée

-- définir ma table de module
local _M = {
  events = events   -- exporter la table des événements

  -- l'implémentation se fait ici
}
return _M

-- Exemple de client d'événements ;
local mymod = require("some_module")  -- module avec une table `events`

-- définir un rappel et utiliser la table des événements du module source
local my_callback = function(data, event, source, pid)
    if event == mymod.events.started then  -- 'started' est le nom de l'événement

        -- événement démarré du module resty-worker-events

    elseif event == mymod.events.stoppping then  -- 'stopping' est le nom de l'événement

        -- ce qui précède générera une erreur en raison de la faute de frappe dans `stoppping`

    end
end

ev.register(my_callback, mymod.events._source)

poll

syntax: success, err = events.poll()

Sondera les nouveaux événements et les traitera tous (appelera les rappels enregistrés). L'implémentation est efficace, elle vérifiera uniquement une seule valeur de mémoire partagée et retournera immédiatement si aucun nouvel événement n'est disponible.

La valeur de retour sera "done" lorsqu'il aura traité tous les événements, "recursive" s'il était déjà dans une boucle de sondage, ou nil + error si quelque chose s'est mal passé. Le résultat "recursive" signifie simplement que l'événement a été publié avec succès, mais pas encore traité, en raison d'autres événements qui doivent être traités en premier.

post

syntax: success, err = events.post(source, event, data, unique)

Publiera un nouvel événement. source et event sont tous deux des chaînes. data peut être n'importe quoi (y compris nil) tant qu'il est (dé)serialisable par le module cjson.

Si le paramètre unique est fourni, alors un seul worker exécutera l'événement, les autres workers l'ignoreront. De plus, tous les événements de suivi avec la même valeur unique seront ignorés (pour la période de timeout spécifiée à configure). Le processus exécutant l'événement ne sera pas nécessairement le processus publiant l'événement.

La valeur de retour sera true lorsque l'événement a été publié avec succès ou nil + error en cas d'échec.

Note : le processus de travail envoyant l'événement recevra également l'événement ! Donc, si la source d'événement agira également sur l'événement, elle ne devrait pas le faire à partir du code de publication de l'événement, mais seulement lors de sa réception.

post_local

syntax: success, err = events.post_local(source, event, data)

Identique à post sauf que l'événement sera local au processus de travail, il ne sera pas diffusé aux autres workers. Avec cette méthode, l'élément data ne sera pas jsonifié.

La valeur de retour sera true lorsque l'événement a été publié avec succès ou nil + error en cas d'échec.

register

syntax: events.register(callback, source, event1, event2, ...)

Enregistrera une fonction de rappel pour recevoir des événements. Si source et event sont omis, alors le rappel sera exécuté sur tous les événements, si source est fourni, alors seuls les événements avec une source correspondante seront transmis. Si (un ou plusieurs) noms d'événements sont donnés, alors seulement lorsque les deux source et event correspondent, le rappel est invoqué.

Le rappel doit avoir la signature suivante ;

syntax: callback = function(data, event, source, pid)

Les paramètres seront les mêmes que ceux fournis à post, sauf pour la valeur supplémentaire pid qui sera le pid du processus de travail d'origine, ou nil s'il s'agissait d'un événement local uniquement. Toute valeur de retour du callback sera ignorée. Note : data peut être un type de référence de données (par exemple, un type de table Lua). La même valeur est passée à tous les rappels, donc ne changez pas la valeur dans votre gestionnaire, à moins que vous ne sachiez ce que vous faites !

La valeur de retour de register sera true, ou elle générera une erreur si callback n'est pas une valeur de fonction.

AVERTISSEMENT : les gestionnaires d'événements doivent retourner rapidement. Si un gestionnaire prend plus de temps que la valeur de timeout configurée, les événements seront abandonnés !

Note : pour recevoir l'événement started du processus lui-même, le gestionnaire doit être enregistré avant d'appeler configure

register_weak

syntax: events.register_weak(callback, source, event1, event2, ...)

Cette fonction est identique à register, à l'exception que le module ne conservera que des références faibles à la fonction callback.

unregister

syntax: events.unregister(callback, source, event1, event2, ...)

Désenregistrera la fonction de rappel et l'empêchera de recevoir d'autres événements. Les paramètres fonctionnent exactement de la même manière qu'avec register.

La valeur de retour sera true si elle a été supprimée, false si elle n'était pas dans la liste des gestionnaires, ou elle générera une erreur si callback n'est pas une valeur de fonction.

Historique

Publication de nouvelles versions

  • assurez-vous que le changelog ci-dessous est à jour
  • mettez à jour le numéro de version dans le code
  • créez un nouveau rockspec dans ./rockspecs
  • validez avec le message release x.x.x
  • taguez le commit comme x.x.x
  • poussez le commit et les tags
  • téléchargez sur luarocks

2.0.1, 28-juin-2021

  • correction : possible interblocage dans la phase 'init'

2.0.0, 16-septembre-2020

  • CHANGEMENT MAJEUR : la fonction post n'appelle plus poll, rendant tous les événements asynchrones. Lorsqu'un traitement immédiat d'un événement est nécessaire, un appel explicite à poll doit être effectué.
  • CHANGEMENT MAJEUR : la fonction post_local n'exécute plus immédiatement l'événement, rendant tous les événements locaux asynchrones. Lorsqu'un traitement immédiat d'un événement est nécessaire, un appel explicite à poll doit être effectué.
  • correction : éviter de tourner à 100 % CPU lors d'un rechargement lorsque le shm d'événements est effacé
  • correction : amélioration de la journalisation en cas d'échec d'écriture dans le shm (ajout de la taille de la charge utile à des fins de dépannage)
  • correction : ne pas enregistrer la charge utile, car cela pourrait exposer des données sensibles à travers les journaux
  • changement : mise à jour de la valeur par défaut de shm_retries à 999
  • changement : changement de la boucle de minuteur en une boucle de sommeil (performance)
  • correction : lors de la reconfiguration, assurez-vous que la table des rappels est initialisée

1.1.0, 23-décembre-2020 (version de maintenance)

  • fonctionnalité : la boucle de sondage s'exécute désormais indéfiniment, dormant pendant 0,5 seconde entre les exécutions, évitant de créer de nouveaux minuteurs à chaque étape.

1.0.0, 18-juillet-2019

  • CHANGEMENT MAJEUR : les valeurs de retour de poll (et donc aussi de post et post_local) ont changé pour être plus "lua-ish", pour être véridiques lorsque tout va bien.
  • fonctionnalité : nouvelle option shm_retries pour corriger les erreurs "no memory" causées par la fragmentation de la mémoire dans le shm lors de la publication d'événements.
  • correction : correction de deux fautes de frappe dans les noms de variables (cas limites)

0.3.3, 8-mai-2018

  • correction : délais d'attente dans les phases d'initialisation, en supprimant le paramètre de délai d'attente, voir le problème #9

0.3.2, 11-avril-2018

  • changement : ajout d'une trace de pile aux erreurs de gestionnaire
  • correction : échec du gestionnaire d'erreurs si la valeur était non-serialisable, voir le problème #5
  • correction : correction d'un test pour les gestionnaires faibles

Voir Aussi

GitHub

Vous pouvez trouver des conseils de configuration supplémentaires et de la documentation pour ce module dans le dépôt GitHub pour nginx-module-worker-events.