Nginx限速模块分为哪几种?按请求速率限速的burst和nodelay参数是呀意思?漏桶算法和令牌桶算法究竟有啊不同?本文将拉动您平探究竟。我们见面经过一些简单易行的示范展示Nginx限速模块是怎么行事的,然后成代码讲解其幕后的算法和法则。

20150712 Created By BaoXinjian

主干算法

每当探究Nginx限速模块之前,我们事先来瞧网络传输中经常因此简单个底流量控制算法:漏桶算法让牌桶算法。这有限但“桶”到底出啊异同呢?

统计 1一、摘要

漏桶算法(leaky bucket)

漏桶算法(leaky
bucket)算法思想如图所示:

统计 2

一个形象的诠释是:

  • 次(请求)从上面倒入水桶,从水桶下方流出(被拍卖);
  • 不及流出的道是水桶中(缓冲),以固定速率流出;
  • 水桶满后水漫起(丢弃)。

以此算法的为主是:缓存请求、匀速处理、多余的乞求直接扔。


让牌桶算法(token bucket)

令牌桶(token
bucket)算法思想如图所示:

统计 3

算法思想是:

  • 俾牌以固定速率产生,并缓存到叫牌桶中;
  • 使牌桶放满时,多余的令牌被废弃;
  • 呼吁而消耗相当于比例之令牌才能够吃拍卖;
  • 令牌不够时,请求让缓存。

对比漏桶算法,令牌桶算法不同之处在于其不只有一样只“桶”,还发生只班,这个桶是故来存放在令牌的,队列才是用来存放请求的。

自图上吧,漏桶和令牌桶算法最醒目的界别就是是否允许突如其来流量(burst)的拍卖,漏桶算法能够粗限制数量的实时传输(处理)速率,对突如其来流量不做额外处理;而使得牌桶算法能够当限定数量的平分传输速率的还要允许某种程度之爆发传输

Nginx按请求速率限速模块使用的凡漏桶算法,即能强行保证请求的实时处理速度不会见越设置的阈值。

假设以OAF中动态创建LOV的效用是不行复杂的一致码事,本文所讲述的动态LOV创建场合用于先行未能够亮页面上会见生多少只LOV,而且LOV所采取的SQL查询,也是由于用户交互而收获的。

Nginx限速模块

Nginx主要出星星点点种植限速措施:按连接数限速(ngx_http_limit_conn_module)、按请求速率限速(ngx_http_limit_req_module)。我们根本讲解按请求速率限速。

思路如下:

遵连接数限速

随连接数限速凡凭限制单个IP(或者其它的key)同时提倡的连续数,超出这范围后,Nginx将直拒绝再多的连天。这个模块的安排于好明,详见ngx_http_limit_conn_module官方文档。

首先创建一个通用的LOV
Region,建立一个询问的视图对象(Select … From
Dual),这个LOV并无能够识破任何内容,在运行时我们见面动态更改视图对象的概念。

比如请求速率限速

遵请求速率限速凡赖限制单个IP(或者其它的key)发送请求的速率,超出指定速率后,Nginx将直拒绝再多的伸手。采用leaky
bucket
算法实现。为深切摸底是模块,我们先从试验现象说于。开始之前我们事先简单介绍一下拖欠模块的安排方式,以下面的部署也例:

http {
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
    ...
    server {
        ...
        location /search/ {
            limit_req zone=mylimit burst=4 nodelay;
        }

使用limit_req_zone重在字,我们定义了一个叫作也mylimit大小也10MB的共享内存区域(zone),用来存放在限速相关的统计信息,限速的key价为二进制的IP地址($binary_remote_addr),限速上限(rate)为2r/s;接着我们应用limit_req要害字用上述规则作用及/search/上。burstnodelay的打算稍后解释。

行使上述规则,对于/search/目录的访问,单个IP的访问速度被限定在了2伸手求/秒,超过这范围的造访将直给Nginx拒绝。

 

试1——毫秒级统计

我们发出如下配置:

...
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server { 
    location / { 
        limit_req zone=mylimit;
    }
}
...

上述规则限制了每个IP访问的快慢为2r/s,并将拖欠规则作用为同目录。如果单个IP在死短的时刻外并发发送多单请求,结果会怎么样也?

# 单个IP 10ms内并发发送6个请求
send 6 requests in parallel, time cost: 2 ms
HTTP/1.1 503 Service Temporarily Unavailable
HTTP/1.1 200 OK
HTTP/1.1 503 Service Temporarily Unavailable
HTTP/1.1 503 Service Temporarily Unavailable
HTTP/1.1 503 Service Temporarily Unavailable
HTTP/1.1 503 Service Temporarily Unavailable
end, total time cost: 461 ms

我们利用单个IP在10ms内发并发送了6只请求,只来1独成功,剩下的5独还给拒绝。我们设置的进度是2r/s,为什么只发生1单成功吧,是免是Nginx限制错了?当然不是,举凡盖Nginx的限流统计是根据毫秒的,我们设置的速是2r/s,转换一下虽是500ms内单个IP只同意通过1单请求,从501ms开始才同意通过第二独请求。

统计 4

统计 5仲、锁定统计信息

实验2——burst允许缓存处理突发请求

实验1咱们看到,我们少日内发送了汪洋请,Nginx按照毫秒级精度统计,超出限制的乞求直接拒绝。这在实际上状况被莫休过于严苛,真实网络环境面临求到来不是匀速的,很可能发生请求“突发”的情状,也就是“一股子一股子”的。Nginx考虑到了这种情形,可以通过burst一言九鼎字被对突发请求的缓存处理,而休是直拒绝。

来拘禁我们的安排:

...
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server { 
    location / { 
        limit_req zone=mylimit burst=4;
    }
}
...

咱们进入了burst=4,意思是每个key(此处是每个IP)最多允许4独突发请求的过来。如果单个IP在10ms内发送6只请求,结果会怎么样也?

# 单个IP 10ms内发送6个请求,设置burst
send 6 requests in parallel, time cost: 2 ms
HTTP/1.1 200 OK
HTTP/1.1 503 Service Temporarily Unavailable
HTTP/1.1 200 OK
HTTP/1.1 200 OK
HTTP/1.1 200 OK
HTTP/1.1 200 OK
end, total time cost: 2437 ms

相对而言实验1改成功数增加了4个,这个我们安的burst数目是平等的。具体处理流程是:1独请求让当下处理,4个请求被置于burst队列里,另外一个要让拒。通过burst参数,我们叫Nginx限流具备了缓存处理突发流量之力

而要留心:burst的来意是被多余的求可以先放到行列里,慢慢处理。如果未加以nodelay参数,队列里之乞求未见面立刻处理,而是按照rate设置的快,以毫秒级精确的快慢逐年处理。


试验3——nodelay降低排队时

试验2蒙受我们看到,通过安装burst参数,我们得允许Nginx缓存处理得水平的突发,多余的乞求可以先放到队里,慢慢处理,这起及了平滑流量的用意。但是要是队列设置的可比深,请求排队的岁月便会于长,用户角度看来就是RT变长了,这对用户大不谐和。有啊解决办法呢?nodelay参数允许请求在排队的下便即刻叫处理,也就是说要请求能上burst队列,就见面即刻为后台worker处理,请留意,这意味burst设置了nodelay时,系统瞬间的QPS可能会见跳rate设置的阈值。nodelay参数要与burst共利用才发出打算。

延续实验2的部署,我们在nodelay选项:

...
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server { 
    location / { 
        limit_req zone=mylimit burst=4 nodelay;
    }
}
...

单个IP 10ms内连发发送6个请求,结果如下:

# 单个IP 10ms内发送6个请求
   实验3, 设置burst和nodelay       |  实验2, 只设置burst
send 6 requests, time cost: 4 ms |  time cost: 2 ms
HTTP/1.1 200 OK                  |  HTTP/1.1 200 OK
HTTP/1.1 200 OK                  |  HTTP/1.1 503 ...
HTTP/1.1 200 OK                  |  HTTP/1.1 200 OK
HTTP/1.1 200 OK                  |  HTTP/1.1 200 OK
HTTP/1.1 503 ...                 |  HTTP/1.1 200 OK
HTTP/1.1 200 OK                  |  HTTP/1.1 200 OK
total time cost: 465 ms          |  total time cost: 2437 ms

和实验2对照,请求成功率没转,但是完整耗时变短了。这怎么解释为?实验2着,有4个请求让坐burst队列当中,工作过程每隔500ms(rate=2r/s)取一个呼吁进行拍卖,最后一个伸手而排队2s才会为处理;实验3饱受,请求放入队列跟实验2是平等的,但不同之是,队列中的要而持有了深受处理的身份,所以实验3遭受的5单请求可以说是同时起受拍卖的,花费时间本变短了。

可是要留意,虽然设置burst和nodelay能够降低突发请求的拍卖时,但是老来拘禁并无会见增进吞吐量的上限,长期吞吐量的上限是出于rate决定的,因为nodelay只能保证burst的伸手被随即处理,但Nginx会限制队列元素释放的进度,就像是限制了驱动牌桶中令牌产生的速度。

见到此间您或会见咨询,加入了nodelay参数后的限速算法,到底算是哪一个“桶”,是漏桶算法还是令牌桶算法?当然还算是漏桶算法。考虑同种植状况,令牌桶算法的token为耗尽时会见怎么开也?由于她发一个告队列,所以会见管接下的请缓存下来,缓存多少受限于行大小。但这时缓存这些请求还有意思呢?如果server已经过载,缓存队列越来越丰富,RT越来越大,即使过了好漫长请求被拍卖了,对用户来说吧从未什么价值了。所以当token不足够用时,最神之做法就是是一直拒绝用户的乞求,这虽变成了漏桶算法,哈哈~

Step1,创建LOV

源码剖析

透过地方的示范,我们队要限速模块出矣一定之认,现在我们深切剖析代码实现。按请求速率限流模块ngx_http_limit_req_module代码位于src/http/modules/ngx_http_limit_req_module.c,900大多好代码可谓短小精悍。相关代码有个别独核心数据结构:

  1. 瑞黑树:通过红黑树记录每个节点(按照声明时指定的key)的统计信息,方便找;
  2. LRU队列:将红黑树上的节点按照最近造访时间排序,时间近的放在队列头部,以便使LRU队列淘汰旧的节点,避免内存溢出。

当下片只主要目标存储在ngx_http_limit_req_shctx_t中:

typedef struct {
    ngx_rbtree_t                  rbtree; /* red-black tree */
    ngx_rbtree_node_t             sentinel; /* the sentinel node of red-black tree */
    ngx_queue_t                   queue; /* used to expire info(LRU algorithm) */
} ngx_http_limit_req_shctx_t;

里头除rbtree和queue之外,还有一个叫sentinel的变量,这个变量用作红黑树的NIL节点。

拖欠模块的基本逻辑在函数ngx_http_limit_req_lookup()惨遭,这个函数主要流程是哪也?对于各级一个要:

  1. 自根节点开始查找红黑树,找到key对应之节点;
  2. 找到后修改该点在LRU队列中之岗位,表示该点最近吃看过;
  3. 施行漏桶算法;
  4. 没找到时根据LRU淘汰,腾出空间;
  5. 扭转并插入新的开门红黑树节点;
  6. 尽下一样漫长限流规则。

流程很清晰,但是代码中牵扯到祥黑树、LRU队列等高档数据结构,是休是会见写得死复杂?好当Nginx作者功力深厚,代码写得简洁易亮,哈哈~

// 漏桶算法核心流程
ngx_http_limit_req_lookup(...){
  while (node != sentinel) {
    // search rbtree
    if (hash < node->key) { node = node->left; continue;} // 1. 从根节点开始查找红黑树
    if (hash > node->key) { node = node->right; continue;}
    rc = ngx_memn2cmp(key->data, lr->data, key->len, (size_t) lr->len);
    if (rc == 0) {// found
      ngx_queue_remove(&lr->queue); // 2. 修改该点在LRU队列中的位置,表示该点最近被访问过
      ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);// 2
      ms = (ngx_msec_int_t) (now - lr->last);
      excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000; // 3. 执行漏桶算法
      if (excess < 0) 
        excess = 0;
      if ((ngx_uint_t) excess > limit->burst)
        return NGX_BUSY; // 超过了突发门限,拒绝
      if (account) {// 是否是最后一条规则
        lr->excess = excess;    
        lr->last = now;    
        return NGX_OK; // 未超过限制,通过
      }
      ...
      return NGX_AGAIN; // 6. 执行下一条限流规则
    }
    node = (rc < 0) ? node->left : node->right; // 1
  } // while
  ...
  // not found
  ngx_http_limit_req_expire(ctx, 1); // 4. 根据LRU淘汰,腾出空间
  node = ngx_slab_alloc_locked(ctx->shpool, size); // 5. 生成新的红黑树节点
  ngx_rbtree_insert(&ctx->sh->rbtree, node);// 5. 插入该节点,重新平衡红黑树
  ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
  if (account) {    
    lr->last = now; 
    lr->count = 0;
    return NGX_OK;
  }
  ...
  return NGX_AGAIN; // 6. 执行下一条限流规则
}

代码有三种回到值,它们的意是:

  • NGX_BUSY 超过了突发门限,拒绝
  • NGX_OK 未超限定,通过
  • NGX_AGAIN 未超过限定,但是还有规则不履行,需实施下一样长条限流规则

上述代码不难理解,但我们还有几只问题:

  1. LRU是怎么落实的?
  2. 漏桶算法是怎促成之?
  3. 每个key相关的burst队排在哪里?

率先以应用LOV的页面的控制器的processRequest方法被创造一个LOV

LRU是哪促成的

LRU算法的兑现好简单,苟一个节点被拜了,那么即使把它换到队的脑瓜儿,当空间欠缺需要淘汰节点时,就选出队列尾部底节点淘汰掉,主要体现在如下代码中:

ngx_queue_remove(&lr->queue); // 2. 修改该点在LRU队列中的位置,表示该点最近被访问过
ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);// 2
...
ngx_http_limit_req_expire(ctx, 1); // 4. 根据LRU淘汰,腾出空间
OAMessageLovInputBean lovItem = (OAMessageLovInputBean)createWebBean(pageContext,LOV_TEXT,null );
region.addIndexedChild(lovItem);

lovItem.setAttributeValue(REGION_CODE, "/xxx/oracle/apps/cux/lov/webui"); 
lovItem.setAttributeValue(REGION_APPLICATION_ID, new Integer(pageContext.getResponsibilityApplicationId()));

addLovItem.setLovRegion(pageContext,"/xxx/oracle/apps/cux/lov/webui/CommonLovRN");
lovItem.setUnvalidated(false);
lovItem.addLovRelations(pageContext,lovItem.getID(),,LOV_RESULT,LOV_REQUIRED_NO);
lovItem.setPrompt(); 

漏桶算法是什么促成的

漏桶算法的落实为比较我们想象的大概,其主干是立即等同执公式excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000,这样代码的意是:excess表示手上key上遗留的请求数,此次遗留的恳求数
= 上次遗留的呼吁数 – 预设速率 X 过去的日子 +
1
。这个1意味手上夫要,由于Nginx内部表示以单位压缩了1000加倍,所以1个请求而变成为1000。

excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000; // 3. 执行漏桶算法
if (excess < 0) 
    excess = 0;
if ((ngx_uint_t) excess > limit->burst)
    return NGX_BUSY; // 超过了突发门限,拒绝
if (account) { // 是否是最后一条规则
    lr->excess = excess;    
    lr->last = now;    
    return NGX_OK; // 未超过限制,通过
}
...
return NGX_AGAIN; // 6. 执行下一条限流规则

上述代码受限算出当前key上遗留的伸手数,如果超过了burst,就一直拒绝;由于Nginx允许多修限速规则以从作用,如果已是最终一条规则,则允许通过,否则执行下同样长达规则。

待专注的凡动态建立LOV时,不要动addLovRelations去多LOV_CRITERIA(criteria
item)
,如果安了criteria item,

单个key相关的burst队排于乌

从未有过单个key相关的burst队列。上面代码中我们视当到达最终一久规则时,只要excess<limit->burst限速模块就会见回NGX_OK,并从未管剩余请求放入队列的操作,这是为Nginx是依据timer来治本要的,当限速模块返回NGX_OK时,调度函数会计算一个延缓处理的时光,同时把这个要放入到共享的timer队列中(一株按等待时由小至十分排序的红黑树)。

ngx_http_limit_req_handler(ngx_http_request_t *r)
{
    ...
    for (n = 0; n < lrcf->limits.nelts; n++) {
        ...
        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);// 释放锁
        ...
        if (rc != NGX_AGAIN)
            break;
    }
    ...
    delay = ngx_http_limit_req_account(limits, n, &excess, &limit);// 计算当前请求需要的延迟时间
    if (!delay) {
        return NGX_DECLINED;// 不需要延迟,交给后续的handler进行处理
    }
    ...
    ngx_add_timer(r->connection->write, delay);// 否则将请求放到定时器队列里
    return NGX_AGAIN; // the request has been successfully processed, the request must be suspended until some event. http://www.nginxguts.com/2011/01/phases/
}

我们看ngx_http_limit_req_handler()调用了函数ngx_http_limit_req_lookup(),并因该返回回值决定如何操作:或是拒绝,或是交给下一个handler处理,或是将呼吁放入定期器队列。当限速规则都通过后,该hanlder通过调用函数ngx_http_limit_req_account()查获当前要需要的延迟时间,如果非需延期,就用请提交后续的handler进行拍卖,否则将呼吁放到定时器队列里。注意是定时器队列是共享的,并无啊单独的key(比如,每个IP地址)设置队列。关于handler模块背景知识之牵线,可参看Tengine团队写的Nginx开发从入门到精通

至于按请求速率限速的规律教学,可参照Rate Limiting with NGINX and NGINX
Plus,关于源码更详实的辨析只是参看ngx_http_limit_req_module
源码分析以及y123456yz的Nginx源码分析的git项目

那么LOV就会见为此原来定义的SQL去拼页面及之值组成SQL查询,而无见面采用我们安的动态SQL去查询。

结尾

正文主要讲解了Nginx按请求速率限速模块的用法及原理,其中burst和nodelay参数是善招误解的,虽然可经过burst允许缓存处理突发请求,结合nodelay能够降低突发请求的拍卖时,但是老来拘禁她们并无见面提高吞吐量的上限,长期吞吐量的上限是由rate决定的。需要特别注意的是,burst设置了nodelay时,系统瞬间的QPS可能会见超越rate设置的阈值。

本文只是针对Nginx管中窥豹,更多关于Nginx介绍的篇章,可参照Tengine团队编写的Nginx开发从入门到精通。

(顺便取一下,对于无是动态创建的LOV,而要动态修改LOV对应之视图对象定义的场面,因为LOV基本的附和项必须装成criteria
item,

为了防LOV的自发性查询,可以装lovMAP的Programmatic
Query
True,这样即便非会见管页面上之值自动加到WHERE子句后查询。) 

 

Step2,修改视图对象的询问

于LOV的决定器类的processRequest倍受动态修改视图对象的查询,因为咱们遮挡了LOV的活动查询,所以在processRequest中尚须处理自动查询的动静

(自动查询的图景是为用户输入了价值或事先早已摘了,重新打开LOV),这里不为来实际的代码。在LOV控制器的processFormRequest方式中必做以下处理

为尽管我们改变了视图对象的概念,但是当点查询按钮时,LOV还是以最好原始定义之SQL绑定条件来查询,所以这里的操作是免给LOV执行查询,而上processRequest重新改变视图的概念:

if ("lovFilter".equals(pageContext.getParameter("event"))) {
    try {
        String strRedirectUrl = pageContext.getRequestUrl();
        int intIndex = strRedirectUrl.indexOf("&amp;");
        strRedirectUrl =
                strRedirectUrl.substring(0, intIndex + 1) + "searchText=" +
                URLEncoder.encode(pageContext.getParameter("searchText")) +
                "&amp;" + "FormRequest=Y&amp;" +
                strRedirectUrl.substring(intIndex + 1);

        pageContext.sendRedirect(strRedirectUrl);
    } catch (Exception expt) {
    }
} 

 

统计 6其三、运行测试


1

 

 

 

 

  public void processRequest(OAPageContext pageContext, OAWebBean
webBean)
  {
    super.processRequest(pageContext, webBean);
      // Get the list of items configured as “passive criteria” for the
LOV.
      Dictionary passiveCriteriaItems =
pageContext.getLovCriteriaItems();
      String sourceType =
(String)passiveCriteriaItems.get(“SourceType”);
      if (sourceType == null || “”.equalsIgnoreCase(sourceType))
      {
        throw new OAException(“必须先行选核算项目项目”);
      }
      String voName = null;
      if (“H0001供应商”.equals(sourceType)){
          voName = “AccountSourceVendorVO1”;
      }
      else if (“H0002银行账户”.equals(sourceType)){
          // Sinolee added on 2013/3/12 for
REQ124-支付模块-付款签收财务总账中核算项目类别选项“银行账户”
          //voName = “AccountSourceBankVO1”;
          voName = “XvkAccountSourceBankVO1”;
      }
      else if (“H0003公司”.equals(sourceType)){
          voName = “AccountSourceOrgVO1”;
      }
      else if (“WK006合同编号”.equals(sourceType)){
          voName = “AccountSourcePoVO1”;
      }
      else if (“H0007工程项目”.equals(sourceType)){
          voName = “AccountSourceProjectVO1”;
      }
      else if (“H0008产品种类”.equals(sourceType)){
          voName = “AccountSourceItemCateVO1”;
      }
      else {
          voName = “AccountSourceTempVO1”;
          throw new OAException(sourceType +
“直接输入,无需要选择值列表”);
      }
      ((OAWebBeanData)webBean).setViewUsageName(voName);

  }

SELECT ‘H0007工程项目’ source_type,
ppa.org_id
,ppa.project_id source_id
      ,ppa.attribute2 source_code
      ,ppa.attribute1 source_name
      
  FROM pa_projects_all      ppa
      ,pa_project_set_lines ppsl
 WHERE ppa.project_id = ppsl.project_id

SELECT ‘H0003公司’ source_type
,paorg.org_id
            ,hrorg.organization_id source_id
            ,hrorg.attribute1 source_code
            ,hrorg.NAME source_name
    FROM hr_organization_units hrorg
            ,pa_all_organizations  paorg
 WHERE paorg.organization_id = hrorg.organization_id
     AND paorg.pa_org_use_type = ‘PROJECTS’
     AND paorg.inactive_date IS NULL
     AND hrorg.attribute1 IS NOT NULL
     AND org_id = fnd_global.org_id

select ‘WK006合同编号’ source_type,
ph.org_id,ph.po_header_id source_id
      ,ph.attribute3 source_code
      ,ph.comments source_name
  FROM po_headers_all ph

 

Thanks and Regards

参考:张礼军 –
http://oracleseeker.com/2008/12/16/dynamic\_create\_lov\_in\_oracle\_ebs/

统计 7