2

ngx_http_limit_req_module 是 Nginx 官方提供的一个 http 模块,它工作在 NGX_HTTP_PREACCESS_PHASE 阶段,通过在 nginx.conf 中进行简单地配置,我们可以轻易地对请求速率进行限制。

配置指令

官方文档地址

                        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, location

该指令为名为 name 的共享内存设置一个突发请求限制大小(burst)和一个 nodelay 标志位


limit_req_log_level

Syntax: limit_req_log_level info | notice | warn | error;

Default:

limit_req_log_level error;

Context: http, server, location

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

参考资料


Alex
17 声望6 粉丝

喜欢打篮球的程序员爱好者