主頁 > 知識庫 > Nginx源碼研究之nginx限流模塊詳解

Nginx源碼研究之nginx限流模塊詳解

熱門標(biāo)簽:地圖標(biāo)注w是什么方向 昆明外呼系統(tǒng) 臨汾電銷機器人費用 福州呼叫中心外呼系統(tǒng)哪家好 新鄉(xiāng)人工智能電話機器人加盟 七大洲地圖標(biāo)注 地圖標(biāo)注需要提交啥資料入駐 河南省鄭州市地圖標(biāo)注 400電話申請找哪家公司

高并發(fā)系統(tǒng)有三把利器:緩存、降級和限流;

限流的目的是通過對并發(fā)訪問/請求進(jìn)行限速來保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以拒絕服務(wù)(定向到錯誤頁)、排隊等待(秒殺)、降級(返回兜底數(shù)據(jù)或默認(rèn)數(shù)據(jù));

高并發(fā)系統(tǒng)常見的限流有:限制總并發(fā)數(shù)(數(shù)據(jù)庫連接池)、限制瞬時并發(fā)數(shù)(如nginx的limit_conn模塊,用來限制瞬時并發(fā)連接數(shù))、限制時間窗口內(nèi)的平均速率(nginx的limit_req模塊,用來限制每秒的平均速率);

另外還可以根據(jù)網(wǎng)絡(luò)連接數(shù)、網(wǎng)絡(luò)流量、CPU或內(nèi)存負(fù)載等來限流。

1.限流算法

最簡單粗暴的限流算法就是計數(shù)器法了,而比較常用的有漏桶算法和令牌桶算法;

1.1計數(shù)器

計數(shù)器法是限流算法里最簡單也是最容易實現(xiàn)的一種算法。比如我們規(guī)定,對于A接口來說,我們1分鐘的訪問次數(shù)不能超過100個。

那么我們我們可以設(shè)置一個計數(shù)器counter,其有效時間為1分鐘(即每分鐘計數(shù)器會被重置為0),每當(dāng)一個請求過來的時候,counter就加1,如果counter的值大于100,就說明請求數(shù)過多;

這個算法雖然簡單,但是有一個十分致命的問題,那就是臨界問題。

如下圖所示,在1:00前一刻到達(dá)100個請求,1:00計數(shù)器被重置,1:00后一刻又到達(dá)100個請求,顯然計數(shù)器不會超過100,所有請求都不會被攔截;

然而這一時間段內(nèi)請求數(shù)已經(jīng)達(dá)到200,遠(yuǎn)超100。

1.2 漏桶算法

如下圖所示,有一個固定容量的漏桶,按照常量固定速率流出水滴;如果桶是空的,則不會流出水滴;流入到漏桶的水流速度是隨意的;如果流入的水超出了桶的容量,則流入的水會溢出(被丟棄);

可以看到漏桶算法天生就限制了請求的速度,可以用于流量整形和限流控制;

1.3 令牌桶算法

令牌桶是一個存放固定容量令牌的桶,按照固定速率r往桶里添加令牌;桶中最多存放b個令牌,當(dāng)桶滿時,新添加的令牌被丟棄;

當(dāng)一個請求達(dá)到時,會嘗試從桶中獲取令牌;如果有,則繼續(xù)處理請求;如果沒有則排隊等待或者直接丟棄;

可以發(fā)現(xiàn),漏桶算法的流出速率恒定或者為0,而令牌桶算法的流出速率卻有可能大于r;

2.nginx基礎(chǔ)知識

Nginx主要有兩種限流方式:按連接數(shù)限流(ngx_http_limit_conn_module)、按請求速率限流(ngx_http_limit_req_module);

學(xué)習(xí)限流模塊之前還需要了解nginx對HTTP請求的處理過程,nginx事件處理流程等;

2.1HTTP請求處理過程

nginx將HTTP請求處理流程分為11個階段,絕大多數(shù)HTTP模塊都會將自己的handler添加到某個階段(其中有4個階段不能添加自定義handler),nginx處理HTTP請求時會挨個調(diào)用所有的handler;

typedef enum {
 NGX_HTTP_POST_READ_PHASE = 0, //目前只有realip模塊會注冊handler(nginx作為代理服務(wù)器時有用,后端以此獲取客戶端原始ip)
 
 NGX_HTTP_SERVER_REWRITE_PHASE, //server塊中配置了rewrite指令,重寫url
 
 NGX_HTTP_FIND_CONFIG_PHASE, //查找匹配location;不能自定義handler;
 NGX_HTTP_REWRITE_PHASE,  //location塊中配置了rewrite指令,重寫url
 NGX_HTTP_POST_REWRITE_PHASE, //檢查是否發(fā)生了url重寫,如果有,重新回到FIND_CONFIG階段;不能自定義handler;
 
 NGX_HTTP_PREACCESS_PHASE,  //訪問控制,限流模塊會注冊handler到此階段
 
 NGX_HTTP_ACCESS_PHASE,  //訪問權(quán)限控制
 NGX_HTTP_POST_ACCESS_PHASE, //根據(jù)訪問權(quán)限控制階段做相應(yīng)處理;不能自定義handler;
 
 NGX_HTTP_TRY_FILES_PHASE,  //只有配置了try_files指令,才會有此階段;不能自定義handler;
 NGX_HTTP_CONTENT_PHASE,  //內(nèi)容產(chǎn)生階段,返回響應(yīng)給客戶端
 
 NGX_HTTP_LOG_PHASE   //日志記錄
} ngx_http_phases;

nginx使用結(jié)構(gòu)體ngx_module_s表示一個模塊,其中字段ctx,是一個指向模塊上下文結(jié)構(gòu)體的指針;nginx的HTTP模塊上下文結(jié)構(gòu)體如下所示(上下文結(jié)構(gòu)體的字段都是一些函數(shù)指針):

typedef struct {
 ngx_int_t (*preconfiguration)(ngx_conf_t *cf);
 ngx_int_t (*postconfiguration)(ngx_conf_t *cf); //此方法注冊handler到相應(yīng)階段
 
 void  *(*create_main_conf)(ngx_conf_t *cf); //http塊中的主配置
 char  *(*init_main_conf)(ngx_conf_t *cf, void *conf);
 
 void  *(*create_srv_conf)(ngx_conf_t *cf); //server配置
 char  *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
 
 void  *(*create_loc_conf)(ngx_conf_t *cf); //location配置
 char  *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

以ngx_http_limit_req_module模塊為例,postconfiguration方法簡單實現(xiàn)如下:

static ngx_int_t ngx_http_limit_req_init(ngx_conf_t *cf)
{
 h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
 
 *h = ngx_http_limit_req_handler; //ngx_http_limit_req_module模塊的限流方法;nginx處理HTTP請求時,都會調(diào)用此方法判斷應(yīng)該繼續(xù)執(zhí)行還是拒絕請求
 
 return NGX_OK;
}

2.2 nginx事件處理簡單介紹

假設(shè)nginx使用的是epoll。

nginx需要將所有關(guān)心的fd注冊到epoll,添加方法生命如下:

static ngx_int_t ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

方法第一個參數(shù)是ngx_event_t結(jié)構(gòu)體指針,代表關(guān)心的一個讀或者寫事件;nginx為事件可能會設(shè)置一個超時定時器,從而能夠處理事件超時情況;定義如下:

struct ngx_event_s {
 
 ngx_event_handler_pt handler; //函數(shù)指針:事件的處理函數(shù)
 
 ngx_rbtree_node_t timer;  //超時定時器,存儲在紅黑樹中(節(jié)點的key即為事件的超時時間)
 
 unsigned   timedout:1; //記錄事件是否超時
 
};

一般都會循環(huán)調(diào)用epoll_wait監(jiān)聽所有fd,處理發(fā)生的讀寫事件;epoll_wait是阻塞調(diào)用,最后一個參數(shù)timeout是超時時間,即最多阻塞timeout時間如果還是沒有事件發(fā)生,方法會返回;

nginx在設(shè)置超時時間timeout時,會從上面說的記錄超時定時器的紅黑樹中查找最近要到時的節(jié)點,以此作為epoll_wait的超時時間,如下面代碼所示;

ngx_msec_t ngx_event_find_timer(void)
{
 node = ngx_rbtree_min(root, sentinel);
 timer = (ngx_msec_int_t) (node->key - ngx_current_msec);
 
 return (ngx_msec_t) (timer > 0 ? timer : 0);
}

同時nginx在每次循環(huán)的最后,會從紅黑樹中查看是否有事件已經(jīng)過期,如果過期,標(biāo)記timeout=1,并調(diào)用事件的handler;

void ngx_event_expire_timers(void)
{
 for ( ;; ) {
  node = ngx_rbtree_min(root, sentinel);
 
  if ((ngx_msec_int_t) (node->key - ngx_current_msec) <= 0) { //當(dāng)前事件已經(jīng)超時
   ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));
 
   ev->timedout = 1;
 
   ev->handler(ev);
 
   continue;
  }
 
  break;
 }
}

nginx就是通過上面的方法實現(xiàn)了socket事件的處理,定時事件的處理;

ngx_http_limit_req_module模塊解析

ngx_http_limit_req_module模塊是對請求進(jìn)行限流,即限制某一時間段內(nèi)用戶的請求速率;且使用的是令牌桶算法;

3.1配置指令

ngx_http_limit_req_module模塊提供一下配置指令,供用戶配置限流策略

//每個配置指令主要包含兩個字段:名稱,解析配置的處理方法
static ngx_command_t ngx_http_limit_req_commands[] = {
 
 //一般用法:limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
 //$binary_remote_addr表示遠(yuǎn)程客戶端IP;
 //zone配置一個存儲空間(需要分配空間記錄每個客戶端的訪問速率,超時空間限制使用lru算法淘汰;注意此空間是在共享內(nèi)存分配的,所有worker進(jìn)程都能訪問)
 //rate表示限制速率,此例為1qps
 { ngx_string("limit_req_zone"),
  ngx_http_limit_req_zone,
  },
 
 //用法:limit_req zone=one burst=5 nodelay;
 //zone指定使用哪一個共享空間
 //超出此速率的請求是直接丟棄嗎?burst配置用于處理突發(fā)流量,表示最大排隊請求數(shù)目,當(dāng)客戶端請求速率超過限流速率時,請求會排隊等待;而超出burst的才會被直接拒絕;
 //nodelay必須與burst一起使用;此時排隊等待的請求會被優(yōu)先處理;否則假如這些請求依然按照限流速度處理,可能等到服務(wù)器處理完成后,客戶端早已超時
 { ngx_string("limit_req"),
  ngx_http_limit_req,
  },
 
 //當(dāng)請求被限流時,日志記錄級別;用法:limit_req_log_level info | notice | warn | error;
 { ngx_string("limit_req_log_level"),
  ngx_conf_set_enum_slot,
  },
 
 //當(dāng)請求被限流時,給客戶端返回的狀態(tài)碼;用法:limit_req_status 503
 { ngx_string("limit_req_status"),
  ngx_conf_set_num_slot,
 },
};

注意:$binary_remote_addr是nginx提供的變量,用戶在配置文件中可以直接使用;nginx還提供了許多變量,在ngx_http_variable.c文件中查找ngx_http_core_variables數(shù)組即可:

static ngx_http_variable_t ngx_http_core_variables[] = {
 
 { ngx_string("http_host"), NULL, ngx_http_variable_header,
  offsetof(ngx_http_request_t, headers_in.host), 0, 0 },
 
 { ngx_string("http_user_agent"), NULL, ngx_http_variable_header,
  offsetof(ngx_http_request_t, headers_in.user_agent), 0, 0 },
 …………
}

3.2源碼解析

ngx_http_limit_req_module在postconfiguration過程會注冊ngx_http_limit_req_handler方法到HTTP處理的NGX_HTTP_PREACCESS_PHASE階段;

ngx_http_limit_req_handler會執(zhí)行漏桶算法,判斷是否超出配置的限流速率,從而進(jìn)行丟棄或者排隊或者通過;

當(dāng)用戶第一次請求時,會新增一條記錄(主要記錄訪問計數(shù)、訪問時間),以客戶端IP地址(配置$binary_remote_addr)的hash值作為key存儲在紅黑樹中(快速查找),同時存儲在LRU隊列中(存儲空間不夠時,淘汰記錄,每次都是從尾部刪除);當(dāng)用戶再次請求時,會從紅黑樹中查找這條記錄并更新,同時移動記錄到LRU隊列首部;

3.2.1數(shù)據(jù)結(jié)構(gòu)

limit_req_zone配置限流算法所需的存儲空間(名稱及大?。蘖魉俣?,限流變量(客戶端IP等),結(jié)構(gòu)如下:

typedef struct {
 ngx_http_limit_req_shctx_t *sh;
 ngx_slab_pool_t    *shpool;//內(nèi)存池
 ngx_uint_t     rate; //限流速度(qps乘以1000存儲)
 ngx_int_t     index; //變量索引(nginx提供了一系列變量,用戶配置的限流變量索引)
 ngx_str_t     var; //限流變量名稱
 ngx_http_limit_req_node_t *node;
} ngx_http_limit_req_ctx_t;
 
//同時會初始化共享存儲空間
struct ngx_shm_zone_s {
 void      *data; //data指向ngx_http_limit_req_ctx_t結(jié)構(gòu)
 ngx_shm_t     shm; //共享空間
 ngx_shm_zone_init_pt  init; //初始化方法函數(shù)指針
 void      *tag; //指向ngx_http_limit_req_module結(jié)構(gòu)體
};

limit_req配置限流使用的存儲空間,排隊隊列大小,是否緊急處理,結(jié)構(gòu)如下:

typedef struct {
 ngx_shm_zone_t    *shm_zone; //共享存儲空間
  
 ngx_uint_t     burst;  //隊列大小
 ngx_uint_t     nodelay; //有請求排隊時是否緊急處理,與burst配合使用(如果配置,則會緊急處理排隊請求,否則依然按照限流速度處理)
} ngx_http_limit_req_limit_t;

前面說過用戶訪問記錄會同時存儲在紅黑樹與LRU隊列中,結(jié)構(gòu)如下:

//記錄結(jié)構(gòu)體
typedef struct {
 u_char      color;
 u_char      dummy;
 u_short      len; //數(shù)據(jù)長度
 ngx_queue_t     queue; 
 ngx_msec_t     last; //上次訪問時間
  
 ngx_uint_t     excess; //當(dāng)前剩余待處理的請求數(shù)(nginx用此實現(xiàn)令牌桶限流算法)
 ngx_uint_t     count; //此類記錄請求的總數(shù)
 u_char      data[1];//數(shù)據(jù)內(nèi)容(先按照key(hash值)查找,再比較數(shù)據(jù)內(nèi)容是否相等)
} ngx_http_limit_req_node_t;
 
//紅黑樹節(jié)點,key為用戶配置限流變量的hash值;
struct ngx_rbtree_node_s {
 ngx_rbtree_key_t  key;
 ngx_rbtree_node_t  *left;
 ngx_rbtree_node_t  *right;
 ngx_rbtree_node_t  *parent;
 u_char     color;
 u_char     data;
};
 
 
typedef struct {
 ngx_rbtree_t     rbtree; //紅黑樹
 ngx_rbtree_node_t    sentinel; //NIL節(jié)點
 ngx_queue_t     queue; //LRU隊列
} ngx_http_limit_req_shctx_t;
 
//隊列只有prev和next指針
struct ngx_queue_s {
 ngx_queue_t *prev;
 ngx_queue_t *next;
};

思考1:ngx_http_limit_req_node_t記錄通過prev和next指針形成雙向鏈表,實現(xiàn)LRU隊列;最新訪問的節(jié)點總會被插入鏈表頭部,淘汰時從尾部刪除節(jié)點;

ngx_http_limit_req_ctx_t *ctx;
ngx_queue_t    *q;
 
q = ngx_queue_last(&ctx->sh->queue);
 
lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);//此方法由ngx_queue_t獲取ngx_http_limit_req_node_t結(jié)構(gòu)首地址,實現(xiàn)如下:
 
#define ngx_queue_data(q, type, link) (type *) ((u_char *) q - offsetof(type, link)) //queue字段地址減去其在結(jié)構(gòu)體中偏移,為結(jié)構(gòu)體首地址

思考2:限流算法首先使用key查找紅黑樹節(jié)點,從而找到對應(yīng)的記錄,紅黑樹節(jié)點如何與記錄ngx_http_limit_req_node_t結(jié)構(gòu)關(guān)聯(lián)起來呢?在ngx_http_limit_req_module模塊可以找到以代碼:

size = offsetof(ngx_rbtree_node_t, color) //新建記錄分配內(nèi)存,計算所需空間大小
  + offsetof(ngx_http_limit_req_node_t, data)
  + len;
 
node = ngx_slab_alloc_locked(ctx->shpool, size);
 
node->key = hash;
 
lr = (ngx_http_limit_req_node_t *) &node->color; //color為u_char類型,為什么能強制轉(zhuǎn)換為ngx_http_limit_req_node_t指針類型呢?
 
lr->len = (u_char) len;
lr->excess = 0;
 
ngx_memcpy(lr->data, data, len);
 
ngx_rbtree_insert(&ctx->sh->rbtree, node);
 
ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);

通過分析上面代碼,ngx_rbtree_node_s結(jié)構(gòu)體的color與data字段其實是無意義的,結(jié)構(gòu)體的生命形式與最終存儲形式是不同的,nginx最終使用以下存儲形式存儲每條記錄;

3.2.2限流算法

上面提到在postconfiguration過程會注冊ngx_http_limit_req_handler方法到HTTP處理的NGX_HTTP_PREACCESS_PHASE階段;

因此在處理HTTP請求時,會執(zhí)行ngx_http_limit_req_handler方法判斷是否需要限流;

3.2.2.1漏桶算法實現(xiàn)

用戶可能同時配置若干限流,因此對于HTTP請求,nginx需要遍歷所有限流策略,判斷是否需要限流;

ngx_http_limit_req_lookup方法實現(xiàn)了漏桶算法,方法返回3種結(jié)果:

  • NGX_BUSY:請求速率超出限流配置,拒絕請求;
  • NGX_AGAIN:請求通過了當(dāng)前限流策略校驗,繼續(xù)校驗下一個限流策略;
  • NGX_OK:請求已經(jīng)通過了所有限流策略的校驗,可以執(zhí)行下一階段;
  • NGX_ERROR:出錯
//limit,限流策略;hash,記錄key的hash值;data,記錄key的數(shù)據(jù)內(nèi)容;len,記錄key的數(shù)據(jù)長度;ep,待處理請求數(shù)目;account,是否是最后一條限流策略
static ngx_int_t ngx_http_limit_req_lookup(ngx_http_limit_req_limit_t *limit, ngx_uint_t hash, u_char *data, size_t len, ngx_uint_t *ep, ngx_uint_t account)
{
 //紅黑樹查找指定界定
 while (node != sentinel) {
 
  if (hash < node->key) {
   node = node->left;
   continue;
  }
 
  if (hash > node->key) {
   node = node->right;
   continue;
  }
 
  //hash值相等,比較數(shù)據(jù)是否相等
  lr = (ngx_http_limit_req_node_t *) &node->color;
 
  rc = ngx_memn2cmp(data, lr->data, len, (size_t) lr->len);
  //查找到
  if (rc == 0) {
   ngx_queue_remove(&lr->queue);
   ngx_queue_insert_head(&ctx->sh->queue, &lr->queue); //將記錄移動到LRU隊列頭部
  
   ms = (ngx_msec_int_t) (now - lr->last); //當(dāng)前時間減去上次訪問時間
 
   excess = lr->excess - ctx->rate * ngx_abs(ms) / 1000 + 1000; //待處理請求書-限流速率*時間段+1個請求(速率,請求數(shù)等都乘以1000了)
 
   if (excess < 0) {
    excess = 0;
   }
 
   *ep = excess;
 
   //待處理數(shù)目超過burst(等待隊列大?。?,返回NGX_BUSY拒絕請求(沒有配置burst時,值為0)
   if ((ngx_uint_t) excess > limit->burst) {
    return NGX_BUSY;
   }
 
   if (account) { //如果是最后一條限流策略,則更新上次訪問時間,待處理請求數(shù)目,返回NGX_OK
    lr->excess = excess;
    lr->last = now;
    return NGX_OK;
   }
   //訪問次數(shù)遞增
   lr->count++;
 
   ctx->node = lr;
 
   return NGX_AGAIN; //非最后一條限流策略,返回NGX_AGAIN,繼續(xù)校驗下一條限流策略
  }
 
  node = (rc < 0) ? node->left : node->right;
 }
 
 //假如沒有查找到節(jié)點,需要新建一條記錄
 *ep = 0;
 //存儲空間大小計算方法參照3.2.1節(jié)數(shù)據(jù)結(jié)構(gòu)
 size = offsetof(ngx_rbtree_node_t, color)
   + offsetof(ngx_http_limit_req_node_t, data)
   + len;
 //嘗試淘汰記錄(LRU)
 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_ERROR
   return NGX_ERROR;
  }
 }
 
 node->key = hash; //賦值
 lr = (ngx_http_limit_req_node_t *) &node->color;
 lr->len = (u_char) len;
 lr->excess = 0;
 ngx_memcpy(lr->data, data, len);
 
 ngx_rbtree_insert(&ctx->sh->rbtree, node); //插入記錄到紅黑樹與LRU隊列
 ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
 
 if (account) { //如果是最后一條限流策略,則更新上次訪問時間,待處理請求數(shù)目,返回NGX_OK
  lr->last = now;
  lr->count = 0;
  return NGX_OK;
 }
 
 lr->last = 0;
 lr->count = 1;
 
 ctx->node = lr;
 
 return NGX_AGAIN; //非最后一條限流策略,返回NGX_AGAIN,繼續(xù)校驗下一條限流策略
  
}

舉個例子,假如burst配置為0,待處理請求數(shù)初始為excess;令牌產(chǎn)生周期為T;如下圖所示

3.2.2.2LRU淘汰策略

上一節(jié)叩痛算法中,會執(zhí)行ngx_http_limit_req_expire淘汰一條記錄,每次都是從LRU隊列末尾刪除;

第二個參數(shù)n,當(dāng)n==0時,強制刪除末尾一條記錄,之后再嘗試刪除一條或兩條記錄;n==1時,會嘗試刪除一條或兩條記錄;代碼實現(xiàn)如下:

static void ngx_http_limit_req_expire(ngx_http_limit_req_ctx_t *ctx, ngx_uint_t n)
{
 //最多刪除3條記錄
 while (n < 3) {
  //尾部節(jié)點
  q = ngx_queue_last(&ctx->sh->queue);
  //獲取記錄
  lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);
   
  //注意:當(dāng)為0時,無法進(jìn)入if代碼塊,因此一定會刪除尾部節(jié)點;當(dāng)n不為0時,進(jìn)入if代碼塊,校驗是否可以刪除
  if (n++ != 0) {
 
   ms = (ngx_msec_int_t) (now - lr->last);
   ms = ngx_abs(ms);
   //短時間內(nèi)被訪問,不能刪除,直接返回
   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);
 }
}

3.2.2.3 burst實現(xiàn)

burst是為了應(yīng)對突發(fā)流量的,偶然間的突發(fā)流量到達(dá)時,應(yīng)該允許服務(wù)端多處理一些請求才行;

當(dāng)burst為0時,請求只要超出限流速率就會被拒絕;當(dāng)burst大于0時,超出限流速率的請求會被排隊等待 處理,而不是直接拒絕;

排隊過程如何實現(xiàn)?而且nginx還需要定時去處理排隊中的請求;

2.2小節(jié)提到事件都有一個定時器,nginx是通過事件與定時器配合實現(xiàn)請求的排隊與定時處理;

ngx_http_limit_req_handler方法有下面的代碼:

//計算當(dāng)前請求還需要排隊多久才能處理
delay = ngx_http_limit_req_account(limits, n, &excess, &limit);

//添加可讀事件
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; //可寫事件處理函數(shù)
ngx_add_timer(r->connection->write, delay); //可寫事件添加定時器(超時之前是不能往客戶端返回的)

計算delay的方法很簡單,就是遍歷所有的限流策略,計算處理完所有待處理請求需要的時間,返回最大值;

if (limits[n].nodelay) { //配置了nodelay時,請求不會被延時處理,delay為0
 continue;
}
 
delay = excess * 1000 / ctx->rate;
 
if (delay > max_delay) {
 max_delay = delay;
 *ep = excess;
 *limit = &limits[n];
}

簡單看看可寫事件處理函數(shù)ngx_http_limit_req_delay的實現(xiàn)

static void ngx_http_limit_req_delay(ngx_http_request_t *r)
{
 
 wev = r->connection->write;
 
 if (!wev->timedout) { //沒有超時不會處理
 
  if (ngx_handle_write_event(wev, 0) != NGX_OK) {
   ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
  }
 
  return;
 }
 
 wev->timedout = 0;
 
 r->read_event_handler = ngx_http_block_reading;
 r->write_event_handler = ngx_http_core_run_phases;
 
 ngx_http_core_run_phases(r); //超時了,繼續(xù)處理HTTP請求
}

4.實戰(zhàn)

4.1測試普通限流

1)配置nginx限流速率為1qps,針對客戶端IP地址限流(返回狀態(tài)碼默認(rèn)為503),如下:

http{
 limit_req_zone $binary_remote_addr zone=test:10m rate=1r/s;
 
 server {
  listen  80;
  server_name localhost;
  location / {
   limit_req zone=test;
   root html;
   index index.html index.htm;
  }
}

2)連續(xù)并發(fā)發(fā)起若干請求;3)查看服務(wù)端access日志,可以看到22秒連續(xù)到達(dá)3個請求,只處理1個請求;23秒到達(dá)兩個請求,第一個請求處理,第二個請求被拒絕

xx.xx.xx.xxx - - [22/Sep/2018:23:33:22 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:33:22 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:33:22 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:33:23 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:33:23 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"

4.2測試burst

1)限速1qps時,超過請求會被直接拒絕,為了應(yīng)對突發(fā)流量,應(yīng)該允許請求被排隊處理;因此配置burst=5,即最多允許5個請求排隊等待處理;

http{
 limit_req_zone $binary_remote_addr zone=test:10m rate=1r/s;
 
 server {
  listen  80;
  server_name localhost;
  location / {
   limit_req zone=test burst=5;
   root html;
   index index.html index.htm;
  }
}

2)使用ab并發(fā)發(fā)起10個請求,ab -n 10 -c 10 http://xxxxx;

3)查看服務(wù)端access日志;根據(jù)日志顯示第一個請求被處理,2到5四個請求拒絕,6到10五個請求被處理;為什么會是這樣的結(jié)果呢?

查看ngx_http_log_module,注冊handler到NGX_HTTP_LOG_PHASE階段(HTTP請求處理最后一個階段);

因此實際情況應(yīng)該是這樣的:10個請求同時到達(dá),第一個請求到達(dá)直接被處理,第2到6個請求到達(dá),排隊延遲處理(每秒處理一個);第7到10個請求被直接拒絕,因此先打印access日志;

第2到6個請求米誒秒處理一個,處理完成打印access日志,即49到53秒每秒處理一個;

xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:48 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:49 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:50 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:51 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:52 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [22/Sep/2018:23:41:53 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"

4)ab統(tǒng)計的響應(yīng)時間見下面,最小響應(yīng)時間87ms,最大響應(yīng)時間5128ms,平均響應(yīng)時間為1609ms:

    min mean[+/-sd] median max
Connect:  41 44 1.7  44  46
Processing: 46 1566 1916.6 1093 5084
Waiting:  46 1565 1916.7 1092 5084
Total:   87 1609 1916.2 1135 5128

4.3測試nodelay

1)4.2顯示,配置burst后,雖然突發(fā)請求會被排隊處理,但是響應(yīng)時間過長,客戶端可能早已超時;因此添加配置nodelay,使得nginx緊急處理等待請求,以減小響應(yīng)時間:

http{
 limit_req_zone $binary_remote_addr zone=test:10m rate=1r/s;
 
 server {
  listen  80;
  server_name localhost;
  location / {
   limit_req zone=test burst=5 nodelay;
   root html;
   index index.html index.htm;
  }
}

2)使用ab并發(fā)發(fā)起10個請求,ab -n 10 -c 10 http://xxxx/;

3)查看服務(wù)端access日志;第一個請求直接處理,第2到6個五個請求排隊處理(配置nodelay,nginx緊急處理),第7到10四個請求被拒絕

xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 200 612 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"
xx.xx.xx.xxx - - [23/Sep/2018:00:04:47 +0800] "GET / HTTP/1.0" 503 537 "-" "ApacheBench/2.3"

4)ab統(tǒng)計的響應(yīng)時間見下面,最小響應(yīng)時間85ms,最大響應(yīng)時間92ms,平均響應(yīng)時間為88ms:

    min mean[+/-sd] median max
Connect:  42 43 0.5  43  43
Processing: 43 46 2.4  47  49
Waiting:  42 45 2.5  46  49
Total:   85 88 2.8  90  92

總結(jié)

本文首先分析常用限流算法(漏桶算法與令牌桶算法),并簡單介紹nginx處理HTTP請求的過程,nginx定時事件實現(xiàn);然后詳細(xì)分析ngx_http_limit_req_module模塊的基本數(shù)據(jù)結(jié)構(gòu),及其限流過程;并以實例幫助讀者體會nginx限流的配置及結(jié)果。至于另一個模塊ngx_http_limit_conn_module是針對鏈接數(shù)的限流,比較容易理解,在此就不做詳細(xì)介紹。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

標(biāo)簽:紅河 股票 烏海 四川 臨沂 鎮(zhèn)江 岳陽

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Nginx源碼研究之nginx限流模塊詳解》,本文關(guān)鍵詞  Nginx,源碼,研究,之,nginx,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《Nginx源碼研究之nginx限流模塊詳解》相關(guān)的同類信息!
  • 本頁收集關(guān)于Nginx源碼研究之nginx限流模塊詳解的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章