← 学习库技术专题源码原理 · 故障排查 · 架构实践

专题 12 / 完整阅读

Nginx限流原理与工程实践

从漏桶的时间记账,到共享内存、异步延迟与多实例边界。本文以开源NGINX1.28.0为固定阅读基线,对照官方文档;固定版本用于解释机制,不代表当前生产选型建议。

01 从请求速率到请求负债

漏桶控制的对象

limit_req通过控制请求的接纳与继续处理节奏,保护入口和上游服务。官方将其归类为漏桶算法,但理解实现时,最有用的模型是随时间消化的超额请求量excess。它不统计自然秒内的请求总数,也不统计业务正在执行的任务数。

固定窗口的反例:前一个窗口在0.99秒接纳10个请求,下一个窗口在1.01秒接纳10个请求,每个窗口都满足10次,却在20ms内接纳了20次。按时间差记账可以避免这种窗口切换,但配置允许突发时,仍会出现集中放行。

最小代理配置

http {
    limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
    server {
        listen 8080;
        location /api/ {
            limit_req zone=perip burst=5;
            limit_req_status 429;
            proxy_pass http://127.0.0.1:9000;
        }
    }
}

这里按IP分别限流,10r/s意味着约100ms消化一个请求单位;burst=5允许最多5个超额单位。新Key的首个请求立即通过,其后的超额请求在容量内延迟,超出容量则返回配置的429。默认拒绝码是503,429来自显式配置。

zone的10MB保存Key的状态,不是HTTP请求缓存。100个不同IP各自拥有额度,这个规则不能单独约束服务总流量。官方limit_req文档

单zone的正常流程;多zone暂存与最终记账见第6节。
单zone的正常流程;多zone暂存与最终记账见第6节。

单zone的正常流程;多zone暂存与最终记账见第6节。

02 excess的整数运算与状态提交

候选超额量与内部单位

源码没有为每个IP安排一个定时扣减任务。请求到达时读取当前毫秒时间,用上次记账时间last计算衰减。对于已经存在的Key,单zone计算可写成:

候选excess = max(0, 旧excess - rate × 经过毫秒 / 1000 + 1000)
候选excess > burst → 拒绝,不提交该候选值
候选excess ≤ burst → 记账,再计算是否需要等待

ngx_http_limit_req_lookup:已有Key的计算、拒绝与提交 · 原文件L404—L532

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

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

        if (rc == 0) {
            ngx_queue_remove(&lr->queue);
            ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);

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

            if (ms < -60000) {
                ms = 1;

            } else if (ms < 0) {
                ms = 0;
            }

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

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

            *ep = excess;

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

            if (account) {
                lr->excess = excess;

                if (ms) {
                    lr->last = now;
                }

                return NGX_OK;
            }

            lr->count++;

            ctx->node = lr;

            return NGX_AGAIN;
        }

        node = (rc < 0) ? node->left : node->right;
    }

    *ep = 0;

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

    ngx_http_limit_req_expire(ctx, 1);

    node = ngx_slab_alloc_locked(ctx->shpool, size);

    if (node == NULL) {
        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) {
        lr->last = now;
        lr->count = 0;
        return NGX_OK;
    }

    lr->last = 0;
    lr->count = 1;

    ctx->node = lr;

    return NGX_AGAIN;
}
量 配置或数学值 内部值
一个请求单位 1 1000
速率 10r/s 10000
burst阈值 5 5000
delay阈值 2 2000
时间差 200ms 200

把整数舍入暂时省略,已存在Key的公式是E新=max(0,E旧+1−R×Δt),Δt以秒计。E旧=4,经过200ms,R=10,则E新=4+1−2=3。源码采用整数除法;rate=Nr/m还会先换算并截断为内部速率,因此纸面公式不是任意参数下的逐位精确结果。

新Key、拒绝与时间回退

这个公式不用于新节点的首个请求。创建节点时源码直接将lr->excess=0。所以全新Key、同一毫秒、burst=5时,是首个请求加5个超额请求,共6个被接纳。长期空闲只会将负债消化到0,不会无限积累可以立即消费的额度。

ngx_http_limit_req_lookup:新节点的首个请求 · 原文件L404—L532

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

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

        if (rc == 0) {
            ngx_queue_remove(&lr->queue);
            ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);

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

            if (ms < -60000) {
                ms = 1;

            } else if (ms < 0) {
                ms = 0;
            }

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

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

            *ep = excess;

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

            if (account) {
                lr->excess = excess;

                if (ms) {
                    lr->last = now;
                }

                return NGX_OK;
            }

            lr->count++;

            ctx->node = lr;

            return NGX_AGAIN;
        }

        node = (rc < 0) ? node->left : node->right;
    }

    *ep = 0;

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

    ngx_http_limit_req_expire(ctx, 1);

    node = ngx_slab_alloc_locked(ctx->shpool, size);

    if (node == NULL) {
        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) {
        lr->last = now;
        lr->count = 0;
        return NGX_OK;
    }

    lr->last = 0;
    lr->count = 1;

    ctx->node = lr;

    return NGX_AGAIN;
}

单zone拒绝在赋值lr->excess之前返回NGX_BUSY。假设已记账excess=5、last=t0,t0时第7次请求的候选值为6,被拒绝,但状态仍是5;t0+100ms再到一个请求,候选值为5+1−1=5,可以接纳。拒绝不会把负债永久推高。查找仍会更新节点在近期访问队列中的位置,因此不能把拒绝理解为完全不触及共享数据。

时间回退也有专门分支:时间差小于−60000ms时按1ms处理,较小负差按0处理。解释正常请求序列时可以忽略这个防御分支,读取完整实现时应保留它。

03 rate、burst、nodelay与delay的组合

三种延迟策略

burst决定候选超额量的拒绝阈值,delay决定其中有多少超额单位可以立即继续。不写delay时阈值为0;delay=2表示2个超额单位,而不是2秒。默认burst为0,配置时省略burst即可;不要写burst=0或delay=0,此版本解析器要求显式数值大于0。

# 以下三行是三个独立场景,选择其中一个配置。
limit_req zone=perip burst=5;
limit_req zone=perip burst=5 nodelay;
limit_req zone=perip burst=5 delay=2;

对于全新Key,同一毫秒到达7个请求,rate=10r/s、burst=5:

到达次序 候选excess 默认延迟 nodelay delay=2
1 0 立即 立即 立即
2 1 100ms 立即 立即
3 2 200ms 立即 立即
4 3 300ms 立即 100ms
5 4 400ms 立即 200ms
6 5 500ms 立即 300ms
7 6 拒绝 拒绝 拒绝

表中的延迟从各请求完成限流判断时算起,是相同到达时刻的理论值;真实事件调度、网络和后端处理会引入偏差,请求完成顺序也不保证等同于到达顺序。

延迟毫秒 = max(0, 内部excess - 内部delay阈值) × 1000 / 内部rate
普通单位:等待秒数 = max(0, E - D) / R

ngx_http_limit_req_account:最后一项的初始延迟 · 原文件L535—L606

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 = *ep;

    if ((ngx_uint_t) excess <= (*limit)->delay) {
        max_delay = 0;

    } else {
        ctx = (*limit)->shm_zone->data;
        max_delay = (excess - (*limit)->delay) * 1000 / ctx->rate;
    }

    while (n--) {
        ctx = limits[n].shm_zone->data;
        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);

        if (ms < -60000) {
            ms = 1;

        } else if (ms < 0) {
            ms = 0;
        }

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

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

        if (ms) {
            lr->last = now;
        }

        lr->excess = excess;
        lr->count--;

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

        ctx->node = NULL;

        if ((ngx_uint_t) excess <= limits[n].delay) {
            continue;
        }

        delay = (excess - limits[n].delay) * 1000 / ctx->rate;

        if (delay > max_delay) {
            max_delay = delay;
            *ep = excess;
            *limit = &limits[n];
        }
    }

    return max_delay;
}

突发仍需记账

nodelay允许容量内的请求立即处理,但仍然记账。源码将delay设为一个极大阈值,随后同样使用burst判定。6个请求瞬时通过后,负债是5;再到请求仍需等待时间消化出接纳空间。后端提前执行完毕,不会归还limit_req额度。

ngx_http_limit_req:nodelay的阈值解析 · 原文件L963—L1064

static char *
ngx_http_limit_req(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_limit_req_conf_t  *lrcf = conf;

    ngx_int_t                    burst, delay;
    ngx_str_t                   *value, s;
    ngx_uint_t                   i;
    ngx_shm_zone_t              *shm_zone;
    ngx_http_limit_req_limit_t  *limit, *limits;

    value = cf->args->elts;

    shm_zone = NULL;
    burst = 0;
    delay = 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;

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

            continue;
        }

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

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

            continue;
        }

        if (ngx_strcmp(value[i].data, "nodelay") == 0) {
            delay = NGX_MAX_INT_T_VALUE / 1000;
            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;
    limit->delay = delay * 1000;

    return NGX_CONF_OK;
}
标号代表同一毫秒内到达的次序;真实调度存在误差。
标号代表同一毫秒内到达的次序;真实调度存在误差。

标号代表同一毫秒内到达的次序;真实调度存在误差。

04 延迟通过事件定时器恢复

限流发生的HTTP阶段

限流处理函数注册在HTTP的PREACCESS阶段。对普通proxy_pass路径,它在向上游发起业务请求前作出判断。这里不能使用return 200来验证限流:rewrite阶段可能提前结束请求,根本没有进入PREACCESS。实验应使用代理或确实继续到内容阶段的处理器。

ngx_http_limit_req_init:PREACCESS处理函数注册 · 原文件L1086—L1102

static ngx_int_t
ngx_http_limit_req_init(ngx_conf_t *cf)
{
    ngx_http_handler_pt        *h;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_http_limit_req_handler;

    return NGX_OK;
}

挂起与恢复

需要等待时,模块设置请求回调,在连接写事件上标记delayed并调用ngx_add_timer,然后返回NGX_AGAIN,暂停此请求的阶段推进。Worker可以继续处理其他连接,没有为这个等待执行阻塞式sleep。

ngx_http_limit_req_handler:挂起请求并注册定时器 · 原文件L194—L329

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_status) {
        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) {
            ngx_http_limit_req_unlock(limits, n);
            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 = ngx_crc32_short(key.data, key.len);

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

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

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

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

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

        ngx_http_limit_req_unlock(limits, n);

        if (lrcf->dry_run) {
            r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED_DRY_RUN;
            return NGX_DECLINED;
        }

        r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED;

        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) {
        r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_PASSED;
        return NGX_DECLINED;
    }

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

    if (lrcf->dry_run) {
        r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_DELAYED_DRY_RUN;
        return NGX_DECLINED;
    }

    r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_DELAYED;

    if (r->connection->read->ready) {
        ngx_post_event(r->connection->read, &ngx_posted_events);

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

    r->read_event_handler = ngx_http_test_reading;
    r->write_event_handler = ngx_http_limit_req_delay;

    r->connection->write->delayed = 1;
    ngx_add_timer(r->connection->write, delay);

    return NGX_AGAIN;
}

定时器到期清除事件的delayed标记后,ngx_http_limit_req_delay将写处理函数切换为ngx_http_core_run_phases,恢复HTTP阶段推进。r->main->limit_req_status已设置,重新进入处理函数时可避免重复限流记账。

ngx_http_limit_req_delay:恢复HTTP阶段 · 原文件L332—L360

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->delayed) {

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

        return;
    }

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

等待中的请求仍持有连接、请求结构、内存和文件描述符等资源。burst不是一个免费的FIFO缓存容量,模块也没有为每个IP构建保存请求对象的独立FIFO桶。对于默认延迟,满负债时最大理论等待约为burst/rate;例如1000/10=100秒,足以耗尽许多客户端的超时预算。

05 多Worker共享内存、锁与淘汰

状态结构与互斥

每个zone包含共享红黑树、近期访问双向队列和slab内存池。各Worker访问同一份状态;共享互斥锁保护查找、节点分配和记账,不会一直持有到上游请求执行结束。

ngx_http_limit_req_handler:带共享锁的查找 · 原文件L194—L329

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_status) {
        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) {
            ngx_http_limit_req_unlock(limits, n);
            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 = ngx_crc32_short(key.data, key.len);

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

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

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

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

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

        ngx_http_limit_req_unlock(limits, n);

        if (lrcf->dry_run) {
            r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED_DRY_RUN;
            return NGX_DECLINED;
        }

        r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED;

        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) {
        r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_PASSED;
        return NGX_DECLINED;
    }

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

    if (lrcf->dry_run) {
        r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_DELAYED_DRY_RUN;
        return NGX_DECLINED;
    }

    r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_DELAYED;

    if (r->connection->read->ready) {
        ngx_post_event(r->connection->read, &ngx_posted_events);

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

    r->read_event_handler = ngx_http_test_reading;
    r->write_event_handler = ngx_http_limit_req_delay;

    r->connection->write->delayed = 1;
    ngx_add_timer(r->connection->write, delay);

    return NGX_AGAIN;
}
字段或结构 作用 不能混淆的概念
hash与原始Key 先按CRC32定位,再比较原始字节和长度 哈希碰撞不会直接合并两个Key
last、excess 时间记账和超额负债 不表示业务执行耗时
queue 访问后移到队头,尾部用于淘汰 不是等待请求的FIFO队列
count 保护多zone流程中暂存待记账的节点 不是正在代理的请求数
ctx->node 进程内暂存当前zone待记账节点指针 不是集群级请求状态

例如单zone、同一时刻excess=4:WorkerA在锁内提交5,WorkerB随后读到5,计算6并拒绝。单zone的状态不会因为有8个Worker就得到8倍独立额度。zone级共享锁也意味着热点Key和高基数流量需要关注锁竞争与内存开销。

内存容量与节点淘汰

新Key创建时先执行expire(ctx,1),尝试清除尾部较旧、已无负债且count=0的节点。通常要求至少60秒未记账,一次最多清理两个。分配失败后再调用expire(ctx,0),在count=0的条件下强制淘汰最旧节点,再尝试清理最多两个符合条件的旧节点。如果仍无法分配,返回NGX_ERROR,正常模式按limit_req_status拒绝。burst没有超过,也可能因状态内存不足而拒绝。

ngx_http_limit_req_expire:淘汰条件 · 原文件L632—L695

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
     */

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

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

官方以二进制IP为Key给出的典型状态占用是32位平台64字节、64位平台128字节,1MB约容纳8千个128字节状态;10MB约8万个是容量估算,实际取决于Key长度和分配开销。节点被淘汰后再次访问会重新创建状态,频繁高基数Key会改变限流连续性,不能把有限zone当作永久历史账本。

06 多个zone的判定与记账

lookup与account的分工

同时配置单IP和虚拟主机总量时,handler依次检查各zone。非最后一个zone通常返回NGX_AGAIN,递增count并暂存ctx->node;最后一个配置项检查成功时可以直接提交。全部通过后,account回头对暂存节点重新计算、记账、递减count,并取各zone要求的最大等待时间。

zone A:lookup → 在锁内检查 → 暂存节点,count++
zone B:lookup → 检查并提交最后一项
account:回到A重新取时钟并记账,count--
最终延迟:max(A的等待时间, B的等待时间)

释放引用与并发边界

如果后续zone拒绝,ngx_http_limit_req_unlock减少之前暂存节点的count,清空进程内指针。这是释放节点引用,不是回滚一次后端调用,也不是对所有已经发生的访问行为做事务回滚。

ngx_http_limit_req_account:暂存节点的最终记账 · 原文件L535—L606

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 = *ep;

    if ((ngx_uint_t) excess <= (*limit)->delay) {
        max_delay = 0;

    } else {
        ctx = (*limit)->shm_zone->data;
        max_delay = (excess - (*limit)->delay) * 1000 / ctx->rate;
    }

    while (n--) {
        ctx = limits[n].shm_zone->data;
        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);

        if (ms < -60000) {
            ms = 1;

        } else if (ms < 0) {
            ms = 0;
        }

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

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

        if (ms) {
            lr->last = now;
        }

        lr->excess = excess;
        lr->count--;

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

        ctx->node = NULL;

        if ((ngx_uint_t) excess <= limits[n].delay) {
            continue;
        }

        delay = (excess - limits[n].delay) * 1000 / ctx->rate;

        if (delay > max_delay) {
            max_delay = delay;
            *ep = excess;
            *limit = &limits[n];
        }
    }

    return max_delay;
}

每个zone有自己的互斥锁,检查与最终记账可能分开,整个多zone流程没有持有一把跨zone事务锁。此版本account重新计算后不再次比较burst,并发交错时不能把“多zone都满足规则”升级为任意瞬间严格原子预留的承诺。上线应测实际流量与拒绝比例,而不是把流程图当作强一致事务协议。

07 单IP与实例总量的完整配置

完整示例与配置继承

下面是可替换地址后验证的示例,不是某个生产系统的已披露配置。两个zone只在本示例的submit路径被使用:单IP20r/s,当前虚拟主机的submit总量75r/s。若其他location复用相同zone与相同Key,它们也会共享这份额度。

worker_processes 2;
events { worker_connections 1024; }
http {
    limit_req_zone $binary_remote_addr zone=submit_ip:10m rate=20r/s;
    limit_req_zone $server_name zone=submit_total:1m rate=75r/s;
    log_format rate_log '$remote_addr pid=$pid "$request" '
                        'status=$status limit=$limit_req_status '
                        'rt=$request_time upstream=$upstream_response_time';
    server {
        listen 8080;
        server_name api.example.test;
        access_log logs/access.log rate_log;
        location = /submit {
            limit_req zone=submit_ip burst=10 nodelay;
            limit_req zone=submit_total burst=15;
            limit_req_status 429;
            limit_req_log_level notice;
            proxy_connect_timeout 2s;
            proxy_read_timeout 5s;
            proxy_pass http://127.0.0.1:9000;
        }
    }
}

第一层允许少量同IP突发立即进入后续阶段,第二层仍可能要求延迟;nodelay只作用于配置它的规则。$server_name是虚拟主机的配置名称,同一个名称产生同一个Key。一个zone可以容纳多个虚拟主机Key,不能把它自动视为整台机器所有服务的统一总预算。

继承不是追加。当前server/location只要定义了自己的limit_req列表,就不会再继承父级limit_req列表。需要两层保护时,应在实际生效的location写全两条规则,并通过nginx -T核对最终配置。

可信身份与Key选择

有代理或ALB时,必须先恢复可信客户端地址。示例中的文档网段需替换成实际代理范围:

# 放在http/server中;仅适用于已启用http_realip_module的构建。
set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

只信任受控代理,确认代理如何清理和追加转发头;直接信任用户可伪造的头会让限流可绕过。恢复真实IP后,企业或家庭共享公网出口仍会共享IP预算。用户维度Key应来自认证后的受信任身份,不能直接取任意客户端自报ID。realip官方文档

空Key不会记账,可以用map按明确策略豁免;但也要防止缺少身份意外变成空Key。超过65535字节的Key在此版本会记录错误并跳过该规则,不应让Key长度由不受控的长头部决定。

08 dry-run、日志与逐步启用

观察状态

首次配置可以在目标location临时启用limit_req_dry_run on;,观察真实Key、状态与影响,再决定rate和burst。它不执行实际拒绝或延迟;单zone超额拒绝分支也不会额外提交被拒绝请求的候选excess。因此dry-run是按该模块逻辑运行的观察模式,并不是记录所有到达请求的完整吞吐计数器。

$limit_req_status 含义
PASSED 通过,未要求限流延迟
DELAYED 接纳但通过定时器延迟
REJECTED 因超额或状态分配失败拒绝
DELAYED_DRY_RUN 判断需要延迟,观察模式下立即继续
REJECTED_DRY_RUN 判断应拒绝,观察模式下继续

未命中生效规则、空Key或提前结束的路径可能没有上述状态。HTTP429/503也可能来自上游,必须结合limit状态与error_log判断来源;REJECTED本身也不能区分超额和内存分配失败。

指标与参数选择

同时观察每个Key的流量分布、限流状态占比、上游错误率、连接占用和端到端延迟。被延迟的请求会把等待计入request_time,upstream_response_time只覆盖上游交互相关时间;二者不能简单当作同一个指标。多规则情况下状态代表最终请求结果,不提供每个zone独立的完整时间序列。

配额根据后端持续处理能力制定,burst根据短时承载能力制定,delay根据客户端延迟预算制定。对于默认延迟,burst/rate给出满负债的等待估算;增大burst可能减少拒绝,也可能积累大量等待。配置nodelay后,又可能让这批请求同时冲击后端。

09 速率、并发与跨实例边界

请求速率与在途请求

limit_req按时间恢复额度,与请求何时完成无关。若稳态接纳速率λ=150r/s、平均耗时W=0.2秒,Little定律给出的平均在途量L≈λW=30;耗时变成2秒时估算为300。该关系适用于稳定系统的平均值,不是每一瞬间的并发上限,也不能直接用于无限积压中的系统。

机制 控制维度 边界
limit_req 请求接纳速率与超额负债 无法感知Java线程或数据库是否饱和
limit_conn 满足统计条件的连接/并发请求 只统计已完整读入请求头并处理中的请求;HTTP/2、HTTP/3各并发请求分别计数
应用线程池与隔离 业务执行资源 需处理队列容量与拒绝策略
超时与熔断 等待时间与故障传播 需匹配重试与幂等策略

例如在http定义limit_conn_zone $binary_remote_addr zone=conn_ip:10m;,再在目标location配置limit_conn conn_ip 20;。这是Nginx所定义的统计口径,不能称为20个Java工作线程或20条全部TCP连接。limit_conn官方文档

多实例与全局预算

开源标准limit_req的共享内存只在同一Nginx实例的Worker间共享。两台实例各75r/s、均衡分流且持续运行时,名义持续接纳能力相加为150r/s;单IP也可能在两台各自获得额度。路由偏斜、扩缩容、Key淘汰和burst会改变观察结果,不能由此保证任意一秒或整个集群严格不超过150次。

如果需要全局预算,应明确作用范围和一致性要求,再选择统一入口、分片配额或集中式原子记账。集中机制还需处理网络延迟、故障时放行/拒绝策略、时钟与重复请求。商业版提供zone同步选项,但同步不应未经验证就被当作强一致全局限流。

10 可复现的本地实验

运行方式

随文提供lab-nginx.conf、lab.py和run-lab.py。实验只监听127.0.0.1,使用两个Worker和独立zone测试默认延迟、nodelay、delay=2、dry-run和双zone。后端是真实HTTP代理目标,不通过rewrite阶段的return模拟。

# 安装或构建固定版本的nginx后,将路径替换为实际可执行文件。
python3 run-lab.py --nginx /path/to/nginx --output /tmp/nginx-rate-lab

脚本检查版本为1.28.0,创建独立临时前缀,运行nginx -t,启动本地后端并并发发出7个请求,写出客户端时间、后端接收时间、状态和Worker PID,最后停止实验进程。各场景使用不同Key;多次运行不会把上次负债误当作新Key。

理论预期与实验范围

对于前三个单zone场景,预期各6次成功、1次429;默认等待的成功请求后端接收时间大致分布在0、100、200、300、400、500ms,nodelay集中接收,delay=2大致在0、0、0、100、200、300ms。并发启动存在调度误差,源码表中的“同一毫秒”条件更严格,原始测量值和理论值应分开阅读。

dry-run应让7次请求继续到后端,同时日志出现观察状态。双zone实验验证组合规则的可观察行为,不能凭一次实验证明任意并发交错的强一致性。实验结果及执行范围见VERIFICATION.md;这里不把步骤或预期写成生产实测。

11 源码阅读路线与参考依据

固定版本与调用链

固定标签release-1.28.0,提交481d28cb4e04c8096b9b6134856891dc52ecc68f。本文的源码窗口逐行保留原文,并标注原始行号;本地完整模块源码与许可证随页面提供。

继续阅读

建议按init → handler → lookup → account → delay → expire阅读:先确认执行阶段和返回码,再跟踪Key、候选excess、提交时机、事件恢复和状态淘汰。需要掌握的核心是:rate消化负债,burst约束接纳,delay控制等待,zone与锁控制状态共享范围。

本文根据“Nginx限流原理”讨论的主题重新组织,经固定源码核对后补齐配置、状态分支和实验材料;所有数值示例均注明条件,公开内容采用技术学习与工程实践表述。

先记住这几件事

  • excess按请求到达时间记账;burst设接纳上限,delay与nodelay决定延迟策略。
  • 共享zone在Worker之间共享状态,但多个Nginx实例各自计数,不是全局限额。
  • 延迟请求通过事件定时器挂起和恢复;限流不等于并发数限制。
  • 多zone先分别查验,成功后统一提交,拒绝路径需注意状态与引用释放。
  • 配置可信客户端地址、日志观测与dry-run,再逐步启用并用负载实验验证。

四个重点问题

核心要点

常见误解与源码

全文搜索

输入关键词,搜索本篇完整正文与答案。