---
title: "La magie de la communication entre les workers NGINX : l'une des structures de données les plus importantes, le `shared dict` - API7.ai"
description: "shared dict est une structure de données importante dans NGINX et OpenResty."
url: "https://api7.ai/fr/learning-center/openresty/communication-magic-between-nginx-workers-shared-dict"
---

# La magie de la communication entre les workers NGINX : l'une des structures de données les plus importantes, le \`shared dict\`

API7.ai

October 27, 2022

[OpenResty (NGINX + Lua)](https://api7.ai/learning-center/openresty)

Comme nous l'avons dit dans l'article précédent, la `table` est la seule structure de données en Lua. Cela correspond au `shared dict`, qui est la structure de données la plus importante que vous pouvez utiliser dans la programmation OpenResty. Il prend en charge le stockage de données, la lecture, le comptage atomique et les opérations de file d'attente.

Basé sur le `shared dict`, vous pouvez implémenter la mise en cache et la communication entre plusieurs `Worker`s, ainsi que la limitation de débit, les statistiques de trafic et d'autres fonctions. Vous pouvez utiliser `shared dict` comme un simple Redis, sauf que les données dans `shared dict` ne sont pas persistantes, donc vous devez considérer la perte des données stockées.

## Plusieurs méthodes de partage de données

En écrivant le code Lua OpenResty, vous rencontrerez inévitablement le partage de données entre différents `Worker`s à différentes phases de la requête. Vous pourriez également avoir besoin de partager des données entre le code Lua et le code C.

Donc, avant de présenter formellement les API de `shared dict`, comprenons d'abord les méthodes courantes de partage de données dans OpenResty et apprenons comment choisir une méthode de partage de données plus appropriée selon la situation actuelle.

**La première est les variables dans NGINX.** Elle peut partager des données entre les modules C de NGINX. Naturellement, elle peut également partager des données entre les modules C et le `lua-nginx-module` fourni par OpenResty, comme dans le code suivant.

```
location /foo {
     set $my_var ''; # cette ligne est nécessaire pour créer $my_var au moment de la configuration
     content_by_lua_block {
         ngx.var.my_var = 123;
         ...
     }
 }
```

Cependant, utiliser les variables NGINX pour partager des données est lent car cela implique des recherches de hachage et des allocations de mémoire. De plus, cette approche a la limitation qu'elle ne peut être utilisée que pour stocker des chaînes de caractères et ne peut pas supporter des types Lua complexes.

**La deuxième est `ngx.ctx`, qui peut partager des données entre différentes phases de la même requête.** C'est une `table` Lua normale, donc elle est rapide et peut stocker divers objets Lua. Son cycle de vie est au niveau de la requête ; lorsque la requête se termine, `ngx.ctx` est détruit.

Voici un scénario d'utilisation typique où nous utilisons `ngx.ctx` pour mettre en cache des appels coûteux comme les variables NGINX et l'utiliser à différentes étapes.

```
location /test {
     rewrite_by_lua_block {
         ngx.ctx.host = ngx.var.host
     }
     access_by_lua_block {
        if (ngx.ctx.host == 'api7.ai') then
            ngx.ctx.host = 'test.com'
        end
     }
     content_by_lua_block {
         ngx.say(ngx.ctx.host)
     }
 }
```

Dans ce cas, si vous utilisez `curl` pour y accéder.

```
curl -i 127.0.0.1:8080/test -H 'host:api7.ai'
```

Il affichera alors `test.com`, montrant que `ngx.ctx` partage des données à différentes étapes. Bien sûr, vous pouvez également modifier l'exemple ci-dessus en sauvegardant des objets plus complexes comme des `table`s au lieu de simples chaînes de caractères pour voir si cela répond à vos attentes.

Cependant, une note spéciale ici est que, comme le cycle de vie de `ngx.ctx` est au niveau de la requête, il ne met pas en cache au niveau du module. Par exemple, j'ai fait l'erreur d'utiliser ceci dans mon fichier `foo.lua`.

```
local ngx_ctx = ngx.ctx

local function bar()
    ngx_ctx.host =  'test.com'
end
```

Nous devrions appeler et mettre en cache au niveau de la fonction.

```
local ngx = ngx

local function bar()
    ngx_ctx.host =  'test.com'
end
```

Il y a beaucoup plus de détails sur `ngx.ctx`, que nous explorerons plus tard dans la section d'optimisation des performances.

**La troisième approche utilise des variables au niveau du module pour partager des données entre toutes les requêtes au sein du même `Worker`.** Contrairement aux variables NGINX et `ngx.ctx` précédentes, cette approche est un peu moins compréhensible. Mais ne vous inquiétez pas, le concept est abstrait, et le code vient en premier, donc regardons un exemple pour comprendre une variable au niveau du module.

```
-- mydata.lua
local _M = {}

local data = {
    dog = 3,
    cat = 4,
    pig = 5,
}

function _M.get_age(name)
    return data[name]
end

return _M
```

La configuration dans `nginx.conf` est la suivante.

```
location /lua {
     content_by_lua_block {
         local mydata = require "mydata"
         ngx.say(mydata.get_age("dog"))
     }
 }
```

Dans cet exemple, `mydata` est un module qui n'est chargé qu'une seule fois par le processus `Worker`, et toutes les requêtes traitées par le `Worker` après cela partagent le code et les données du module `mydata`.

Naturellement, la variable `data` dans le module `mydata` est une variable au niveau du module située au sommet du module, c'est-à-dire au début du module, et est accessible à toutes les fonctions.

Donc, vous pouvez mettre les données qui doivent être partagées entre les requêtes dans la variable de niveau supérieur du module. Cependant, il est essentiel de noter que nous n'utilisons généralement cette méthode que pour stocker des données en lecture seule. Si des opérations d'écriture sont impliquées, vous devez être très prudent car il pourrait y avoir une `condition de concurrence`, qui est un bug difficile à localiser.

Nous pouvons expérimenter cela avec l'exemple le plus simplifié suivant.

```
-- mydata.lua
local _M = {}

local data = {
    dog = 3,
    cat = 4,
    pig = 5,
}

function _M.incr_age(name)
    data[name]  = data[name] + 1
    return data[name]
end

return _M
```

Dans le module, nous ajoutons la fonction `incr_age`, qui modifie les données dans la table `data`.

Ensuite, dans le code d'appel, nous ajoutons la ligne la plus critique `ngx.sleep(5)`, où `sleep` est une opération `yield`.

```
location /lua {
     content_by_lua_block {
         local mydata = require "mydata"
         ngx.say(mydata. incr_age("dog"))
         ngx.sleep(5) -- API yield
         ngx.say(mydata. incr_age("dog"))
     }
 }
```

Sans cette ligne de code `sleep` (ou d'autres opérations d'IO non bloquantes, comme l'accès à Redis, etc.), il n'y aurait pas d'opération de yield, pas de concurrence, et la sortie finale serait séquentielle.

Mais lorsque nous ajoutons cette ligne de code, même si ce n'est que pendant 5 secondes de sommeil, une autre requête appellera probablement la fonction `mydata.incr_age` et modifiera la valeur de la variable, ce qui entraînera des nombres de sortie finaux discontinus. La logique n'est pas si simple dans le code réel, et le bug est beaucoup plus difficile à localiser.

Donc, à moins que vous ne soyez sûr qu'il n'y a pas d'opération `yield` entre les deux qui donnera le contrôle à la boucle d'événements NGINX, je recommande de garder vos variables au niveau du module en lecture seule.

La quatrième et dernière approche utilise `shared dict` pour partager des données qui peuvent être partagées entre plusieurs workers.

Cette approche est basée sur une implémentation d'arbre rouge-noir, qui fonctionne bien. Mais elle a ses limites : vous devez déclarer la taille de la mémoire partagée dans le fichier de configuration NGINX à l'avance, et cela ne peut pas être modifié au moment de l'exécution :

```
lua_shared_dict dogs 10m;
```

Le `shared dict` ne met également en cache que les données de type `string` et ne supporte pas les types de données Lua complexes. Cela signifie que lorsque je dois stocker des types de données complexes comme des `table`s, je devrai utiliser JSON ou d'autres méthodes pour les sérialiser et les désérialiser, ce qui entraînera naturellement une perte de performance importante.

Quoi qu'il en soit, il n'y a pas de solution miracle ici, et il n'y a pas de méthode parfaite pour partager des données. Vous devez combiner plusieurs méthodes selon vos besoins et scénarios.

## Shared dict

Nous avons passé beaucoup de temps à apprendre la partie sur le partage de données ci-dessus, et certains d'entre vous pourraient se demander : il semble qu'ils ne soient pas directement liés au shared dict. N'est-ce pas hors sujet ?

En fait, non. Réfléchissez-y : pourquoi y a-t-il un `shared dict` dans OpenResty ? Rappelez-vous que les trois premières méthodes de partage de données sont toutes au niveau de la requête ou au niveau du `Worker` individuel. Par conséquent, dans l'implémentation actuelle d'OpenResty, seul `shared dict` peut accomplir le partage de données entre `Worker`s, permettant la communication entre `Worker`s, ce qui est la raison de son existence.

À mon avis, comprendre pourquoi une technologie existe et comprendre ses différences et avantages par rapport à d'autres technologies similaires est bien plus important que de simplement être compétent dans l'appel des API qu'elle fournit. Cette vision technique vous donne un degré de prévoyance et de perspicacité et est sans doute une différence importante entre les ingénieurs et les architectes.

Revenons au shared dict, qui fournit plus de 20 API Lua au public, toutes atomiques, donc vous n'avez pas à vous soucier de la concurrence dans le cas de plusieurs `Workers` et de haute concurrence.

Ces API ont toutes des [documents officiels](https://github.com/openresty/lua-nginx-module#ngxshareddict) détaillés, donc je ne vais pas toutes les aborder. Je veux souligner à nouveau qu'aucun cours technique ne peut remplacer une lecture attentive de la documentation officielle. Personne ne peut sauter ces procédures chronophages et stupides.

Ensuite, continuons à regarder les API de `shared dict`, qui peuvent être divisées en trois catégories : lecture/écriture de dict, opération de file d'attente et gestion.

### Lecture/écriture de dict

Regardons d'abord les classes de lecture et d'écriture de dict. Dans la version originale, il n'y avait que des API pour les classes de lecture et d'écriture de dict, les fonctionnalités les plus courantes des dictionnaires partagés. Voici l'exemple le plus simple.

```
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
                               dict:set("Tom", 56)
                               print(dict:get("Tom"))'
```

En plus de `set`, OpenResty fournit également quatre méthodes d'écriture : `safe_set`, `add`, `safe_add` et `replace`. La signification du préfixe `safe` ici est que si la mémoire est pleine, au lieu d'éliminer les anciennes données selon LRU, l'écriture échouera et retournera une erreur `no memory`.

En plus de `get`, OpenResty fournit également la méthode `get_stale` pour lire les données, qui a une valeur de retour supplémentaire pour les données expirées par rapport à la méthode `get`.

```
value, flags, stale = ngx.shared.DICT:get_stale(key)
```

Vous pouvez également appeler la méthode `delete` pour supprimer la clé spécifiée, ce qui équivaut à `set(key, nil)`.

### Opération de file d'attente

Passons aux opérations de file d'attente, c'est un ajout ultérieur à OpenResty qui fournit une interface similaire à Redis. Chaque élément dans une file d'attente est décrit par `ngx_http_lua_shdict_list_node_t`.

```
typedef struct {
    ngx_queue_t queue;
    uint32_t value_len;
    uint8_t value_type;
    u_char data[1];
} ngx_http_lua_shdict_list_node_t;
```

J'ai posté le [PR](https://github.com/openresty/lua-nginx-module/pull/586/files) de ces API de file d'attente dans l'article. Si cela vous intéresse, vous pouvez suivre la documentation, les cas de test et le code source pour analyser l'implémentation spécifique.

Cependant, il n'y a pas d'exemples de code correspondants pour les cinq API de file d'attente suivantes dans la documentation, donc je vais les présenter brièvement ici.

- `lpush``/``rpush` signifie ajouter des éléments aux deux extrémités de la file d'attente.
- `lpop``/``rpop`, qui retire des éléments aux deux extrémités de la file d'attente.
- `llen`, qui indique le nombre d'éléments retournés à la file d'attente.

N'oublions pas un autre outil utile dont nous avons parlé dans le dernier article : les cas de test. Nous pouvons généralement trouver le code correspondant dans un cas de test s'il n'est pas dans la documentation. Les tests liés à la file d'attente sont précisément dans le fichier `145-shdict-list.t`.

```
=== TEST 1: lpush & lpop
--- http_config
    lua_shared_dict dogs 1m;
--- config
    location = /test {
        content_by_lua_block {
            local dogs = ngx.shared.dogs

            local len, err = dogs:lpush("foo", "bar")
            if len then
                ngx.say("push success")
            else
                ngx.say("push err: ", err)
            end

            local val, err = dogs:llen("foo")
            ngx.say(val, " ", err)

            local val, err = dogs:lpop("foo")
            ngx.say(val, " ", err)

            local val, err = dogs:llen("foo")
            ngx.say(val, " ", err)

            local val, err = dogs:lpop("foo")
            ngx.say(val, " ", err)
        }
    }
--- request
GET /test
--- response_body
push success
1 nil
bar nil
0 nil
nil nil
--- no_error_log
[error]
```

### Gestion

L'API de gestion finale est également un ajout ultérieur et est une demande populaire dans la communauté. L'un des exemples les plus typiques est l'utilisation de la mémoire partagée. Par exemple, si un utilisateur demande `100M` d'espace comme `shared dict`, est-ce que ce `100M` est suffisant ? Combien de clés sont stockées dedans, et quelles sont ces clés ? Ce sont toutes des questions authentiques.

Pour ce genre de problème, l'équipe officielle d'OpenResty espère que les utilisateurs utilisent les graphiques de flamme pour les résoudre, c'est-à-dire de manière non invasive, en gardant la base de code efficace et propre, au lieu de fournir une API invasive pour retourner les résultats directement.

Mais d'un point de vue convivial pour l'utilisateur, ces API de gestion sont toujours essentielles. Après tout, les projets open source sont conçus pour répondre aux besoins des produits, pas pour montrer la technologie elle-même. Alors, regardons les API de gestion suivantes qui seront ajoutées plus tard.

La première est `get_keys(max_count?)`, qui par défaut ne retourne que les premières `1024` clés ; si vous définissez `max_count` à `0`, elle retournera toutes les clés. Ensuite viennent `capacity` et `free_space`, qui font tous deux partie du dépôt `lua-resty-core`, donc vous devez les `require` avant de les utiliser.

```
require "resty.core.shdict"

local cats = ngx.shared.cats
local capacity_bytes = cats:capacity()
local free_page_bytes = cats:free_space()
```

Ils retournent la taille de la mémoire partagée (la taille configurée dans `lua_shared_dict`) et le nombre d'octets de pages libres. Comme le `shared dict` est alloué par page, même si `free_space` retourne `0`, il peut y avoir de l'espace dans les pages allouées. Par conséquent, sa valeur de retour ne représente pas combien de mémoire partagée est occupée.

## Résumé

En pratique, nous utilisons souvent la mise en cache à plusieurs niveaux, et le projet officiel OpenResty a également un package de mise en cache. Pouvez-vous trouver quels projets ils sont ? Ou connaissez-vous d'autres bibliothèques `lua-resty` qui encapsulent la mise en cache ?

Vous êtes invités à partager cet article avec vos collègues et amis afin que nous puissions communiquer et nous améliorer ensemble.

Share article link

Share

Related Posts

[![FAQ OpenResty | Comment OpenResty est utilisé en pratique](https://static.api7.ai/uploads/2023/02/12/jbv9M0zi_Hello%20World%20in%20OpenResty%20%283%29.png)](https://api7.ai/learning-center/openresty/faq-5)

## [FAQ OpenResty | Comment OpenResty est utilisé en pratique](https://api7.ai/learning-center/openresty/faq-5)

[![Partie 3 : Comment construire une passerelle API pour microservices avec OpenResty](https://static.api7.ai/uploads/2023/02/07/3XJXY9Jy_45.png)](https://api7.ai/learning-center/openresty/how-to-build-a-microservices-api-gateway-part-3)

## [Partie 3 : Comment construire une passerelle API pour microservices avec OpenResty](https://api7.ai/learning-center/openresty/how-to-build-a-microservices-api-gateway-part-3)

Table of Contents

- [Plusieurs méthodes de partage de données](#plusieurs-m%C3%A9thodes-de-partage-de-donn%C3%A9es)

- [Shared dict](#shared-dict)

  - [Lecture/écriture de dict](#lecture%C3%A9criture-de-dict)
  - [Opération de file d'attente](#op%C3%A9ration-de-file-dattente)
  - [Gestion](#gestion)

- [Résumé](#r%C3%A9sum%C3%A9)
