lua-nginx-moduleのshared dictの空き容量について

はじめに

openresty/lua-nginx-module: Embed the Power of Lua into NGINX HTTP serversngx.shared.DICT は複数ワーカープロセス間でデータを共有することができ、非常に便利です。

使用する際は lua_shared_dict ディレクティブ で以下のように shared dict の名称とサイズを指定する必要があります。

lua_shared_dict dogs 10m;

しかし、このサイズをどれぐらいにしたらよいかわからず、適当に設定していました。

そこでコードを読んで、おおよそのメモリ使用量の目安の計算について調査しました。 また、概略の残り容量を把握するための仕組みを追加するプルリクエストを送ってマージされました。 ということでメモしておきます。

shared dictのメモリ管理

lua-nginx-moduleのshared dictのコードリーディング で shared dict のメモリ割り当てについてコードを読んでみました。

https://github.com/openresty/lua-nginx-module/tree/bf14723e4e7749c989134c029742185db1c78255

要約すると以下のようになっています。

また ngx_http_lua_shdict_init_zone 関数で slab allocator から2つメモリ割り当てを行っています。

1つめは sizeof(ngx_http_lua_shdict_shctx_t) = 80バイトを割り当てるので128バイトのスロットの1エントリを消費します。

lua-nginx-module/ngx_http_lua_shdict.c#L108

ctx->sh = ngx_slab_alloc(ctx->shpool, sizeof(ngx_http_lua_shdict_shctx_t));

2つめは sizeof(" in lua_shared_dict zone \"\"") + shm_zone->shm.name.len バイトの割り当てです。

lua-nginx-module/ngx_http_lua_shdict.c#L120

len = sizeof(" in lua_shared_dict zone \"\"") + shm_zone->shm.name.len;
(gdb) print sizeof(" in lua_shared_dict zone \"\"")
$4 = 28

shared dictの名前が5〜36バイトであれば33〜64バイト用のスロットのエントリを1つ消費します。 名前が4バイト以下なら17〜32バイトのスロットになりますし、37〜100バイトなら65〜128バイトのスロットになります。

名前が 37〜100バイトで128バイトのスロットを使う場合は、上記の ngx_http_lua_shdict_shctx_t の割り当てに使うのと同じページを使うことになります。 が、ほとんどの場合はそこまで長い名前にはしないでしょうから、 ngx_http_lua_shdict_shctx_t で128バイトのスロットに1ページ、 sizeof(" in lua_shared_dict zone \"\"") + shm_zone->shm.name.len で32バイトか64バイトのスロットに1ページが初期状態で割り当てられることになります。

ページ数の見積もり

例えば lua_shared_dict12k と最低容量で宣言していたケースを考えます。

管理領域の合計は sizeof(ngx_slab_pool_t) + 9 * (sizeof(ngx_slab_page_t) + sizeof(ngx_slab_stat_t)) で、以下の計算により 704 バイトです。

>>> 200 + 9 * (24 + 32)
704

この704バイトを除いた領域を4KiB単位のページに分割します。この例ではページ数は以下の計算により 2 です。

>>> (12 * 1024 - 704) // 4096
2

しかし、上記の通り ngx_http_lua_shdict_init_zone で 64バイトと128バイトのスロットを1つずつ使っていますので、それぞれのスロットにページが割り当てられて、空きページ数は0となっています。

ですので、この後これ以外のスロットに対応するメモリ割り当てを行おうとすると空きページが無いのでエラーになります。

エントリ追加時のメモリ消費量

以下のコードの通り、追加しようとするキーの長さ key.len と値の長さ value.lenoffsetof(ngx_rbtree_node_t, color)offsetof(ngx_http_lua_shdict_node_t, data) を加えたサイズのメモリ割り当てを行います。

lua-nginx-module/ngx_http_lua_shdict.c#L1164-L1167

n = offsetof(ngx_rbtree_node_t, color)
    + offsetof(ngx_http_lua_shdict_node_t, data)
    + key.len
    + value.len;

offsetof(ngx_rbtree_node_t, color) は32、 offsetof(ngx_http_lua_shdict_node_t, data) は36だったので、 68 + キーの長さ + 値の長さということになります。

例えば、 12k のshared dictではキーの長さ4バイト、値の長さ57バイトのエントリを追加しようとすると 68 + 4 + 57 = 129バイトで256バイトのスロットにエントリ追加が必要になりますが、空きページはもう無いので

ngx.shared.DICT.set

success, err, forcible = ngx.shared.DICT:set(key, value, exptime?, flags?)

errno memory というエラーが返ってきます。

一方、キーの長さが8バイト、値の長さが8バイトであれば、 68 + 8 + 8 = 84バイトなので128バイトのスロットを1つ消費します。128 - 68 = 60なのでキーと値のサイズ合計が60バイト以下であれば128バイトのスロットというこになります。

128バイトのスロットでは4KiBの1ページあたりのエントリ数は

>>> 4096 // 128
32

です。ただし、128バイトのスロットの最初のページは初期化時に sizeof(ngx_http_lua_shdict_shctx_t) で1エントリ消費されているので、残りのエントリ数は31です。

以下のような設定

lua_shared_dict cats 12k;

server {
    // ...

    location /cats2 {
        content_by_lua_block {
            local cats = ngx.shared.cats;
            for i = 1, 33 do
                local key = string.format('key%05d', i)
                local val = string.format('val%05d', i)
                local success, err, forcible = cats:set(key, val)
                if not success or err ~= nil or forcible then
                    ngx.say(string.format("failed to set to shared.dict, i=%d, success=%s, err=%s, forcible=%s", i, success, err, forcible))
                end
            end
            for i = 1, 3 do
                local key = string.format('key%05d', i)
                local val = cats:get(key)
                ngx.say(string.format("key=%s, val=%s", key, val))
            end
        }
    }
}

で /cats にアクセスしてみると i が 32 以降は forcible が true になります。

$ curl localhost/cats2
failed to set to shared.dict, i=32, success=true, err=nil, forcible=true
failed to set to shared.dict, i=33, success=true, err=nil, forcible=true
key=key00001, val=nil
key=key00002, val=nil
key=key00003, val=val00003

forcible については ngx.shared.DICT.set のドキュメントに

forcible: a boolean value to indicate whether other valid items have been removed forcibly when out of storage in the shared memory zone.

と説明があります。

ソースコードでは以下の部分に対応します。

lua-nginx-module/ngx_http_lua_shdict.c#L2759-L2785

2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
node = ngx_slab_alloc_locked(ctx->shpool, n);

if (node == NULL) {

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, ctx->log, 0,
                   "lua shared dict incr: overriding non-expired items "
                   "due to memory shortage for entry \"%*s\"", key_len,
                   key);

    for (i = 0; i < 30; i++) {
        if (ngx_http_lua_shdict_expire(ctx, 0) == 0) {
            break;
        }

        *forcible = 1;

        node = ngx_slab_alloc_locked(ctx->shpool, n);
        if (node != NULL) {
            goto allocated;
        }
    }

    ngx_shmtx_unlock(&ctx->shpool->mutex);

    *err = "no memory";
    return NGX_ERROR;
}

分岐としては以下のケースになります。

つまり、空きページが無い場合は古いキーを破棄させてスロットに空きを作って新しいキーを設定しています。 上記の例では key00001key00002 のキーが破棄されており値を参照しても nil になってしまいます。

空き容量の確認のためのcapacity, free_spaceメソッド

空き容量を監視するために以下のプルリクエストを送りました。

lua-nginx-module だけではなく lua-resty-core にもプルリクエストを送っているのは、ngx.shared.DICT のメソッドは C API として実装してLuaから呼び出す方式から luajit の FFI Library を利用して呼び出す方式に移行中だったからです。

内容ですが、当初は ngx_slab_stat_ttotal を合計すれば使用量合計が出せるのではないかと思ったのですが、コードを読んで考えた結果、監視項目としては空きページサイズ合計を見るのが良いという結論に至りました。

あるスロットに割り当て済みのページに空きがある場合は、同じスロットの割り当ては成功するのですが、上記の例のように別のスロットのページが埋まっていて空きページも無い場合は no memory のエラーが発生するからです。

最終的には以下のコミットになりました。

追加でドキュメントの記法修正のプルリクエストも送ってマージされています。

Fix strike-through in shdict.free_space markdown doc by hnakamur · Pull Request #1170 · openresty/lua-nginx-module

ngx.shared.DICTcapacity メソッドで lua_shared_dict ディレクティブで設定した容量をバイト数で取得できます。

ngx.shared.DICTfree_space メソッドで slab allocator の空きページの合計バイト数が取得できます。

監視用のロケーションを作って free_space の値を capacity で割って100をかければ空き容量をパーセントで計算できますし、free_space そのものを見れば空き容量のバイト数が得られます。

ただし、上記の通りこれはあくまで目安であって実際には free_space がゼロであっても、キーの追加に成功するケースもあります。ですが余裕を持っておきたいので、悲観的なケースに倒して空き容量を計算しています。

capacity, free_spaceメソッド入りのnginxのrpm, debパッケージ

CentOS 6/7用のrpmパッケージとUbuntu 16.04 用のdebパッケージをビルドしました。

まとめ