《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,则立即处理。

posted on 2022-05-19 10:35  开心种树  阅读(129)  评论(0编辑  收藏  举报