---
title: "NGINXワーカー間のコミュニケーションの魔法：最も重要なデータ構造の1つ `shared dict` - API7.ai"
description: "shared dict 是 NGINX 和 OpenResty 中的一个重要数据结构。"
url: "https://api7.ai/ja/learning-center/openresty/communication-magic-between-nginx-workers-shared-dict"
---

# NGINXワーカー間のコミュニケーションの魔法：最も重要なデータ構造の1つ \`shared dict\`

API7.ai

October 27, 2022

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

前回の記事で述べたように、Luaにおける唯一のデータ構造は`table`です。これはOpenRestyプログラミングで使用できる最も重要なデータ構造である`shared dict`に対応しています。`shared dict`は、データの保存、読み取り、アトミックなカウント、キュー操作をサポートしています。

`shared dict`を基に、複数の`Worker`間でのキャッシュや通信、レート制限、トラフィック統計などの機能を実装できます。`shared dict`を簡易的なRedisとして使用することもできますが、`shared dict`のデータは永続的ではないため、保存されたデータの損失を考慮する必要があります。

## データ共有のいくつかの方法

OpenRestyのLuaコードを書く際には、リクエストの異なるフェーズで異なる`Worker`間でデータを共有する必要がある場合があります。また、LuaとCコードの間でデータを共有する必要がある場合もあります。

そのため、`shared dict`のAPIを正式に紹介する前に、まずOpenRestyで一般的なデータ共有方法を理解し、現在の状況に応じてより適切なデータ共有方法を選択する方法を学びましょう。

**1つ目はNGINXの変数です。** これはNGINXのCモジュール間でデータを共有できます。当然、CモジュールとOpenRestyが提供する`lua-nginx-module`間でもデータを共有できます。以下のコードのように。

```
location /foo {
     set $my_var ''; # この行は設定時に$my_varを作成するために必要
     content_by_lua_block {
         ngx.var.my_var = 123;
         ...
     }
 }
```

ただし、NGINXの変数を使用してデータを共有するのは遅いです。なぜなら、ハッシュ検索とメモリ割り当てが関与するからです。また、この方法は文字列を保存するためにしか使用できず、複雑なLuaの型をサポートしていません。

**2つ目は`ngx.ctx`で、同じリクエストの異なるフェーズ間でデータを共有できます。** これは通常のLuaの`table`なので高速であり、さまざまなLuaオブジェクトを保存できます。そのライフサイクルはリクエストレベルです。リクエストが終了すると、`ngx.ctx`は破棄されます。

以下は典型的な使用シナリオで、`ngx.ctx`を使用してNGINX変数のような高価なキャッシュ呼び出しを行い、さまざまな段階で使用します。

```
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)
     }
 }
```

この場合、`curl`でアクセスすると、

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

`test.com`が出力され、`ngx.ctx`が異なる段階でデータを共有していることがわかります。もちろん、上記の例を変更して、単純な文字列ではなく`table`のようなより複雑なオブジェクトを保存し、期待通りに動作するか確認することもできます。

ただし、ここで特に注意すべき点は、`ngx.ctx`のライフサイクルがリクエストレベルであるため、モジュールレベルでキャッシュされないことです。例えば、`foo.lua`ファイルでこのような間違いを犯しました。

```
local ngx_ctx = ngx.ctx

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

関数レベルで呼び出してキャッシュするべきです。

```
local ngx = ngx

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

`ngx.ctx`にはまだ多くの詳細があり、パフォーマンス最適化のセクションで引き続き探求していきます。

**3つ目のアプローチは、モジュールレベルの変数を使用して、同じ`Worker`内のすべてのリクエスト間でデータを共有します。** 前のNGINX変数や`ngx.ctx`とは異なり、このアプローチは少し理解しにくいです。しかし、心配しないでください。概念は抽象的ですが、コードが先にあるので、例を見てモジュールレベルの変数を理解しましょう。

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

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

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

return _M
```

`nginx.conf`の設定は以下の通りです。

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

この例では、`mydata`は`Worker`プロセスによって一度だけロードされるモジュールであり、その後その`Worker`が処理するすべてのリクエストは`mydata`モジュールのコードとデータを共有します。

当然、`mydata`モジュール内の`data`変数はモジュールレベルの変数であり、モジュールのトップレベル、つまりモジュールの最初に位置し、すべての関数からアクセス可能です。

したがって、リクエスト間で共有する必要があるデータをモジュールのトップレベルの変数に置くことができます。ただし、一般的にこの方法は読み取り専用のデータを保存するためにのみ使用します。書き込み操作が関与する場合、非常に注意が必要です。なぜなら、`競合状態`が発生する可能性があり、これは特定が難しいバグだからです。

以下の最も簡略化された例でこれを体験できます。

```
-- 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
```

モジュール内で、`data`テーブルのデータを変更する`incr_age`関数を追加しました。

次に、呼び出しコードで最も重要な行`ngx.sleep(5)`を追加します。ここで`sleep`は`yield`操作です。

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

この`sleep`コードの行（または他の非ブロッキングIO操作、例えばRedisへのアクセスなど）がない場合、yield操作はなく、競合もなく、最終的な出力は順番通りになります。

しかし、この行のコードを追加すると、たとえ5秒間のスリープ中であっても、別のリクエストが`mydata.incr_age`関数を呼び出し、変数の値を変更する可能性があり、その結果、最終的な出力番号が連続しなくなります。実際のコードではロジックがそれほど単純ではなく、バグの特定がはるかに困難です。

したがって、間にyield操作がないことが確実でない限り、モジュールレベルの変数を読み取り専用に保つことをお勧めします。

4つ目で最後のアプローチは、`shared dict`を使用して、複数のワーカー間で共有できるデータを共有します。

このアプローチは赤黒木の実装に基づいており、パフォーマンスは良好ですが、制限があります。NGINX設定ファイルで共有メモリのサイズを事前に宣言する必要があり、実行時に変更することはできません。

```
lua_shared_dict dogs 10m;
```

`shared dict`も`string`データのみをキャッシュし、複雑なLuaデータ型をサポートしていません。つまり、`table`のような複雑なデータ型を保存する必要がある場合、JSONや他の方法を使用してシリアライズおよびデシリアライズする必要があり、これにより当然多くのパフォーマンスの損失が発生します。

いずれにせよ、ここには銀の弾丸はなく、完璧なデータ共有方法はありません。ニーズとシナリオに応じて複数の方法を組み合わせる必要があります。

## Shared dict

上記のデータ共有部分について多くの時間を費やして学びましたが、一部の方は疑問に思うかもしれません。それらは`shared dict`と直接関係がないように見えますが、それは話題から外れているのではないでしょうか？

実際にはそうではありません。考えてみてください。なぜOpenRestyに`shared dict`があるのでしょうか？ 最初の3つのデータ共有方法はすべてリクエストレベルまたは個々の`Worker`レベルです。したがって、現在のOpenRestyの実装では、`shared dict`のみが`Worker`間でのデータ共有を実現し、`Worker`間の通信を可能にします。これがその存在価値です。

私の意見では、技術が存在する理由を理解し、他の類似技術との違いや利点を把握することは、提供されるAPIを呼び出すことに熟練していることよりもはるかに重要です。この技術的視野は、ある程度の先見の明と洞察力を与え、エンジニアとアーキテクトの重要な違いと言えるでしょう。

`shared dict`に戻ると、20以上のLua APIが公開されており、すべてアトミックであるため、複数の`Worker`と高並列性の場合でも競合を心配する必要はありません。

これらのAPIにはすべて詳細な[公式ドキュメント](https://github.com/openresty/lua-nginx-module#ngxshareddict)があるので、すべてを説明しません。再度強調しますが、技術コースは公式ドキュメントを注意深く読むことに代わるものではありません。誰もこれらの時間のかかる愚かな手順をスキップすることはできません。

次に、`shared dict`のAPIを見ていきましょう。これらは3つのカテゴリに分けられます。dictの読み書き、キュー操作、および管理です。

### Dictの読み書き

まず、dictの読み書きクラスを見てみましょう。元のバージョンでは、dictの読み書きクラスのAPIしかありませんでした。これは共有辞書の最も一般的な機能です。以下は最も簡単な例です。

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

`set`に加えて、OpenRestyは`safe_set`、`add`、`safe_add`、`replace`の4つの書き込み方法を提供しています。ここでの`safe`プレフィックスの意味は、メモリが一杯になった場合、LRUに従って古いデータを削除するのではなく、書き込みが失敗し、`no memory`エラーを返すことです。

`get`に加えて、OpenRestyは`get_stale`メソッドを提供しており、`get`メソッドと比較して、期限切れのデータに対する追加の戻り値があります。

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

また、指定されたキーを削除するために`delete`メソッドを呼び出すこともできます。これは`set(key, nil)`と同等です。

### キュー操作

キュー操作に移ると、これはOpenRestyに後から追加されたもので、Redisと同様のインターフェースを提供します。キュー内の各要素は`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;
```

これらのキューイングAPIの[PR](https://github.com/openresty/lua-nginx-module/pull/586/files)を記事に投稿しました。これに興味がある場合は、ドキュメント、テストケース、およびソースコードを追って具体的な実装を分析できます。

ただし、以下の5つのキューAPIに対応するコード例はドキュメントにありませんので、ここで簡単に紹介します。

- `lpush``/``rpush`はキューの両端に要素を追加します。
- `lpop``/``rpop`はキューの両端から要素を取り出します。
- `llen`はキュー内の要素数を返します。

前回の記事で議論したもう一つの便利なツールを忘れないでください。テストケースです。ドキュメントにない場合、通常はテストケースに対応するコードを見つけることができます。キュー関連のテストはまさにファイル`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]
```

### 管理

最後の管理APIも後から追加されたもので、コミュニティで人気のある要件です。最も典型的な例の一つは、共有メモリの使用状況です。例えば、ユーザーが`shared dict`として`100M`のスペースを要求した場合、この`100M`は十分でしょうか？ その中に保存されているキーの数はいくつで、どのキーが保存されているのでしょうか？ これらはすべて実際の問題です。

この種の問題に対して、OpenResty公式はユーザーがフレームグラフを使用して解決することを望んでいます。つまり、非侵襲的な方法でコードベースを効率的で整頓された状態に保ち、直接結果を返す侵襲的なAPIを提供しないことです。

しかし、ユーザーフレンドリーな観点から見ると、これらの管理APIは依然として必要です。結局のところ、オープンソースプロジェクトは製品要件を解決するために設計されており、技術自体を展示するためではありません。したがって、後から追加される以下の管理APIを見てみましょう。

まず、`get_keys(max_count?)`で、デフォルトでは最初の`1024`キーのみを返します。`max_count`を`0`に設定すると、すべてのキーを返します。次に、`capacity`と`free_space`で、これらはどちらも`lua-resty-core`リポジトリの一部であるため、使用する前に`require`する必要があります。

```
require "resty.core.shdict"

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

これらは共有メモリのサイズ（`lua_shared_dict`で設定されたサイズ）と空きページのバイト数を返します。`shared dict`はページ単位で割り当てられるため、`free_space`が`0`を返しても、割り当てられたページ内にスペースがある可能性があります。したがって、その戻り値は共有メモリがどれだけ占有されているかを表すものではありません。

## まとめ

実際には、マルチレベルキャッシュを使用することが多く、公式のOpenRestyプロジェクトにもキャッシュパッケージがあります。それらのプロジェクトがどれかわかりますか？ または、キャッシュをカプセル化した他の`lua-resty`ライブラリを知っていますか？

この記事を同僚や友人と共有して、コミュニケーションと改善を一緒に行いましょう。

Share article link

Share

Related Posts

[![OpenResty FAQ | OpenRestyの実践的な使い方](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)

## [OpenResty FAQ | OpenRestyの実践的な使い方](https://api7.ai/learning-center/openresty/faq-5)

[![第3部：OpenRestyを使用してマイクロサービスAPIゲートウェイを構築する方法](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)

## [第3部：OpenRestyを使用してマイクロサービスAPIゲートウェイを構築する方法](https://api7.ai/learning-center/openresty/how-to-build-a-microservices-api-gateway-part-3)

Table of Contents

- [データ共有のいくつかの方法](#%E3%83%87%E3%83%BC%E3%82%BF%E5%85%B1%E6%9C%89%E3%81%AE%E3%81%84%E3%81%8F%E3%81%A4%E3%81%8B%E3%81%AE%E6%96%B9%E6%B3%95)

- [Shared dict](#shared-dict)

  - [Dictの読み書き](#dict%E3%81%AE%E8%AA%AD%E3%81%BF%E6%9B%B8%E3%81%8D)
  - [キュー操作](#%E3%82%AD%E3%83%A5%E3%83%BC%E6%93%8D%E4%BD%9C)
  - [管理](#%E7%AE%A1%E7%90%86)

- [まとめ](#%E3%81%BE%E3%81%A8%E3%82%81)
