专题 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节。
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与锁控制状态共享范围。
- 固定提交的limit_req模块
- limit_req官方指令文档
- limit_conn官方指令文档
- 真实客户端地址配置
- 现有NGINX源码手册:继续阅读事件循环、HTTP阶段、连接限制、upstream与输出链。
本文根据“Nginx限流原理”讨论的主题重新组织,经固定源码核对后补齐配置、状态分支和实验材料;所有数值示例均注明条件,公开内容采用技术学习与工程实践表述。
先记住这几件事
- excess按请求到达时间记账;burst设接纳上限,delay与nodelay决定延迟策略。
- 共享zone在Worker之间共享状态,但多个Nginx实例各自计数,不是全局限额。
- 延迟请求通过事件定时器挂起和恢复;限流不等于并发数限制。
- 多zone先分别查验,成功后统一提交,拒绝路径需注意状态与引用释放。
- 配置可信客户端地址、日志观测与dry-run,再逐步启用并用负载实验验证。