《ngx底层设计和源码分析》—— 限流模块
1. 限流算法
ngx_http_limit_req_module 使用桶算法,
定义一个桶,桶的出口流速固定,入口流速大于出口时,请求被缓存在桶中,
定义桶的容积,当请求超过容积,则丢弃请求。
limit_req_zone : 定义一个桶,且定义桶的出口流速
limit_req : 定义使用桶,和桶的容积
使用示例
2. 源码解析
2.1 ngx_http_limit_req_zone
保存桶属性,申请分配一个共享块
43 typedef struct {
44 ngx_http_limit_req_shctx_t *sh; // 共享块上的 桶
45 ngx_slab_pool_t *shpool; // 共享块内存池
46 /* integer value, 1 corresponds to 0.001 r/s */
47 ngx_uint_t rate; // 桶的出口流速
48 ngx_http_complex_value_t key; // 桶的节点 key值
49 ngx_http_limit_req_node_t *node; // 最近命中此桶的 请求的节点 // 这里是否会有 并发冲突?
50 } ngx_http_limit_req_ctx_t;
863 static char *
864 ngx_http_limit_req_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf )
865 {
873 ngx_http_limit_req_ctx_t *ctx;
970 ctx->rate = rate * 1000 / scale; // 流速精度为毫秒级
971
972 shm_zone = ngx_shared_memory_add(cf, &name, size,
973 &ngx_http_limit_req_module);
986
987 shm_zone->init = ngx_http_limit_req_init_zone;
988 shm_zone->data = ctx;
989
990 return NGX_CONF_OK;
991 }
共享块上有 一个红黑树 和 一个队列
红黑树用于根据 key 快速找到请求节点
队列为LRU队列,用于回收最长时间未使用的内存,防止共享块被耗尽
722 static ngx_int_t
723 ngx_http_limit_req_init_zone(ngx_shm_zone_t *shm_zone, void *data)
724 {
732 if (octx) {
746 ctx->sh = octx->sh;
747 ctx->shpool = octx->shpool;
748
749 return NGX_OK;
750 }
762 ctx->sh = ngx_slab_alloc(ctx->shpool, sizeof(ngx_http_limit_req_shctx_t));
763 if (ctx->sh == NULL) {
764 return NGX_ERROR;
765 }
766
767 ctx->shpool->data = ctx->sh;
768
769 ngx_rbtree_init(&ctx->sh->rbtree, &ctx->sh->sentinel,
770 ngx_http_limit_req_rbtree_insert_value);
771
772 ngx_queue_init(&ctx->sh->queue);
785
786 return NGX_OK;
787 }
可以看出桶是 http{} 块定义,且桶无法区分 节点是来自 哪个server {} 或 哪个 location{},
所以 传入给 桶的 节点,使用的 key 需要包含 host uri remote_ip 等信息。
2.2 limit_req 使用限制器
用户在需要 限流的 location 下使用 limit_req 指令,添加限制器到 ngx_http_limit_req_conf_t->limits 数组
61 typedef struct {
62 /* ngx_http_limit_req_limit_t * */
63 // limiters for current location
64 ngx_array_t limits;
65 ngx_uint_t limit_log_level;
66 ngx_uint_t delay_log_level;
67 ngx_uint_t status_code;
68 ngx_flag_t dry_run;
69 } ngx_http_limit_req_conf_t;
999 static char *
1000 ngx_http_limit_req(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
1001 {
1002 ngx_http_limit_req_conf_t *lrcf = conf;
1023 shm_zone = ngx_shared_memory_add(cf, &s, 0,
1024 &ngx_http_limit_req_module);
1090 limit = ngx_array_push(&lrcf->limits);
1091 if (limit == NULL) {
1092 return NGX_CONF_ERROR;
1093 }
1094
1095 limit->shm_zone = shm_zone;
1096 limit->burst = burst * 1000;
1097 limit->delay = delay * 1000;
1098
1099 return NGX_CONF_OK;
1100 }
2.3 请求处理
在 NGX_HTTP_PREACCESS_PHASE 阶段 处理
1122 static ngx_int_t
1123 ngx_http_limit_req_init(ngx_conf_t *cf)
1124 {
1125 ngx_http_handler_pt *h;
1126 ngx_http_core_main_conf_t *cmcf;
1127
1128 cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module );
1129
1130 h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handler s);
1131 if (h == NULL) {
1132 return NGX_ERROR;
1133 }
1134
1135 *h = ngx_http_limit_req_handler;
1136
1137 return NGX_OK;
1138 }
对请求进行处理,可能 丢弃,延迟,通过
199 static ngx_int_t
200 ngx_http_limit_req_handler(ngx_http_request_t *r)
201 {
225 // 使用本location下所有限流器,依次检查
// 出现 桶溢出,NGX_BUSY 时跳出 for {}
// 若所有 桶 都没有溢出,返回 NGX_OK,跳出 for{}
226 for (n = 0; n < lrcf->limits.nelts; n++) {
227
228 limit = &limits[n];
229
230 ctx = limit->shm_zone->data;
231
232 if (ngx_http_complex_value(r, &ctx->key, &key) != NGX_OK) {
233 ngx_http_limit_req_unlock(limits, n);
234 return NGX_HTTP_INTERNAL_SERVER_ERROR;
235 }
236
237 if (key.len == 0) {
238 continue;
239 }
240
241 if (key.len > 65535) {
242 ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
243 "the value of the \"%V\" key "
244 "is more than 65535 bytes: \"%V\"",
245 &ctx->key.value, &key);
246 continue;
247 }
248
249 hash = ngx_crc32_short(key.data, key.len);
250
251 ngx_shmtx_lock(&ctx->shpool->mutex);
252
253 /*
254 * NGX_OK : pass the all limiter
255 * NGX_AGAIN : pass limiter, check the next limiter
256 * NGX_BUSY : trigger limit, need to discard
257 * NGX_ERROR : error, need to discard
258 */
259 rc = ngx_http_limit_req_lookup(limit, hash, &key, &excess,
260 (n == lrcf->limits.nelts - 1));
261
262 ngx_shmtx_unlock(&ctx->shpool->mutex);
267
268 if (rc != NGX_AGAIN) {
269 break;
270 }
271 }
273 if (rc == NGX_DECLINED) {
274 return NGX_DECLINED;
275 }
276 // 请求溢出,直接丢弃
277 if (rc == NGX_BUSY || rc == NGX_ERROR) {
278
287 ngx_http_limit_req_unlock(limits, n);
288
289 if (lrcf->dry_run) {
290 r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED_DR Y_RUN;
291 return NGX_DECLINED;
292 }
293
294 r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_REJECTED;
295
296 return lrcf->status_code;
297 }
299 /* rc == NGX_AGAIN || rc == NGX_OK */
300
301 if (rc == NGX_AGAIN) {
302 excess = 0;
303 }
304 // 请求没有溢出,查看 是否需要延迟处理(当桶的出口流速小于 入口流速)
305 delay = ngx_http_limit_req_account(limits, n, &excess, &limit);
306 // 不需要延迟处理,交给下一个模块
307 if (!delay) {
308 r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_PASSED;
309 return NGX_DECLINED;
310 }
// 延迟处理
333 r->read_event_handler = ngx_http_test_reading;
334 r->write_event_handler = ngx_http_limit_req_delay;
335
336 r->connection->write->delayed = 1;
337 ngx_add_timer(r->connection->write, delay);
338
339 return NGX_AGAIN;
340 }
查看请求是否溢出
415 static ngx_int_t
416 ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit, ngx_uint_ t hash,
417 ngx_str_t *key, ngx_uint_t *ep, ngx_uint_t account)
418 {
427 now = ngx_current_msec;
428
429 ctx = limit->shm_zone->data;
430
431 node = ctx->sh->rbtree.root;
432 sentinel = ctx->sh->rbtree.sentinel;
433 // 遍历树
434 while (node != sentinel) {
435
436 if (hash < node->key) {
437 node = node->left;
438 continue;
439 }
440
441 if (hash > node->key) {
442 node = node->right;
443 continue;
444 }
445
446 /* hash == node->key */
447 // 找到节点
448 lr = (ngx_http_limit_req_node_t *) &node->color;
449
450 rc = ngx_memn2cmp(key->data, lr->data, key->len, (size_t) lr->len);
451
452 if (rc == 0) {
// 当请求命中树,将节点移动到 LRU队列 首部,以保持 LRU队列 尾部为 最长时间没有被访问节点
453 // move the recently accessed node to the head of the LRU queue
454 ngx_queue_remove(&lr->queue);
455 ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
456 // 计算对桶容量的消耗
457 ms = (ngx_msec_int_t) (now - lr->last);
458
459 if (ms < -60000) {
460 ms = 1;
461
462 } else if (ms < 0) {
463 ms = 0;
464 }
466 // ctx->rate * ms : num of accessible
467 // + 1000 : num of accesses consumed
468 // lr->excess : sum of overflow num
469 // excess : last overflow num
470 excess = lr->excess - ctx->rate * ms / 1000 + 1000;
471
472 if (excess < 0) {
473 excess = 0;
474 }
475
476 *ep = excess;
477 // 当消耗大于桶的容积,则溢出
478 // req overflow
479 if ((ngx_uint_t) excess > limit->burst) {
480 return NGX_BUSY;
481 }
482 // 没有超过桶的容积,
// 若含有其他限流器,则返回 NGX_AGAIN,以检查下一个限流器
// 若为最后一个限流器,则返回NGX_OK
483 // pass the last limiter, update time and excess
484 // return NGX_OK
485 if (account) {
486 lr->excess = excess;
487
488 if (ms) {
489 lr->last = now;
490 }
491
492 return NGX_OK;
493 }
// 对本节点进行标记,表明本节点刚刚被访问过
495 // increase num of visits
496 lr->count++;
497
498 ctx->node = lr;
499
500 return NGX_AGAIN;
501 }
502
503 node = (rc < 0) ? node->left : node->right;
504 }
// 若没有命中树,则添加新节点
506 // add new node
507
508 *ep = 0;
509
510 size = offsetof(ngx_rbtree_node_t, color)
511 + offsetof(ngx_http_limit_req_node_t, data)
512 + key->len;
513 // 回收长时间没有使用过的节点
514 // recover expired mem
515 ngx_http_limit_req_expire(ctx, 1);
516
517 node = ngx_slab_alloc_locked(ctx->shpool, size);
518
519 if (node == NULL) {
520 ngx_http_limit_req_expire(ctx, 0);
521
522 node = ngx_slab_alloc_locked(ctx->shpool, size);
523 if (node == NULL) {
524 ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0,
525 "could not allocate node%s", ctx->shpool->lo g_ctx);
526 return NGX_ERROR;
527 }
528 }
529
530 node->key = hash;
531
532 lr = (ngx_http_limit_req_node_t *) &node->color;
533
534 lr->len = (u_short) key->len;
535 lr->excess = 0;
536
537 ngx_memcpy(lr->data, key->data, key->len);
538
539 ngx_rbtree_insert(&ctx->sh->rbtree, node);
540
541 ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
542
543 if (account) {
544 lr->last = now;
545 lr->count = 0;
546 return NGX_OK;
547 }
548
549 lr->last = 0;
550 lr->count = 1;
551
552 ctx->node = lr;
553
554 return NGX_AGAIN;
555 }
回收最长时间未访问的节点
655 static void
656 ngx_http_limit_req_expire(ngx_http_limit_req_ctx_t *ctx, ngx_uint_t n)
657 {
665 now = ngx_current_msec;
666
667 /*
668 * n == 1 deletes one or two zero rate entries
669 * n == 0 deletes oldest entry by force
670 * and one or two zero rate entries
671 */
672 // n == 1 : 回收 超过60秒没有请求命中,且为桶缓存的节点,最多2个
// n == 0 : 强制回收一个,再 按n==1,可能再回收 2个
673 while (n < 3) {
675 if (ngx_queue_empty(&ctx->sh->queue)) {
676 return;
677 }
678 // 获得最老的节点
679 // get oldest node
680 q = ngx_queue_last(&ctx->sh->queue);
681
682 lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);
683 // 若 lr->count>0 表示该节点最近才被命中,则说明所有的节点都不应该被回收
684 if (lr->count) {
685
686 /*
687 * There is not much sense in looking further,
688 * because we bump nodes on the lookup stage.
689 */
690
691 return;
692 }
// n != 0 时,检查本节点是否可以避免回收
694 if (n++ != 0) {
695
696 ms = (ngx_msec_int_t) (now - lr->last);
697 ms = ngx_abs(ms);
698
699 if (ms < 60000) {
700 return;
701 }
702
703 excess = lr->excess - ctx->rate * ms / 1000;
704
705 if (excess > 0) {
706 return;
707 }
708 }
// 回收节点
710 ngx_queue_remove(q);
711
712 node = (ngx_rbtree_node_t *)
713 ((u_char *) lr - offsetof(ngx_rbtree_node_t, color));
714
715 ngx_rbtree_delete(&ctx->sh->rbtree, node);
716
717 ngx_slab_free_locked(ctx->shpool, node);
718 }
719 }
检查请求是否延迟处理
558 static ngx_msec_t
559 ngx_http_limit_req_account(ngx_http_limit_req_limit_t *limits, ngx_uin t_t n,
560 ngx_uint_t *ep, ngx_http_limit_req_limit_t **limit)
561 {
// ep 为最后一个限流器的 延迟结果
568 excess = *ep;
569
570 if ((ngx_uint_t) excess <= (*limit)->delay) {
571 max_delay = 0;
572
573 } else {
574 ctx = (*limit)->shm_zone->data;
575 max_delay = (excess - (*limit)->delay) * 1000 / ctx->rate;
576 }
// 遍历所有限流器,找到最到 延迟值
578 while (n--) {
// 在 ngx_http_limit_req_lookup 时,将 本请求对应的红黑树节点指针保存在了 ctx->node,
// 所有可以 不进行 遍历,直接获得节点。
579 ctx = limits[n].shm_zone->data;
580 lr = ctx->node;
581
582 if (lr == NULL) {
583 continue;
584 }
585
586 ngx_shmtx_lock(&ctx->shpool->mutex);
587
588 now = ngx_current_msec;
589 ms = (ngx_msec_int_t) (now - lr->last);
590
591 if (ms < -60000) {
592 ms = 1;
593
594 } else if (ms < 0) {
595 ms = 0;
596 }
597
598 excess = lr->excess - ctx->rate * ms / 1000 + 1000;
599
600 if (excess < 0) {
601 excess = 0;
602 }
603
604 if (ms) {
605 lr->last = now;
606 }
607
608 lr->excess = excess;
609 lr->count--;
610
611 ngx_shmtx_unlock(&ctx->shpool->mutex);
612
613 ctx->node = NULL;
614
615 if ((ngx_uint_t) excess <= limits[n].delay) {
616 continue;
617 }
618
619 delay = (excess - limits[n].delay) * 1000 / ctx->rate;
620
621 if (delay > max_delay) {
622 max_delay = delay;
623 *ep = excess;
624 *limit = &limits[n];
625 }
626 }
627
628 return max_delay;
629 }
3. 总结
此模块展示了 共享块的使用,
由于共享块是 全局的(无法区分根据配置上下文区分(server{} location{})),
所以所有 server{} location{} 的 传来的数据 都应该可以 记录到共享块的容器,
为了区分,这些 数据,必须使用 key。
为了高效回收共享块内存,使用 LRU 链表,
LRU链表就是 把 最近命中的节点放到 链表首部,那么链表尾部就是最长时间未被使用的节点,则最应该没释放。
ngx对红黑树的节点的使用
ngx_http_limit_req_node_t->data[1] 是柔性数组,所以分配空间是数组长度为 key->len
ngx计算请求是否过载 也是十分巧妙
470 excess = lr->excess - ctx->rate * ms / 1000 + 1000;
lr->excess 是桶当前剩余 容量
ctx->rate * ms/1000 : 桶的出口流速*时间 = 这段时间内桶增加的容量
+ 1000 : 单次访问消耗的 容量
所以最后得到 本次请求后,桶剩余的容量,
若容量大于容积,则溢出,则丢弃,
若容量大于0,则延迟处理,
若容量小于0,则立即处理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?