前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解nginx的请求限流模块

深入理解nginx的请求限流模块

作者头像
码农心语
发布2024-04-09 15:57:39
1560
发布2024-04-09 15:57:39
举报
文章被收录于专栏:码农心语码农心语

1. 引言

??当构建高流量的Web应用程序时,保护服务器免受过多请求的影响是至关重要的。过多的请求可能会导致服务器过载,降低性能甚至导致系统崩溃。为了解决这个问题,nginx提供了一个强大的请求限速模块。该模块允许您根据自定义规则限制客户端请求的速率,并且还可以使用延迟机制来平滑处理超出限制的请求。在本文中,我们将深入探讨nginx的请求限速模块,了解它的工作原理、配置选项以及如何在实际应用中使用它来保护您的服务器免受恶意或异常请求的影响。

??当涉及到请求限速功能时,nginx采用了一种称为漏桶算法的经典算法来实现。漏桶算法是一种简单而有效的请求限速算法,它允许以固定的速率处理请求,并且可以处理突发的流量。

??漏桶算法的概念类似于一个物理漏桶,请求被视为水滴,而服务器的处理能力被视为漏桶的出水速率。当请求到达时,它们被放入漏桶中。如果漏桶已满,即请求超出了限定的速率,那么这些请求将被延迟处理或直接丢弃,以确保服务器不会超过其处理能力。

??在nginx中,请求限速模块使用漏桶算法来限制请求的速率。您可以根据自己的需求配置漏桶的容量和速率。容量决定了漏桶可以容纳的最大请求数量,而速率决定了漏桶的出水速率,也就是服务器处理请求的速率。

??当请求到达时,nginx会检查漏桶中的当前请求数量。如果请求数量小于漏桶的容量,请求将被立即处理。然而,如果请求数量超过了漏桶的容量,nginx可以选择将请求延迟处理或直接丢弃,以确保请求速率不会超过设定的限制。

??通过采用漏桶算法,nginx能够有效地控制请求的速率,平衡服务器的负载并保护系统免受过多请求的影响。接下来,我们将探讨如何在nginx中配置和使用请求限速模块,以及如何应对突发的请求流量。

2. 开启请求限速功能

??在nginx中,配置和使用请求限速模块相对简单,以下是一些关键的步骤和选项:

  • 启用请求限速模块:首先,确保您的nginx已经编译并启用了limit_req模块,nginx默认是开启的,因此确认编译的时候没有通过--without-http_limit_req_module 关闭这个模块。
  • 设置请求限速规则:在nginx的配置文件中,您可以通过使用limit_req_zone指令来定义请求限速的共享内存区域。该指令指定了限速的区域名称、存储限速状态的内存大小以及限速的参数。例如下面按照来源IP进行请求限速:
代码语言:javascript
复制
 http {
   limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;
 }

在上面的示例中,我们创建了一个名为mylimit的请求限速区域,使用IP地址作为标识符,并设置了内存大小为10MB,速率为每秒1个请求。

3. 应用请求限速:在需要应用请求限速的地方,可以使用limit_req指令来定义请求限速的策略。例如:

代码语言:javascript
复制
    
    server {
        location /api {
            limit_req zone=mylimit burst=5 nodelay;
            ...
        }
    }

在上面的示例中,我们在/api的位置块中应用了请求限速策略。我们使用了之前定义的mylimit区域,并设置了突发请求数为5,并且使用了nodelay参数,表示不延迟处理超出限制的请求。

??您还可以使用其他参数来进一步控制请求限速的行为,例如delay参数可以指定延迟处理超出限制的请求的数量。

  1. 4. 处理突发请求流量:在面对突发的请求流量时,请求限速模块可以通过延迟处理或丢弃请求来应对。通过调整突发请求数量和延迟时间,您可以根据实际需求平衡服务器的负载和响应速度。 例如,如果您预计会有短暂的高峰请求流量,您可以设置较高的突发请求数量,以允许一定程度的突发。而如果您更关注稳定性和响应时间,您可以设置较小的突发请求数量,并使用适当的延迟来平滑处理请求。

??nginx还允许同时设置多个limit_req指令同时作用于一个请求,譬如:

代码语言:javascript
复制
limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
limit_req_zone $server_name zone=perserver:10m rate=10r/s;

server {
    ...
    limit_req zone=perip burst=5 nodelay;
    limit_req zone=perserver burst=10;
}

以上配置将限制来自单个IP地址的请求处理速率,并同时按照虚拟服务器维度进行请求限制速率处理。

??通过配置和使用Nginx的请求限速模块,您可以灵活地控制请求的速率,保护服务器免受过多请求的影响。这对于确保Web应用程序的稳定性、可用性和性能至关重要。请记住,在实际应用中,您可能需要根据您的特定需求进行一些调整和优化,以获得最佳的结果。

在配置指令中,比较令人费解的是limit_req指令中的burst参数、delay参数和nodelay参数。下面进行说明:

  • burst参数:如果nginx短时间内收到了大量请求,超出限制的请求直接拒绝,这在实际场景中未免过于严苛了。在真实的应用环境中,请求到来并不是匀速的,而是存在潮汐现象,当一个“突发波峰”来的时候,nginx可以通过burst关键字开启对突发请求的缓存,采用漏桶算法对进来的请求进行平滑处理,而不是生硬地直接拒绝。
  • nodelay参数:开启这个参数,表示在漏桶中缓存的过量的请求不进行延时处理,直接提供服务。
  • delay参数:这个参数设定了延迟处理多少个漏桶中缓存的过量的请求数量。

  • 3. 源码分析
  • 3.1 配置指令
  • 3.1.1 limit_req_zone指令
  • ??limit_req_zone指令的作用是定义一个共享内存区,用于在worker进程间共享限速的状态信息,因此请求限速模块的限速功能是服务器级别的,而不是单个worker进程级别的。其配置指令定义如下:
代码语言:javascript
复制
 { ngx_string("limit_req_zone"),
      NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE3,
      ngx_http_limit_req_zone,
      0,
      0,
      NULL 
 },

ngx_http_limit_req_zone函数的重要逻辑就是解析相应的参数,然后创建共享内存区:

代码语言:javascript
复制
static char *
ngx_http_limit_req_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
......
  ngx_http_limit_req_ctx_t *ctx;
......  

  ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_limit_req_ctx_t));

  解析配置指令
......

    ctx->rate = rate * 1000 / scale;

  /* 创建共享内存区 */
    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;
    }

    shm_zone->init = ngx_http_limit_req_init_zone;
    shm_zone->data = ctx;     /* 设置共享内存区的上下文信息 */
}

3.1.2 limit_req指令

??limit_req指令则是开启请求限速功能,它需要引用前面limit_req_zone指令定义的共享内存区,并且指定允许的busrt突发值和delay值。其配置指令定义如下:

代码语言:javascript
复制
    { ngx_string("limit_req"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE123,
      ngx_http_limit_req,
      NGX_HTTP_LOC_CONF_OFFSET,
      0,
      NULL },
代码语言:javascript
复制
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_http_limit_req_limit_t *limit, *limits;

  ......
  解析配置指令


  /* 如果还没有创建限速规则数组,则创建一个 */  
    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_req指令添加到lrcf->limits数组中 */
    limit = ngx_array_push(&lrcf->limits);
    if (limit == NULL) {
        return NGX_CONF_ERROR;
    }

    limit->shm_zone = shm_zone;
    limit->burst = burst * 1000;    /* busrt的单位是r/ms,所以乘以1000 */
    limit->delay = delay * 1000;    /* delay的单位是r/ms,所以乘以1000 */

    return NGX_CONF_OK;
}

3.1.3 limit_req_dry_run指令

?? 该指令设置了一个开关,如果是on的话,如果发生了限流事件,只是在error日志中打印日志,而不是实际执行限流动作。这个指令主要用于开启限流操作前进行测试验证工作。配置指令定义如下:

代码语言:javascript
复制
    { ngx_string("limit_req_dry_run"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_limit_req_conf_t, dry_run),
      NULL },

3.1.4 limit_req_log_level指令

??该指令设置了当发生限流事件的时候,在nginx的error日志中输出的日志的日志级别。配置指令定义如下:

代码语言:javascript
复制
    { ngx_string("limit_req_log_level"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_enum_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_limit_req_conf_t, limit_log_level),
      &ngx_http_limit_req_log_levels },

3.1.5 limit_req_status指令

??该指令设置了当发生限流事件的时候,nginx返回给客户端的响应码。

代码语言:javascript
复制

    { ngx_string("limit_req_status"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_num_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_limit_req_conf_t, status_code),
      &ngx_http_limit_req_status_bounds },

3.2 模块初始化

?? 模块初始化回调函数ngx_http_limit_req_init中设置了在NGX_HTTP_PREACCESS_PHASE阶段的处理函数,该阶段在对应的请求匹配到了location配置之后,因此,可以应用对应location配置下的limit_req指令规则。源码如下:

代码语言:javascript
复制
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;
}

3.3 请求处理

??ngx_http_limit_req_handler函数是Nginx请求限速模块中的核心函数之一。它是在请求处理过程中被调用的处理程序,用于检查并处理请求是否超出了限速规则。

??当一个请求到达nginx服务器时,ngx_http_limit_req_handler函数会被触发。它的主要功能是检查请求是否超出了预定义的限速规则,并根据规则中配置的处理方式来决定如何处理该请求。

??以下是ngx_http_limit_req_handler函数的主要工作流程:

  1. 获取请求的限速区域:首先,函数会根据配置的限速区域参数(例如zone=mylimit)从请求上下文中获取请求的限速区域。
  2. 检查请求是否超出限速:接下来,函数会检查当前请求的标识符(例如IP地址)在限速区域中的状态。它会根据区域的配置,比较请求的速率与限制的速率,以确定请求是否超出了限速。
  3. 处理超出限速的请求:如果请求超出了限速,函数将根据配置的处理方式执行相应的操作。这可能包括延迟处理请求、丢弃请求或直接处理请求,取决于配置中使用的参数。
  4. 更新限速区域状态:无论请求是否超出限速,函数都会根据实际情况更新限速区域的状态。这可以包括增加请求计数、更新时间戳等操作,以反映最新的请求情况。

3.3.1 ngx_http_limit_req_handler

??其实现源码如下:

代码语言:javascript
复制

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

  /* 获取本模块location级别的配置并得到请求限速规则列表limits */
    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

  /* 对limits请求限速规则列表中所有的规则执行限速工作 */
    for (n = 0; n < lrcf->limits.nelts; n++) {

        limit = &limits[n];

        ctx = limit->shm_zone->data;
        
    /* 根据limit_req_zone中设置的key变量来获取变量值 */
        if (ngx_http_complex_value(r, &ctx->key, &key) != NGX_OK) {
            /* 当前限速状态节点的使用者计数器-1 */
            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);

    /* 检查并执行限速规则, n == lrcf->limits.nelts - 1)对应的是account参数,
       表示如果是最后一个规则,需要记录限速状态
     */
        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;
        }
    }

  /* rc == NGX_DECLINED表示本模块因为没有配置限速规则而放弃权利,由其他模块继续处理 */
    if (rc == NGX_DECLINED) {
        return NGX_DECLINED;
    }

  /* rc == NGX_BUSY表示当前请求并发量超过了限制,向客户端返回错误代码
       rc == NGX_ERROR表示内部错误了, 这里应该是共享内存区域分配内存失败
     */
    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);
        }
        
        /* 当前限速状态节点的使用者计数器-1 */
        ngx_http_limit_req_unlock(limits, n);

        /* dry_run标记开启,则不实际执行限流动作 */
        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;
}

?? 这里对请求延时逻辑再说明一下。以下代码截取了ngx_http_limit_req_handler函数的最后一部分,如下:

代码语言:javascript
复制
    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);

首先执行

r->main->limit_req_status = NGX_HTTP_LIMIT_REQ_DELAYED,表示限流已经处理了,后续的子请求不要再重复处理了。 ? 其次如果当前连接已经有数据进来了,那么通过ngx_post_event延后读取数据;如果没有数据进来,那么需要重新向epoll框架注册读取事件。 ? 再次将读取事件处理回调函数设置为ngx_http_test_reading,它只是负责检测一下连接是否中断。同时将写时间回调函数设置为ngx_http_limit_req_delay。 ? 最后在写事件上开启一个定时器,等待超时的时候回调ngx_http_limit_req_delay。

下面顺带分析一下ngx_http_limit_req_delay函数。

代码语言:javascript
复制
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) {
    /* 如果不是定时器时间到了,那么是正常的写事件进来了,因为现在还是在延时过程中,
       现在还不能处理写事件,则重新向epoll注册写事件,避免写事件丢失 */
        if (ngx_handle_write_event(wev, 0) != NGX_OK) {
            ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
        }

        return;
    }

  /* 这里表明定时已经到了,延时已经结束,需要重新注册读事件,
     最后通过ngx_http_core_run_phases继续处理当前的请求 */
    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);
}

设置的r->read_event_handler和r->write_event_handler回调函数是在ngx_http_request_handler函数中被回调的。下面列出了ngx_http_request_handler函数的源码:

代码语言:javascript
复制
static void
ngx_http_request_handler(ngx_event_t *ev)
{
    ngx_connection_t    *c;
    ngx_http_request_t  *r;

    c = ev->data;
    r = c->data;

    ngx_http_set_log_request(c->log, r);

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
                   "http run request: \"%V?%V\"", &r->uri, &r->args);

    if (c->close) {
        r->main->count++;
        ngx_http_terminate_request(r, 0);
        ngx_http_run_posted_requests(c);
        return;
    }

    if (ev->delayed && ev->timedout) {
        ev->delayed = 0;    /* 超时事件发生的时候 ev->delayed标记被置0 */
        ev->timedout = 0;
    }

    if (ev->write) {
        r->write_event_handler(r);

    } else {
        r->read_event_handler(r);
    }

    ngx_http_run_posted_requests(c);
}

3.3.1 ngx_http_limit_req_lookup

??ngx_http_limit_req_lookup函数通过查找保存了请求统计数据的红黑树,结合当前限流规则,判断是否需要进行限流操作。

代码语言:javascript
复制

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;   /* 获取限流共享内存区域的配置上下文信息 */

  /* 根据当前请求的key值在共享内存中的红黑树查找是否已经有对应的统计数据节点在里面 
     查找就是普通的二叉查找树的遍历方法
  */
    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) {
          /* 找到了对应key的节点 */

      /* 将当前节点移动到LRU队列的头部,避免被末尾淘汰 */
            ngx_queue_remove(&lr->queue);
            ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);

      /* ms为离最后一次限流判断处理的毫秒数 
         为什么可能为负数?是不是可能因为多个worker进程本身的ngx_current_msec
         不是同步的,因为nginx本身有个定时器,
         每个worker进程会周期性地设置ngx_current_msec值,
         所以如果计算出来的ms为负数,则需要强制进行调整
      */
            ms = (ngx_msec_int_t) (now - lr->last);

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

            } else if (ms < 0) {
                ms = 0;
            }
            
      /* lr->excess到上一轮结束后漏桶中剩余请求数(1000倍)
         ctx->rate * ms / 1000(最后一次限流判断处理到当前这个时间允许处理的请求书)                +1000 是增加本次请求数(1000倍)
         excess是本次处理完后的漏桶中遗留的请求数。
       */
            excess = lr->excess - ctx->rate * ms / 1000 + 1000;

      /* excess < 0 表示露桶中已经没有剩余的请求了 */
            if (excess < 0) {
                excess = 0;
            }

            *ep = excess;

      /* 如果遗留请求数超过的burst突发值,则返回NGX_BUSY,最终会给客户端返回错误码 */
            if ((ngx_uint_t) excess > limit->burst) {
                return NGX_BUSY;
            }

      /* 如果是最后一个限速规则,则需要将统计值记录到当前key对应的节点中 */
            if (account) {
                lr->excess = excess; /* 设置最后限流剩余请求数(1000倍) */

                if (ms) {
                    lr->last = now;  /* 设置最后限流请求执行时间 */
                }

                return NGX_OK;
            }

      /* 当前节点的引用计数+1,
         在ngx_http_limit_req_expire函数中只会回收引用计数=0的节点,
         避免数据访问竞争引起的问题
       */
            lr->count++;
            
      /* 将当前匹配的节点ngx_http_limit_req_node_t设置到上下文中 */
            ctx->node = lr;

            return NGX_AGAIN;
        }

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

  /* 如果节点没有找到,那么需要分配新的节点,在分配的时候因为共享内存不足,
     需要进行LRU淘汰回收部分内存,然后再分配 */
    *ep = 0;

  /* 计算需要分配的内存大小,内存分布大致为:
      ngx_rbtree_node_t
      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回收操作,仅回收请求率为0的红黑树节点 */
    ngx_http_limit_req_expire(ctx, 1);

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

    if (node == NULL) {
    
    /* 还是没有分配成功,重新执行LRU回收操作(从队尾开始强制回收) */
        ngx_http_limit_req_expire(ctx, 0);

        node = ngx_slab_alloc_locked(ctx->shpool, size);
        if (node == NULL) {
        /* 最终还是分配失败,则返回NGX_ERROR */
            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);
  
  /* 将新建的节点添加到红黑树和LRU队列中 */
    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;
}

从以上代码可以看到,漏桶算法其实没有分配真正的漏桶,而是通过一个记录excess的计数器和last时间戳就可以完成了。其原理是计算excess值:

代码语言:javascript
复制
excess = lr->excess - ctx->rate * ms / 1000 + 1000;

计算得到的excess值就是从最后一次限流请求开始到现在经历的时间,允许有多少个请求进来,实际还差多少。 ? 所以后面会判断如果excess超过burst值,就意味着并发太大了,就直接给拒绝了。

代码语言:javascript
复制
if ((ngx_uint_t) excess > limit->burst) {
                return NGX_BUSY;
}

3.3.2 ngx_http_limit_req_account

ngx_http_limit_req_account主要的任务就是用来为当前请求计算延时时间。源码如下:

代码语言:javascript
复制

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;

  /* 设置max_delay的初始值,max_delay为最大需要延迟的时间(ms)
     后面在限流规则表中循环计算,得到最大的延时时间
   */
    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;
        }
        
    /* 计算剩余请求数(x1000) */
        excess = lr->excess - ctx->rate * ms / 1000 + 1000;

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

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

        lr->excess = excess;
        lr->count--;   /* 当前节点的引用计数-1 */

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

        ctx->node = NULL;

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

    /* 当前剩余请求数-需要delay的请求数后
       再除以每秒允许的请求数就是需要delay的时间(ms) */
        delay = (excess - limits[n].delay) * 1000 / ctx->rate;

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

    return max_delay;
}

经过以上分析,nginx限流模块ngx_http_limit_req_module的主要逻辑都已经清晰了。重点需要关注的是漏桶算法、busrt突发、请求延时逻辑,掌握了这几个的逻辑,理解ngx_http_limit_req_module模块的整个原理应该就不难了。

??通过深入理解Nginx的请求限速模块,管理员可以灵活地控制请求的处理速率,保护服务器免受过多请求的影响。合理配置和使用请求限速模块,对于确保Web应用程序的稳定性、可用性和性能至关重要。请记住,在实际应用中,根据具体需求进行适当的优化和调整,以获得最佳的结果。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-03-16,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 码农心语 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 开启请求限速功能
    • 3.1.2 limit_req指令
      • 3.1.3 limit_req_dry_run指令
        • 3.1.4 limit_req_log_level指令
          • 3.1.5 limit_req_status指令
            • 3.2 模块初始化
              • 3.3 请求处理
                • 3.3.1 ngx_http_limit_req_handler
                • 3.3.1 ngx_http_limit_req_lookup
                • 3.3.2 ngx_http_limit_req_account
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com