ngx_http_limit_req_module 源碼分析

ngx_http_limit_req_module 是 Nginx 官方提供的一個 http 模塊,它工做在 NGX_HTTP_PREACCESS_PHASE 階段,經過在 nginx.conf 中進行簡單地配置,咱們能夠輕易地對請求速率進行限制。html

配置指令

官方文檔地址node

Example Configuration

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

...

server {

    ...

    location /search/ {
        limit_req zone=one burst=5;
    }


limit_req

Syntax: limit_req zone=name [burst=number] [nodelay];

Default: —

Context: http, server, locationnginx

該指令爲名爲 name 的共享內存設置一個突發請求限制大小(burst)和一個 nodelay 標誌位git


limit_req_log_level

Syntax: limit_req_log_level info | notice | warn | error;

Default:

limit_req_log_level error;

Context: http, server, locationgithub

limit_req_log_level 這條指令用來設置當觸發請求限制時,記錄日誌的級別,默認是 error算法


limit_req_status

Syntax: limit_req_status code;

Default:

limit_req_status 503;

Context: http, server, location數組

limit_req_status 用來設置服務器因請求限制設置而拒絕一個請求時,返回的狀態碼,默認是 503服務器


limit_req_zone

Syntax: limit_req_zone key zone=name:size rate=rate;

Default: —

Context: http數據結構

該指令用來分配一塊名爲 name,大小爲 size 的共享內存,
這塊共享內存服務於一個特定的 key,限制了請求頻率不得超過 rate,注意該指令只能配置在 http{} 塊下函數


設計思想

爲了可以讓單機上全部的 worker 進程共享一份最新的關於請求限制的數據,Nginx 把這些「域」的數據都放在共享內存中。

這個模塊很靈活得容許設置多個不一樣的「域」(我指的「域」是指同一類的 key,例如 $binary_remote_addr$uri,每塊共享內存用來存對應「域」的信息),每當一個請求來臨時,在 preaccess 階段進行檢查,經過遍歷每一個「域」,在每一個「域」的紅黑樹上找對應 key 實例化之後 對應的節點,而後根據漏桶算法,計算同一個 key 上次訪問的時間到如今,通過這段時間的處理,還應該剩下的請求數目,而後再容許有多個突發請求(這裏就是令牌桶的思想了),根據是否超過突發請求限制,決定是吐對應的禁止狀態碼仍是延遲處理這個請求。

另一點爲了避免讓每一個「域」的紅黑樹急劇膨脹而致使這個「域」的內存耗盡,這個模塊還設置了一個 LRU 隊列(固然也是每一個「域」一個隊列),將紅黑樹上的節點按最近最久未使用排成一個隊列,而後每當要新建節點的時候,都去嘗試淘汰一些節點(爲了避免長時間處於淘汰的循環中,最多淘汰 3 個節點)。
若是當前須要延遲處理,Nginx 又會把請求放到定時器中,等到定時器過時之後,執行寫事件回調 ngx_http_limit_req_delay,這個函數裏會執行 ngx_http_core_run_phases,從新進行 HTTP 的 11 個階段。

數據結構定義

ngx_http_limit_req_shctx_t

typedef struct {
    ngx_rbtree_t                  rbtree; /* red-black tree */
    ngx_rbtree_node_t             sentinel; /* the sentinel node of red-black tree */
    ngx_queue_t                   queue; /* used to expire info(LRU algorithm) */
} ngx_http_limit_req_shctx_t;

這個數據結構會被放在共享內存中,記錄了一顆紅黑樹和一個隊列。

  • 紅黑樹用來記錄每一個 「域」(即根據 limit_req_zone 裏定義的 key 獲得)目前的情況

  • 隊列將紅黑樹節點按更新時間從早到晚串起來,用來淘汰過於陳舊的節點(LRU)

該數據結構能夠由下面的 ngx_http_limit_req_ctx_t 裏的 sh 成員索引獲得。

ngx_http_limit_req_node_t

typedef struct {
    u_char                       color;
    u_char                       dummy;
    u_short                      len;
    ngx_queue_t                  queue;
    ngx_msec_t                   last;
    /* integer value, 1 corresponds to 0.001 r/s */
    ngx_uint_t                   excess;
    ngx_uint_t                   count; /* 標記使用的 */
    u_char                       data[1];
} ngx_http_limit_req_node_t; /* rbtree node */

表示紅黑樹節點信息的數據結構,這個數據結構的設計十分巧妙,因爲紅黑樹上用的節點結構只能是 ngx_rbtree_node_t,因此和它實際掛到紅黑樹上的節點的數據結構是連續存放的,且共用了成員 colordummy(也就是 ngx_rbtree_node_tdata 成員),另外,因爲 key 是變成的,這裏它只存了這個 key 的第一個字符(data),其餘的字符緊跟在這個數據結構後面,也是連續的,因此在要新建一個節點的時候,計算須要的內存大小應該是

size = offsetof(ngx_rbtree_node_t, color)
           + offsetof(ngx_http_limit_req_node_t, data)
           + key->len;

計算出 colorngx_rbtree_node_t 裏的偏移(這樣等於算出了 ngx_rbtree_node_tcolor 之前的成員佔用內存大小);再計算出 datangx_http_limit_req_node_t 的偏移,最後加上 key 的長度,這就是整個節點信息結構須要的內存的大小。

另外 len 是變長 key 的長度;queue 成員是用來標識這個節點在 LRU 隊列裏的位置的,記錄了上個節點和下一個節點;last 是上次更新時間;excess 表示上次處理完後剩下來的請求數 * 1000(leaky bucket algorithm)

ngx_http_limit_req_ctx_t

typedef struct {
    ngx_http_limit_req_shctx_t  *sh;
    ngx_slab_pool_t             *shpool; /* slab shared memory pool */
    /* integer value, 1 corresponds to 0.001 r/s */
    ngx_uint_t                   rate; /* about limit_req_zone */
    ngx_http_complex_value_t     key; /* about limit_req_zone */
    ngx_http_limit_req_node_t   *node; /* point to one node in red-black tree */
} ngx_http_limit_req_ctx_t;

該結構體存放根據 limit_req_zone 指令建立的共享內存的相關上下文信息。其中

  • ratekey 均是根據指令 limit_req_zone 而解析獲得

  • node 成員指向了一個節點

ngx_http_limit_req_limit_t

typedef struct {
    ngx_shm_zone_t              *shm_zone;
    /* integer value, 1 corresponds to 0.001 r/s */
    ngx_uint_t                   burst;
    ngx_uint_t                   nodelay; /* unsigned  nodelay:1 */
} ngx_http_limit_req_limit_t;

存放了 limit_req 指令的相關配置信息,例如 burst 表示一個 「域」(key)最多容許的突發請求數,nodelay 表示是否要延遲處理那些超出請求速率的請求。

這個結構體能夠經過 ngx_http_limit_req_conf_t limits 成員索引獲得。而且shm_zonedata 指向了 ngx_http_limit_req_ctx_t 結構體。

ngx_http_limit_req_conf_t

typedef struct {
    ngx_array_t                  limits;
    ngx_uint_t                   limit_log_level;
    ngx_uint_t                   delay_log_level;
    ngx_uint_t                   status_code;
} ngx_http_limit_req_conf_t;

這個結構體存放配置項信息,值得提一下的是 limits 成員,這個動態數組把全部建立的共享內存信息給存放起來了,每一個成員都指向一個 ngx_http_limit_req_limit_t 結構。

函數分析

ngx_http_limit_req_zone

功能:對指令 limit_req_zone 指令進行解析,建立對應的共享內存,設置 rate, key 等參數到 ngx_http_limit_req_ctx_t 結構體變量中

static char *
ngx_http_limit_req_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ......
    
    /* 只要解析到一條 limit_req_zone 指令,就會建立一個 ctx */
    ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_limit_req_ctx_t));
    if (ctx == NULL) {
        return NGX_CONF_ERROR;
    }
    
    ......

    size = 0;
    rate = 1;
    scale = 1; /* 單位轉換時用 */
    name.len = 0;

    for (i = 2; i < cf->args->nelts; i++) {

        if (ngx_strncmp(value[i].data, "zone=", 5) == 0) {
            /* 
             * 這裏主要是在解析 zone 的 name 和 size 
             * 代碼比較簡單,能夠自行閱讀
             */
             ......
        }

        if (ngx_strncmp(value[i].data, "rate=", 5) == 0) {
            /*
             * 這裏主要是解析 rate,包括解析單位 r/s 和 r/m
             * 計算對應的 scale
             */
             ......
        }

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

    ......
    
    /* 實際使用的 rate 會被放大 1000 倍 */
    ctx->rate = rate * 1000 / scale;
    
    /* 建立一塊共享內存 name size tag */
    shm_zone = ngx_shared_memory_add(cf, &name, size,
                                     &ngx_http_limit_req_module);
    if (shm_zone == NULL) {
        return NGX_CONF_ERROR;
    }

    if (shm_zone->data) {
        ctx = shm_zone->data;

        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "%V \"%V\" is already bound to key \"%V\"",
                           &cmd->name, &name, &ctx->key.value);
        return NGX_CONF_ERROR;
    }

    /* 設置好自定義的初始化方法,設置好 ctx 的索引 */
    shm_zone->init = ngx_http_limit_req_init_zone;
    shm_zone->data = ctx;

    return NGX_CONF_OK;
}

ngx_http_limit_req_init_zone

功能:該函數負責初始化放在共享內存中的上下文信息,包括紅黑樹的初始化,隊列初始化,因此每一個「域」

static ngx_int_t
ngx_http_limit_req_init_zone(ngx_shm_zone_t *shm_zone, void *data)
{
    ngx_http_limit_req_ctx_t  *octx = data;

    size_t                     len;
    ngx_http_limit_req_ctx_t  *ctx;

    ctx = shm_zone->data;

    if (octx) {
        /* 
         * 這個過程發生在 reload 的時候
         * 若是對應共享內存的 key 沒變,直接複用就好了
         */
        if (ctx->key.value.len != octx->key.value.len
            || ngx_strncmp(ctx->key.value.data, octx->key.value.data,
                           ctx->key.value.len)
               != 0)
        {
            ngx_log_error(NGX_LOG_EMERG, shm_zone->shm.log, 0,
                          "limit_req \"%V\" uses the \"%V\" key "
                          "while previously it used the \"%V\" key",
                          &shm_zone->shm.name, &ctx->key.value,
                          &octx->key.value);
            return NGX_ERROR;
        }
        
        ctx->sh = octx->sh;
        ctx->shpool = octx->shpool;

        return NGX_OK;
    }

    ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;

    if (shm_zone->shm.exists) {
        ctx->sh = ctx->shpool->data;

        return NGX_OK;
    }

    /* 從 slab 池申請一塊存放 ngx_http_limit_req_shctx_t 的內存 */
    ctx->sh = ngx_slab_alloc(ctx->shpool, sizeof(ngx_http_limit_req_shctx_t));
    if (ctx->sh == NULL) {
        return NGX_ERROR;
    }

    ctx->shpool->data = ctx->sh;

    /* 初始化這個「域」的紅黑樹和 LRU 隊列 */
    ngx_rbtree_init(&ctx->sh->rbtree, &ctx->sh->sentinel,
                    ngx_http_limit_req_rbtree_insert_value);

    ngx_queue_init(&ctx->sh->queue);
    
    ......

    return NGX_OK;
}

ngx_http_limit_req

功能:對指令 limit_req 指令進行解析,判斷出設置的共享內存名字,將其掛到 ngx_http_limit_req_limit_tlimits 數組

static char *
ngx_http_limit_req(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ......

    value = cf->args->elts;

    shm_zone = NULL;
    burst = 0;
    nodelay = 0;

    for (i = 1; i < cf->args->nelts; i++) {

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

            s.len = value[i].len - 5;
            s.data = value[i].data + 5;
            
            /* 
             * 若是這條 limit_req 指令在對應聲明共享內存的 limit_req_zone 指令
             * 以前的話,這裏也會先建立好這個 shm_zone, 下次執行到相應的
             * limit_req_zone 指令,只是把 size 改變了下
             * 反之若是 limit_req_zone 先執行,此次操做就是從 cycle->shared_memory
             * 上面把對應的 shm_zone 拿下來而已
             */
            shm_zone = ngx_shared_memory_add(cf, &s, 0,
                                             &ngx_http_limit_req_module);
            if (shm_zone == NULL) {
                return NGX_CONF_ERROR;
            }

            continue;
        }

        if (ngx_strncmp(value[i].data, "burst=", 6) == 0) {
            
            /* 解析 burst,這個「域」容許的最大突發請求數 */
            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;
    }

    ......

    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;
        }
    }
    
    /* 假如 limit_req 重複指定一塊相同的共享內存(由 limit_req_zone 指令指定),則會返回錯誤 */
    for (i = 0; i < lrcf->limits.nelts; i++) {
        if (shm_zone == limits[i].shm_zone) {
            return "is duplicate";
        }
    }
    
    /* 將這個「域」的 ngx_http_limit_req_limit_t 結構體設置好,放到 limits 數組 */
    limit = ngx_array_push(&lrcf->limits);
    if (limit == NULL) {
        return NGX_CONF_ERROR;
    }
    
    /* 
     * 到時候會把 shm_zone->data 指向 ngx_http_limit_req_ctx_t
     * 這樣就和 ngx_http_limit_req_ctx_t 聯繫起來了
     */
    limit->shm_zone = shm_zone;
    limit->burst = burst * 1000; /* burst 也放大 1000 倍 */
    limit->nodelay = nodelay;

    return NGX_CONF_OK;
}

ngx_http_limit_req_create_conf

功能:建立 ngx_http_limit_req_conf_t 結構體,代碼比較簡單。

ngx_http_limit_req_merge_conf

功能:合併配置項,代碼很簡單。

ngx_http_limit_req_init

功能:設置鉤子函數 ngx_http_limit_req_handlerngx_http_core_main_conf 的 phases 數組裏(NGX_HTTP_PREACCESS_PHASE),代碼很簡單。

ngx_http_limit_req_handler

功能:遍歷設置好的共享內存,調用 ngx_http_limit_req_lookup 來判斷是否須要進行禁用或者延遲,若是禁用,則返回設置的對應狀態碼;若是須要延遲,則將這條鏈接上的寫事件處理方法設置爲 ngx_http_limit_req_delay,並放入定時器中,過時時間經過 ngx_http_limit_req_account 計算出來

static ngx_int_t
ngx_http_limit_req_handler(ngx_http_request_t *r)
{
    uint32_t                     hash;
    ngx_str_t                    key;
    ngx_int_t                    rc;
    ngx_uint_t                   n, excess;
    ngx_msec_t                   delay;
    ngx_http_limit_req_ctx_t    *ctx;
    ngx_http_limit_req_conf_t   *lrcf;
    ngx_http_limit_req_limit_t  *limit, *limits;

    if (r->main->limit_req_set) {
        /* 若是這個請求的主請求已經進行了該階段的檢查
         * 直接返回 NGX_DCLIEND,讓下一個 HTTP 模塊介入請求
         */
        return NGX_DECLINED;
    }

    lrcf = ngx_http_get_module_loc_conf(r, ngx_http_limit_req_module);
    limits = lrcf->limits.elts;

    excess = 0;

    rc = NGX_DECLINED;

#if (NGX_SUPPRESS_WARN)
    limit = NULL;
#endif
    
    /* 遍歷設置好的「域」 */
    for (n = 0; n < lrcf->limits.nelts; n++) {

        limit = &limits[n];

        ctx = limit->shm_zone->data;

        if (ngx_http_complex_value(r, &ctx->key, &key) != NGX_OK) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        if (key.len == 0) {
            continue;
        }

        if (key.len > 65535) {
            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                          "the value of the \"%V\" key "
                          "is more than 65535 bytes: \"%V\"",
                          &ctx->key.value, &key);
            continue;
        }
    
        /* 計算 hash */
        hash = ngx_crc32_short(key.data, key.len);

        ngx_shmtx_lock(&ctx->shpool->mutex);
        
        /* 在這個"域" 的紅黑樹上找這個 key 對應的節點 */
        rc = ngx_http_limit_req_lookup(limit, hash, &key, &excess,
                                       (n == lrcf->limits.nelts - 1));

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

        ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "limit_req[%ui]: %i %ui.%03ui",
                       n, rc, excess / 1000, excess % 1000);

        if (rc != NGX_AGAIN) {
            /* 只要 ngx_http_limit_req_lookup 返回的不是 NGX_AGAIN,就 break */
            break;
        }
    }

    if (rc == NGX_DECLINED) {
        return NGX_DECLINED;
    }

    r->main->limit_req_set = 1;

    if (rc == NGX_BUSY || rc == NGX_ERROR) {

        if (rc == NGX_BUSY) {
            ngx_log_error(lrcf->limit_log_level, r->connection->log, 0,
                          "limiting requests, excess: %ui.%03ui by zone \"%V\"",
                          excess / 1000, excess % 1000,
                          &limit->shm_zone->shm.name);
        }

        while (n--) {
            /* 經歷過的 n 個「域」,取出 node,將 count-- */
            ctx = limits[n].shm_zone->data;

            if (ctx->node == NULL) {
                continue;
            }

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

            ctx->node->count--;

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

            ctx->node = NULL;
        }

        return lrcf->status_code;
    }

    /* rc == NGX_AGAIN || rc == NGX_OK */

    if (rc == NGX_AGAIN) {
        excess = 0;
    }
    
    /* 計算好延遲時間 */
    delay = ngx_http_limit_req_account(limits, n, &excess, &limit);

    if (!delay) {
        return NGX_DECLINED;
    }

    ngx_log_error(lrcf->delay_log_level, r->connection->log, 0,
                  "delaying request, excess: %ui.%03ui, by zone \"%V\"",
                  excess / 1000, excess % 1000, &limit->shm_zone->shm.name);

    if (ngx_handle_read_event(r->connection->read, 0) != NGX_OK) {
        /* 
         * 這裏處理下這條鏈接的讀事件,是爲了若是在這段延遲的時間內,客戶端
         * 主動關閉了鏈接,Nginx 也能夠經過事件調度器感知到,從而及時斷開鏈接
         */
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    r->read_event_handler = ngx_http_test_reading;
    r->write_event_handler = ngx_http_limit_req_delay;
    /* 添加到定時器紅黑樹上,等到過時時調用 ngx_http_limit_req_delay */
    ngx_add_timer(r->connection->write, delay);

    /* 
     * 這裏返回 NGX_AGAIN,讓這個模塊有機會再介入這個請求,
     * 其實也很好理解,畢竟 delay 以後,不能保證那個時刻這個請求涉及到的「域」
     * 就必定沒有超過該「域」 的請求設置限制了,因此還須要再次計算
     */
    return NGX_AGAIN;
}

ngx_http_limit_req_lookup

功能:這個函數是核心,在某個「域」的紅黑樹上找到對應 hash 值的節點,根據漏桶算法,以固定速率處理請求,但又不只僅是漏桶算法,這裏還包含了令牌桶算法的突發門限,具體表如今只要不超過突發門限值,就不會返回 NGX_BUSY,這樣就能夠處理必定量的突發請求了。

返回值意義:

- NGX_BUSY 超過了突發門限
- NGX_OK 沒有超過限制的請求頻率
- NGX_AGAIN 超過限制的請求頻率,可是沒有到達突發門限
static ngx_int_t
ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit, ngx_uint_t hash,
    ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account)
{
    size_t                      size;
    ngx_int_t                   rc, excess;
    ngx_msec_t                  now;
    ngx_msec_int_t              ms;
    ngx_rbtree_node_t          *node, *sentinel;
    ngx_http_limit_req_ctx_t   *ctx;
    ngx_http_limit_req_node_t  *lr;

    now = ngx_current_msec;

    ctx = limit->shm_zone->data;

    node = ctx->sh->rbtree.root;
    sentinel = ctx->sh->rbtree.sentinel;

    while (node != sentinel) {

        if (hash < node->key) {
            node = node->left;
            continue;
        }1

        if (hash > node->key) {
            node = node->right;
            continue;
        }

        /* hash == node->key */

        lr = (ngx_http_limit_req_node_t *) &node->color;

        rc = ngx_memn2cmp(key->data, lr->data, key->len, (size_t) lr->len);
        
        /* hash 值相同,且 key 相同,纔算是找到 */
        if (rc == 0) {
            /* 這個節點最近才訪問,放到隊列首部,最不容易被淘汰(LRU 思想)*/
            ngx_queue_remove(&lr->queue);
            ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);

            /*
             * 漏桶算法:以固定速率接受請求,每秒接受 rate 個請求,
             * ms 是距離上次處理這個 key 到如今的時間,單位 ms
             * lr->excess 是上次還遺留着被延遲的請求數(*1000)
             * excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000;
             * 本次還會遺留的請求數就是上次遺留的減去這段時間能夠處理掉的加上這個請求自己(以前 burst 和 rate 都放大了 1000 倍)
             */
            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;
            }

            if (account) {
                /* 這個請求到了最後一個「域」的限制
                 * 更新上次遺留請求數和上次訪問時間
                 * 返回 NGX_OK 表示沒有達到請求限制的頻率
                 */
                lr->excess = excess;
                lr->last = now;
                return NGX_OK;
            }
            
            /* 
             * count++;
             * 把這個「域」的 ctx->node 指針指向這個節點
             * 這個在 ngx_http_limit_req_handler 裏用到
             */
            lr->count++;
            
            /* 這一步是爲了在 ngx_http_limit_req_account 裏更新這些訪問過的節點的信息 */
            ctx->node = lr;
            
            /* 返回 NGX_AGAIN,會進行下一個「域」的檢查 */
            return NGX_AGAIN;
        }

        node = (rc < 0) ? node->left : node->right;
    }
    
    /* 沒有在紅黑樹上找到節點 */
    *ep = 0;

    /* 
     * 新建一個節點,須要的內存大小,包括了紅黑樹節點大小
     * ngx_http_limit_req_node_t 還有 key 的長度
     */
    size = offsetof(ngx_rbtree_node_t, color)
           + offsetof(ngx_http_limit_req_node_t, data)
           + key->len;
    
    /* 先進行 LRU 淘汰,傳入 n=1,則最多淘汰 2 個節點 */
    ngx_http_limit_req_expire(ctx, 1);
    
    /* 因爲調用 ngx_http_limit_req_lookup 以前已經上過鎖,這裏不用再上 */
    node = ngx_slab_alloc_locked(ctx->shpool, size);

    if (node == NULL) {
        /* 分配失敗考慮再進行一次 LRU 淘汰,及時釋放共享內存空間,這裏 n = 0,最多淘汰 3 個節點 */
        ngx_http_limit_req_expire(ctx, 0);

        node = ngx_slab_alloc_locked(ctx->shpool, size);
        if (node == NULL) {
            ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0,
                          "could not allocate node%s", ctx->shpool->log_ctx);
            return NGX_ERROR;
        }
    }
    
    /* 設置相關的信息 */
    node->key = hash;

    lr = (ngx_http_limit_req_node_t *) &node->color;

    lr->len = (u_short) key->len;
    lr->excess = 0;

    ngx_memcpy(lr->data, key->data, key->len);

    ngx_rbtree_insert(&ctx->sh->rbtree, node);

    ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);

    if (account) {
        /* 一樣地,若是這是最後一個「域」的檢查,就更新 last 和 count,返回 NGX_OK */
        lr->last = now;
        lr->count = 0;
        return NGX_OK;
    }
    
    /* 不然就令 count = 1,把節點放到 ctx 上 */
    lr->last = 0;
    lr->count = 1;

    ctx->node = lr;

    return NGX_AGAIN;
}

ngx_http_limit_req_expire

功能:從隊列(ngx_http_limit_req_shctx_t->queue)尾部遍歷,將過時的紅黑樹節點刪除,及時釋放共享內存空間

static void
ngx_http_limit_req_expire(ngx_http_limit_req_ctx_t *ctx, ngx_uint_t n)
{
    ngx_int_t                   excess;
    ngx_msec_t                  now;
    ngx_queue_t                *q;
    ngx_msec_int_t              ms;
    ngx_rbtree_node_t          *node;
    ngx_http_limit_req_node_t  *lr;

    now = ngx_current_msec;

    /*
     * n == 1 deletes one or two zero rate entries
     * n == 0 deletes oldest entry by force
     *        and one or two zero rate entries
     */
    
    /* 從這裏能夠看到,最多隻會刪除 2 - n + 1 個節點 */
    while (n < 3) {

        if (ngx_queue_empty(&ctx->sh->queue)) {
            return;
        }
        
        /* 隊列尾部的節點最近最久沒有訪問,最有可能被淘汰 */
        q = ngx_queue_last(&ctx->sh->queue);
        
        /* 取出對應節點 */
        lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);

        /* 
         * 從這裏能夠看到,若是 count 大於 0,則不會被淘汰
         * 因此看到 ngx_http_limit_req_handler 裏若是這個 key 在某個「域」超過請求限制頻率時,就把那個節點的 count++,避免不當心把節點刪除
         *  
         */
        if (lr->count) {

            /*
             * There is not much sense in looking further,
             * because we bump nodes on the lookup stage.
             */

            return;
        }

        if (n++ != 0) {

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

            if (ms < 60000) {
                return;
            }

            excess = lr->excess - ctx->rate * ms / 1000;

            if (excess > 0) {
                return;
            }
        }

        ngx_queue_remove(q);

        node = (ngx_rbtree_node_t *)
                   ((u_char *) lr - offsetof(ngx_rbtree_node_t, color));

        ngx_rbtree_delete(&ctx->sh->rbtree, node);

        ngx_slab_free_locked(ctx->shpool, node);
    }
}

ngx_http_limit_req_account

功能:這個函數負責對目前的這個請求計算一個延時時間,計算規則是

  • 遍歷每一個以前在 ngx_http_limit_req_lookup 裏訪問過的「域」

  • 若是這個「域」設置了 nodelay,跳到下一個

  • 不然根據漏桶算法,和 ngx_http_limit_req_lookup 同樣的作法,計算出從上次訪問到如今,應該剩下的請求數,除以 rate,獲得了這些請求數應該延遲的時間

  • 取最大值

    其實這些值均可以在 ngx_http_limit_req_lookup 裏計算出來,不過爲了讓一個函數作一件事,這樣設計條理更加清晰吧。

static ngx_msec_t
ngx_http_limit_req_account(ngx_http_limit_req_limit_t *limits, ngx_uint_t n,
    ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit)
{
    ngx_int_t                   excess;
    ngx_msec_t                  now, delay, max_delay;
    ngx_msec_int_t              ms;
    ngx_http_limit_req_ctx_t   *ctx;
    ngx_http_limit_req_node_t  *lr;

    /* 
     * excess 是以前在 ngx_http_limit_req_lookup 
     * 裏遍歷到的最後一個「域」針對這個請求通過漏桶計算後
     * 應該剩下的請求數; limit 則是最後一個「域」的配置
     */
    excess = *ep;

    if (excess == 0 || (*limit)->nodelay) {
        /* 配置項裏設置了 nodelay 或者 excess = 0 */
        max_delay = 0;

    } else {
        ctx = (*limit)->shm_zone->data;
        /* 
         * 剩下了 excess 個請求,加請求的速率是 rate,那麼延遲
         * 就是 excess * 1000 / ctx->rate,這裏乘以 1000 是由於 rate 的單位是 ms
         */
        max_delay = excess * 1000 / ctx->rate;
    }

    while (n--) {
        /* 反向遍歷以前遍歷過的「域」 */
        ctx = limits[n].shm_zone->data;
        /* 爲了更新信息,因此才須要在 ctx 裏放一個 node */
        lr = ctx->node;

        if (lr == NULL) {
            continue;
        }

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

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

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

        if (excess < 0) {
            excess = 0;
        }
        
        /* 更新節點上的信息 */
        lr->last = now;
        lr->excess = excess;
        lr->count--;

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

        ctx->node = NULL;

        if (limits[n].nodelay) {
            continue;
        }

        delay = excess * 1000 / ctx->rate;

        if (delay > max_delay) {
            max_delay = delay;
            *ep = excess;
            *limit = &limits[n];
        }
    }
    
    /* 這裏就計算出了一個最大延遲值 */
    return max_delay;
}

ngx_http_limit_req_rbtree_insert_value

功能:該模塊自定義的紅黑樹節點插入方法,key 就是根據用戶配置的 limit_req_zone 指令裏的 key 字段,hash 方法是 ngx_crc32_short

static void
ngx_http_limit_req_rbtree_insert_value(ngx_rbtree_node_t *temp,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t          **p;
    ngx_http_limit_req_node_t   *lrn, *lrnt;

    for ( ;; ) {

        if (node->key < temp->key) {

            p = &temp->left;

        } else if (node->key > temp->key) {

            p = &temp->right;

        } else { /* node->key == temp->key */
            /*
             * 值相等不見得 key 必定相同,存在 hash 衝突的
             * 前面說過,ngx_http_limit_req_node_t 和 ngx_rbtree_node_t 
             * 複用了 color 和 data 這兩個字段,ngx_http_limit_req_node_t 的地址
             * 就是 ngx_rbtree_node_t 裏的 color 字段的地址
             */
            lrn = (ngx_http_limit_req_node_t *) &node->color;
            lrnt = (ngx_http_limit_req_node_t *) &temp->color;

            p = (ngx_memn2cmp(lrn->data, lrnt->data, lrn->len, lrnt->len) < 0)
                ? &temp->left : &temp->right;
        }

        if (*p == sentinel) {
            break;
        }

        temp = *p;
    }

    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    /* 新加入節點須要塗成紅色 */
    ngx_rbt_red(node);
}

ngx_http_limit_req_delay

功能:做爲寫事件回調,再次運行 ngx_http_core_run_phases ,執行 HTTP 的 11 個階段處理。

static void
ngx_http_limit_req_delay(ngx_http_request_t *r)
{
    ngx_event_t  *wev;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "limit_req delay");

    wev = r->connection->write;

    if (!wev->timedout) {

        if (ngx_handle_write_event(wev, 0) != NGX_OK) {
            ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
        }

        return;
    }

    wev->timedout = 0;

    if (ngx_handle_read_event(r->connection->read, 0) != NGX_OK) {
        ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
        return;
    }

    r->read_event_handler = ngx_http_block_reading;
    r->write_event_handler = ngx_http_core_run_phases;

    ngx_http_core_run_phases(r);
}

參考資料

相關文章
相關標籤/搜索