nginx長連接keepalive與pipeline使用及說明
tcp與http都支持keepalive機制,但兩者是不同的。先看下tcp的keepalive機制。當客戶端與服務器建立了tcp連接后,如果客戶端一直不發(fā)送數(shù)據(jù),或者隔很長時間才發(fā)送一次數(shù)據(jù)。當連接很久沒有數(shù)據(jù)報文傳輸時,服務器如何去確定對方還在線。到底是掉線了還是確實沒有數(shù)據(jù)傳輸,連接還需不需要保持,這種情況在TCP協(xié)議設(shè)計中是需要考慮的。TCP協(xié)議通過一種巧妙的方式去解決這個問題,當超過一段時間之后,TCP自動發(fā)送一個數(shù)據(jù)為空的報文給對方,如果對方回應了這個報文,說明對方還在線,連接可以繼續(xù)保持,如果對方?jīng)]有報文返回并且重試了多次之后則認為連接丟失,沒有必要保持連接。這個過程相當于服務器向客戶端發(fā)送心跳包,確認客戶端是否還在線。
而http的keepalive機制為:通??蛻舳藶g覽器要展現(xiàn)一張完整的頁面需要很多個請求才能完成,如圖片,js,CSS等。如果每一個HTTP請求都需要新建并斷開一個TCP,這個開銷是完全沒有必要的。開啟HTTP Keep-Alive之后,能復用已有的TCP鏈接, 當一個請求已經(jīng)響應完畢,服務器端沒有立即關(guān)閉TCP連接,而是等待一段時間繼續(xù)接收瀏覽器可能發(fā)送過來的第二個請求,通常瀏覽器在第一個請求返回之后會立即發(fā)送第二個請求。因此在一個tcp連接上可以存在多個http請求,當然這個如果客戶端一直不發(fā)送新的http請求,超過一段時間后,nginx服務器還是會關(guān)閉這個TCP長連接。
開啟keepalive功能,可以減少time-wait套接字的個數(shù)
一、keealive的請求頭部解析
在http1.0協(xié)議里,需要客戶端發(fā)送connection: keep-alive請求頭來實現(xiàn)與服務器之間的長連接,否則長連接默認是關(guān)閉的。 http1.1默認支持keepalive,也就是說即使客戶端沒有發(fā)送connection:keep_alive請求頭部,服務器也會設(shè)置keepalive屬性,然后等待客戶端下一次請求。但通過請求頭connection: close可明確要求不進行長連接保持。先看下如何設(shè)置keepalive。
//http請求頭部常用頭部對應的處理函數(shù)
ngx_http_header_t ngx_http_headers_in[] =
{
{ ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),ngx_http_process_connection },
};//處理http請求頭部的connection字段
static ngx_int_t ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h,
ngx_uint_t offset)
{
if (ngx_strcasestrn(h->value.data, "close", 5 - 1))
{
//長連接關(guān)閉
r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE;
}
else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1))
{
//保持長連接
r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE;
}
return NGX_OK;
}在解析http頭部的connection字段后,將會設(shè)置TCP的連接類型,是打開長連接還是關(guān)閉長連接。然后會設(shè)置到請求對象的keepalive中。對于keepalive只用于服務器與客戶端保持TCP長連接,而nginx內(nèi)部操作,例如內(nèi)部跳轉(zhuǎn), 命名location跳轉(zhuǎn)、子請求等是不需要用到keepalive的。
//調(diào)用各個http模塊協(xié)同處理這個請求
void ngx_http_handler(ngx_http_request_t *r)
{
//不需要進行內(nèi)部跳轉(zhuǎn)。keepalive機制是在客戶端和nginx服務器之間才需要關(guān)注。對于內(nèi)部跳轉(zhuǎn)則不會用到
//keepalive機制
if (!r->internal)
{
switch (r->headers_in.connection_type)
{
case 0:
r->keepalive = (r->http_version > NGX_HTTP_VERSION_10);//http1.1版本默認開啟keepalive
break;
case NGX_HTTP_CONNECTION_CLOSE:
r->keepalive = 0; //關(guān)閉長連接
break;
case NGX_HTTP_CONNECTION_KEEP_ALIVE:
r->keepalive = 1; //打開長連接
break;
}
}
}二、keepalive的設(shè)置
當一個http請求完成后, 這個時候引用計數(shù)為0了,會釋放這個http請求,但到底要不要釋放tcp連接,是由keepalive機制與延遲關(guān)閉機制決定的。keepalive相比延遲關(guān)閉,優(yōu)先級更高。來看下ngx_http_finalize_connection函數(shù)的實現(xiàn)。
//釋放http請求與連接
static void ngx_http_finalize_connection(ngx_http_request_t *r)
{
//執(zhí)行到這里,引用計數(shù)為1,則要準備結(jié)束請求了
//keepalive為1表示請求需要釋放,但tcp連接還是用復用的
if (!ngx_terminate
&& !ngx_exiting
&& r->keepalive
&& clcf->keepalive_timeout > 0)
{
ngx_http_set_keepalive(r);
return;
}
}keepalive_timeout可以在nginx.conf配置文件中,通過keepalive_timeout指令設(shè)置,默認情況下超時時間就是75秒。如果超過這個時間都還沒有收到來自客戶端新的http請求,則會關(guān)閉這個tcp連接,keepalive功能也就結(jié)束了。ngx_http_set_keepalive函數(shù)則是正式開始keepalive的處理,函數(shù)有點長,實現(xiàn)了pipeline長連接與普通長連接。先來看下pipeline的實現(xiàn)。
三、pipeline處理
那么什么是pipeline呢?pipeline其實就是流水線作業(yè),它可以看作為keepalive的一種升華,因為pipeline也是基于長連接的,目的就是利用一個連接做多次請求。如果客戶端要提交多個請求,對于keepalive來說,那么第二個請求,必須要等到第一個請求的響應接收完全后,才能發(fā)起,這和TCP的停止等待協(xié)議是一樣的,得到兩個響應的時間至少為2*RTT。而對pipeline來說,客戶端不必等到第一個請求處理完后,就可以馬上發(fā)起第二個請求。得到兩個響應的時間可能能夠達到1*RTT。nginx是直接支持pipeline的,但是,nginx對pipeline中的多個請求的處理卻不是并行的,依然是一個請求接一個請求的處理,只是在處理第一個請求的時候,客戶端就可以發(fā)起第二個請求。這樣,nginx利用pipeline減少了處理完一個請求后,等待第二個請求的請求行與請求頭部的時間。其實nginx的做法很簡單,前面說到,nginx在讀取數(shù)據(jù)時,會將讀取的數(shù)據(jù)放到一個buffer里面,所以,如果nginx在處理完前一個請求后,如果發(fā)現(xiàn)buffer里面還有數(shù)據(jù),就認為剩下的數(shù)據(jù)是下一個請求的開始,然后接下來處理下一個請求,否則就設(shè)置keepalive。
來看下nginx服務器是如何處理pipeline的?
//設(shè)置keepalive過程
static void ngx_http_set_keepalive(ngx_http_request_t *r)
{
//在接收到來自客戶端的連接請求時,同時也接收到了同一個tcp連接上的第2個http請求頭部。
//因此在處理完第一個請求時,pos不等于last,說明接收到了第二個http請求頭部,接下來要立馬處理第二個請求
if (b->pos < b->last)
{
//在接收來自客戶端的請求行、或者請求頭部時。如果連接對象的buffer緩沖區(qū)不能夠存放所有的請求行,請求頭。
//則http請求對象自己會開辟新的空間。因此在請求結(jié)束時,需要把請求對象開辟的空間加入到空閑表中。
//這樣在這個連接上有新的http請求到來時,可以復用上一個請求對象開辟的空間
if (b != c->buffer)
{
if (hc->free == NULL)
{
hc->free = ngx_palloc(c->pool, cscf->large_client_header_buffers.num * sizeof(ngx_buf_t *));
}
//將在用表移動到空閑表
for (i = 0; i < hc->nbusy - 1; i++)
{
f = hc->busy[i];
hc->free[hc->nfree++] = f;
f->pos = f->start;
f->last = f->start;
}
hc->busy[0] = b;
hc->nbusy = 1;
}
}
}(1) nginx服務器是怎么知道需要進行pipeline處理呢? 條件就是這個b->pos < b->last。 多個http請求可以復用同一個tcp連接,客戶端瀏覽器在發(fā)出第一個http請求時,可以不需要等待收到服務器的響應,可以立馬發(fā)出第二個http請求。在這種請求下,nginx服務器收到第一個http請求時,是有可能也接收到了第二個http請求的頭部信息,并保存到http請求對象的header_in緩沖區(qū)中。看下這個接收過程, 函數(shù)只負責從內(nèi)核讀取數(shù)據(jù)到緩沖區(qū),并沒有限制只讀取第一個http請求的數(shù)據(jù),如果有第二個http請求,也會把第二個http請求頭部也讀取到緩沖區(qū)。
static ssize_t ngx_http_read_request_header(ngx_http_request_t *r)
{
//事件就緒,也就是從epoll_wait中返回后,從內(nèi)核緩沖區(qū)中讀取內(nèi)容到應用層緩沖區(qū)header_in
if (rev->ready)
{
//ngx_unix_recv
n = c->recv(c, r->header_in->last, r->header_in->end - r->header_in->last);
}
}那這個b != c->buffer又怎么理解呢? 默認情況下http請求結(jié)構(gòu)的header_in緩沖區(qū)是等于連接對象的buffer緩沖區(qū)的。 看下面這個函數(shù)就知道了。剛建立http請求時,兩個緩沖區(qū)都指向同一個內(nèi)存空間。
//首次建立tcp連接后,讀事件的回調(diào),用于創(chuàng)建一個ngx_http_request_t對象
static void ngx_http_init_request(ngx_event_t *rev)
{
//剛建立的請求,默認情況下這兩個緩沖區(qū)是相等的
if (r->header_in == NULL)
{
r->header_in = c->buffer;
}
}那什么情況下http請求結(jié)構(gòu)的header_in緩沖區(qū)會不等于連接對象的buffer緩沖區(qū)呢? nginx服務器在接收到來自客戶端的http請求行或者請求頭部時,如果接受緩沖區(qū)不能夠存放請求行或者請求頭時,是會開辟一個新的緩沖區(qū),并把這個緩沖區(qū)加入到在用緩沖區(qū)數(shù)組busy中。通常情況下nginx服務器的默認緩沖區(qū)是足夠存放一個http請求行或者請求頭部的, 之所以緩沖區(qū)不夠,大部分情況下是接收到了多個http請求頭部。這種情況下將會執(zhí)行pipeline處理
//開辟一個大的緩沖區(qū),并把舊緩沖區(qū)的數(shù)據(jù)拷貝到新緩沖區(qū)中
//解析請求行時,request_line = 1, 解析請求頭時,request_line = 0
static ngx_int_t ngx_http_alloc_large_header_buffer(ngx_http_request_t *r, ngx_uint_t request_line)
{
/* 檢查給該請求分配的請求頭緩沖區(qū)個數(shù)是否已經(jīng)超過限制,默認最大個數(shù)為4個 */
if (hc->busy == NULL)
{
hc->busy = ngx_palloc(r->connection->pool, cscf->large_client_header_buffers.num * sizeof(ngx_buf_t *));
}
/* 如果還沒有達到最大分配數(shù)量,則分配一個新的大緩沖區(qū) */
b = ngx_create_temp_buf(r->connection->pool, cscf->large_client_header_buffers.size);
/* 將從空閑隊列取得的或者新分配的緩沖區(qū)加入已使用隊列 */
hc->busy[hc->nbusy++] = b;
//header_in指向這個新的緩沖區(qū), 連接對象的緩沖區(qū)仍然為舊的緩沖區(qū),大小沒有改變
r->header_in = b;
}為什么要把busy這個在用緩沖區(qū)數(shù)據(jù)移動到空閑緩沖區(qū)數(shù)組呢? 為什么不直接釋放這些空間呢? 還是因為pipeline的原因,因為除了當前要結(jié)束的這個http請求外,緩沖區(qū)中還存放了其它的http請求。當這個http請求結(jié)束時,可以立即處理剩余的http請求。而如果不是pipeline,則會關(guān)閉這些緩沖區(qū)的, 這種情況下是一個普通的TCP長連接,客戶端什么時候會再次發(fā)送http請求,以及是否真的還會在發(fā)送http請求是不可知的, nginx將會釋放這些資源。
(2)接下來要釋放一個http請求了,但請求本身這個對象沒有釋放, 這樣在這個tcp連接上的已經(jīng)接收到的其它請求可以復用這個http請求對象。同時也會設(shè)置keepalive的超時時間,超過這個時間后會真正關(guān)閉這個tcp連接。
//設(shè)置keepalive過程
static void ngx_http_set_keepalive(ngx_http_request_t *r)
{
//釋放請求的內(nèi)容空間
ngx_http_free_request(r, 0);
//將讀事件注冊到紅黑樹實現(xiàn)的定時器中,超時時間由keepalive_timeout命令設(shè)置,默認為75秒。
//如果超時時間到后都還沒有再收到來自客戶端的http請求,則會關(guān)閉連接。
ngx_add_timer(rev, clcf->keepalive_timeout);
ngx_handle_read_event(rev, 0);
//接收來自客戶端請求時,還不需要向客戶端寫入數(shù)據(jù),因此把寫回調(diào)設(shè)置為不做任何事情
wev = c->write;
wev->handler = ngx_http_empty_handler;
}(3)在pipeline這種情況下,當前http請求已經(jīng)結(jié)束了。那nginx如何處理這個tcp連接上的其它http請求呢? nginx服務器將會設(shè)置讀事件ngx_event_t的接收回調(diào)handler為:ngx_http_init_request,并立馬把讀事件加入到post隊列中。因此這個讀事件就會被立即調(diào)用,從而開始處理這個tcp連接上的其它http請求。
static void ngx_http_set_keepalive(ngx_http_request_t *r)
{
//該請求結(jié)束了,設(shè)置接收回調(diào)為ngx_http_init_request。在這個tcp連接上的下一個請求到來時,
//重新開始對一個新請求進行處理
if (b->pos < b->last)
{
//設(shè)置為串行請求
hc->pipeline = 1;
//設(shè)置讀事件的回調(diào),并加入到post事件隊列
rev->handler = ngx_http_init_request;
ngx_post_event(rev, &ngx_posted_events);
return;
}
}看下ngx_http_init_request這個函數(shù)怎么在這個tcp連接上重新創(chuàng)建一個http請求。對于pipeline,是不會創(chuàng)建一個新的http請求的,而是復用已經(jīng)結(jié)束的http請求。也可以看出不管是在這個tcp連接上新建立的http請求,還是當前請求結(jié)束后重新建立了一個新的http請求,都會調(diào)用ngx_http_init_request這個函數(shù)進行處理。
static void ngx_http_init_request(ngx_event_t *rev)
{
r = hc->request;
if (r)
{
ngx_memzero(r, sizeof(ngx_http_request_t));
//串行請求時值為1,表示舊請求結(jié)束后,內(nèi)部資源已經(jīng)釋放了,但請求本身沒有釋放。
//這樣的話,在這個tcp連接上的新請求可以使用舊請求的對象。
//配和ngx_http_set_keepalive函數(shù)就理解這部分的實現(xiàn)了
r->pipeline = hc->pipeline;
//串行請求時,在用緩沖區(qū)加入到了空閑緩沖區(qū)數(shù)組中。配和ngx_http_set_keepalive函數(shù)幫助理解
if (hc->nbusy)
{
r->header_in = hc->busy[0];
}
}
}需要注意的是pipeline只支持冪等請求,也就是并發(fā)請求之間沒有依賴關(guān)系,不管執(zhí)行多少次結(jié)果都應該是一樣的。例如在第二個請求需要等待第一個請求的響應從而決定一些行為時,這種場景下就不能使用pipeline。第一個請求添加用戶信息,第二個請求更新用戶信息,這種場景下就不能使用pipeline。當然這需要由客戶端自己來保證,并發(fā)發(fā)送多個冪等性的請求。
到此對pipeline這種長連接的處理過程已經(jīng)分析完成了,下面將分析下非pipeline情況下的長連接的處理過程。暫且稱非pipeline的長連接為普通長連接吧!
四、普通長連接處理
如果不是pipeline這種串行請求,因此會盡可能的釋放該請求空間。因為這個tcp連接上什么時候會有客戶端發(fā)來新的http請求,以及是否真的會有新的http請求是不確定的。nginx秉承一個能盡量減少資源占用就減少資源的原則,會把這個http請求內(nèi)部開辟的資源給釋放,同時也把請求對象本身也真正的釋放, 但tcp連接還是沒有關(guān)閉,等超時沒有收到來自客戶端的http請求時在關(guān)閉。以此同時將會設(shè)置讀事件ngx_event_t的接收回調(diào)handler為:ngx_http_keepalive_handler,用來處理在這個tcp連接上的其它http請求(這里不像pipeline, 當前http請求結(jié)束了,但此時這個tcp連接上并沒有其它的http請求存在)。
static void ngx_http_set_keepalive(ngx_http_request_t *r)
{
//釋放資源
if (hc->busy)
{
for (i = 0; i < hc->nbusy; i++)
{
ngx_pfree(c->pool, hc->busy[i]->start);
hc->busy[i] = NULL;
}
}
//設(shè)置讀事件回調(diào),用于處理來自客戶端的新的http請求
rev->handler = ngx_http_keepalive_handler;
}現(xiàn)在來看是ngx_http_keepalive_handler這個函數(shù)的處理過程。函數(shù)內(nèi)部會開辟一些必要的緩沖區(qū)外,最終還是回調(diào)用ngx_http_init_request這個函數(shù)重新開始一個新的http請求。
//http請求關(guān)閉后,tcp連接并沒有關(guān)閉。這個函數(shù)用于在這個tcp連接上接收新的http請求
static void ngx_http_keepalive_handler(ngx_event_t *rev)
{
//開辟接收請求行、請求頭緩沖區(qū)。因為非pipeline情況下,釋放之前的一個請求時,
//也把連接對象的buffer緩沖區(qū)也給釋放了。因此新的請求到來時,需要重新開辟空間
b->pos = ngx_palloc(c->pool, size);
//調(diào)用這個函數(shù),重新開始一個新的http請求處理
ngx_http_init_request(rev);
}可以看出,在同一個tcp連接下,不管是第一次建立的請求,還是之后重新建立的請求,最終都會調(diào)用ngx_http_init_request這個函數(shù)。由這個函數(shù)負責對請求的初始化操作。由此也可以知道,多個http請求是可以復用同一個tcp連接的。沒有必要客戶端發(fā)起一個http請求就建立一個tcp連接,太浪費資源了。到此keepalive機制與pipeline已經(jīng)分析完成了, 因為keepalive機制和在同一個tcp連接上重新建立一個新的http請求有關(guān),也與關(guān)閉一個http請求有關(guān),跨度比較大,看了也比較混亂。如果對http請求初始化過程還不是很清楚,可以參考前面的文章。下一篇文章將會對nginx的延遲關(guān)閉功能進行分析。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
ubuntu16.04下徹底卸載nginx的相關(guān)命令
nginx是一款自由的、開源的、高性能的HTTP服務器和反向代理服務器;這篇文章主要介紹了ubuntu16.04下徹底卸載nginx的相關(guān)命令,需要的朋友可以參考下2018-12-12
前端服務器部署Nginx?+docker?+?ubuntu的完整過程
Docker是一個開源的容器化平臺,可以讓你快速構(gòu)建、測試和部署應用程序,Nginx是一個高性能的Web服務器和反向代理服務器,常用于部署靜態(tài)網(wǎng)站、負載均衡等場景,這篇文章主要介紹了前端服務器部署Nginx?+docker?+?ubuntu的完整過程,需要的朋友可以參考下2025-11-11

