ngx_http_limit_req_moduleのコードリーディング

はじめに

Module ngx_http_limit_req_module を使おうと思ってコードを読んでみたのでメモです。

leaky bucket

上記のドキュメントに “leaky bucket” を使ってリクエスト数の制御を行っていると書かれています。

leaky bucketについては Leaky Bucket Algorithm| Computer Networks - GeeksforGeeks の説明が具体例もあってわかりやすかったです。

nginxの実装

rateとlimitの値を読み取る

設定からrateとlimitを読み取るコードは以下の部分です。内部的には秒間リクエスト数を1000倍して管理しています。

ngx_http_limit_req_module.c#L792-L828

if (ngx_strncmp(value[i].data, "rate=", 5) == 0) {

    len = value[i].len;
    p = value[i].data + len - 3;

    if (ngx_strncmp(p, "r/s", 3) == 0) {
        scale = 1;
        len -= 3;

    } else if (ngx_strncmp(p, "r/m", 3) == 0) {
        scale = 60;
        len -= 3;
    }

    rate = ngx_atoi(value[i].data + 5, len - 5);
    if (rate <= 0) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "invalid rate \"%V\"", &value[i]);
        return NGX_CONF_ERROR;
    }

    continue;
}

ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                   "invalid parameter \"%V\"", &value[i]);
return NGX_CONF_ERROR;
}

if (name.len == 0) {
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                       "\"%V\" must have \"zone\" parameter",
                       &cmd->name);
    return NGX_CONF_ERROR;
}

ctx->rate = rate * 1000 / scale;

ngx_http_limit_req_module.c#L885-L937

if (ngx_strncmp(value[i].data, "burst=", 6) == 0) {

    burst = ngx_atoi(value[i].data + 6, value[i].len - 6);
    if (burst <= 0) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "invalid burst rate \"%V\"", &value[i]);
        return NGX_CONF_ERROR;
    }

    continue;
}

if (ngx_strcmp(value[i].data, "nodelay") == 0) {
    nodelay = 1;
    continue;
}

ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                   "invalid parameter \"%V\"", &value[i]);
return NGX_CONF_ERROR;
}

if (shm_zone == NULL) {
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                       "\"%V\" must have \"zone\" parameter",
                       &cmd->name);
    return NGX_CONF_ERROR;
}

limits = lrcf->limits.elts;

if (limits == NULL) {
    if (ngx_array_init(&lrcf->limits, cf->pool, 1,
                       sizeof(ngx_http_limit_req_limit_t))
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }
}

for (i = 0; i < lrcf->limits.nelts; i++) {
    if (shm_zone == limits[i].shm_zone) {
        return "is duplicate";
    }
}

limit = ngx_array_push(&lrcf->limits);
if (limit == NULL) {
    return NGX_CONF_ERROR;
}

limit->shm_zone = shm_zone;
limit->burst = burst * 1000;

nginxのlimit_reqのleaky bucket

nginxの実装は(おそらく軽く動かすために)ちょっと変わった方法をとっていて、タイマーで定期的に許容できる量を更新していくのではなく、以下のように前回のアクセスから今回のアクセスまでの時間にレートを掛けて引くという方式を取っています。

ngx_http_limit_req_module.c#L400-L412

ms = (ngx_msec_int_t) (now - lr->last);

excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;

if (excess < 0) {
    excess = 0;
}

*ep = excess;

if ((ngx_uint_t) excess > limit->burst) {
    return NGX_BUSY;
}

もし2つのアクセスがほぼ同時にあったとすると now - lr->last の部分がほぼ 0 になります。 すると excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;excess = lr->excess + 1000; とほぼ同じことになります。

その下の if 文で excess がマイナスの場合は0にしています。 lr->excess を設定する箇所はここでは省略しますが、前回の excess の値になっています。 ということでほぼ同時にアクセスがあると excess = lr->excess + 1000; の結果、 excess は 1000より大きな値になります。

すると limit->burst が 0 だと NGX_BUSY を返してリミットに引っかかることになります。 同時にアクセスがあっただけでひっかかるのは困るので burst を良い感じに調整して設定しておく必要があります。

また上記の excess の式でわかるように rate の設定値は秒間リクエスト数と言っても、秒単位で制御しているわけではないので、正確な値というよりおおよその目安として考えておいたほうが良いと思います。