地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Nginx】小ネタ、denyを大量に書く際はCIDR 形式でまとめた方がパフォーマンス(レイテンシ、メモリ効率)が上がる

mogile.web.fc2.com

例えば

deny 192.168.1.0;
deny 192.168.1.1;
(snip)
deny 192.168.1.255;
deny 192.168.1.256;

deny 192.168.1.0/24;

だと後者の方がレスポンスタイムが短くなるようです。(メモリ使用量も抑えられる)

実験してみる

二つのconfigでそれぞれ計測してみます

# configにdenyの192.168.1.1/20を設定
$ for i in $(seq 100); do curl localhost -s -o /dev/null -w  "%{time_starttransfer}\n"; done | awk '{sum+=$1} END {print sum}'
0.34399

# configにdenyの192.168.1.1 ~ 192.168.15.255を設定
for i in $(seq 100); do curl localhost -s -o /dev/null -w  "%{time_starttransfer}\n"; done | awk '{sum+=$1} END {print sum}'
39.6531

すごい差が出ました!!こんな例はあんまないと思いますがわかりやすくするためにやってます。。。w

なぜ

実際にdenyをした際に動くソースは以下。リクエストごとにアクセス元IPとdenyで設定し内容が一致するかを見るわけだが探索自体は線形探索を用いて実装されておりconfigで設定したdenyの行数分ループしていく実装となっている。denyが10000行あったら毎回この探索が実施されるという。CIDR形式の指定だとアクセス元IPとconfigに記載したCIDRのビット演算とルールのIPの一回の比較で済むので早いという仕組み。

static ngx_int_t
ngx_http_access_inet(ngx_http_request_t *r, ngx_http_access_loc_conf_t *alcf, in_addr_t addr)
{
    ngx_uint_t               i;
    ngx_http_access_rule_t  *rule;

    rule = alcf->rules->elts;

    // allow, denyのルールの行数ごとに探索を実施する(netltsは配列内に格納された要素の数)
    for (i = 0; i < alcf->rules->nelts; i++) { 
        ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "access: %08XD %08XD %08XD",
                       addr, rule[i].mask, rule[i].addr);

        if ((addr & rule[i].mask) == rule[i].addr) { // 「アクセス元IPとconfigに設定されたCIDRとのビットAND」とconfigに設定されたIPの比較
            return ngx_http_access_found(r, rule[i].deny); // 一致していたらdenyフラグを立てて検出関数を呼び出す
        }
    }

    return NGX_DECLINED; // 一致するものがなければ処理継続を呼び出し元に戻す
}

// アクセスルールの構造体
typedef struct {
    in_addr_t         mask; // configのCIDR
    in_addr_t         addr; // configのアドレス
    ngx_uint_t        deny;      /* unsigned  deny:1; */
} ngx_http_access_rule_t;

// ルール格納用の配列
typedef struct {
    ngx_array_t      *rules;     /* array of ngx_http_access_rule_t */
    ngx_array_t      *rules6;    /* array of ngx_http_access_rule6_t */
    ngx_array_t      *rules_un;  /* array of ngx_http_access_rule_un_t */
} ngx_http_access_loc_conf_t;

実際にNGX_HTTP_FORBIDDENを返すのはこっちの関数

static ngx_int_t
ngx_http_access_found(ngx_http_request_t *r, ngx_uint_t deny)
{
    ngx_http_core_loc_conf_t  *clcf;

    // 今回のケースだとdenyフラグは有効化されて呼び出される
    if (deny) {
        clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

        if (clcf->satisfy == NGX_HTTP_SATISFY_ALL) {
            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                          "access forbidden by rule");
        }

        // クライアントへは403が返る
        return NGX_HTTP_FORBIDDEN;
    }

    return NGX_OK;
}

まとめ

CIDR 形式の方が計算量も少ないので良さそうというのとあとはメモリ効率も違うのでまとめれるならCIDR形式でガットかけると良さそうでした。ちなみに線形探索なのでよくアクセスしてくるIPを上に書くことでループ回数を抑えるテクニックがあることにも気づきました(そこまでやる必要がある機会はよくわからないけど...)