在nginx中connection就是對(duì)tcp連接的封裝,其中包括連接的socket,讀事件,寫(xiě)事件。利用nginx封裝的connection,我們可以很方便的使用nginx來(lái)處理與連接相關(guān)的事情,比如,建立連接,發(fā)送與接受數(shù)據(jù)等。而nginx中的http請(qǐng)求的處理就是建立在connection之上的,所以nginx不僅可以作為一個(gè)web服務(wù)器,也可以作為郵件服務(wù)器。當(dāng)然,利用nginx提供的connection,我們可以與任何后端服務(wù)打交道。

結(jié)合一個(gè)tcp連接的生命周期,我們看看nginx是如何處理一個(gè)連接的。首先,nginx在啟動(dòng)時(shí),會(huì)解析配置文件,得到需要監(jiān)聽(tīng)的端口與ip地址,然后在nginx的master進(jìn)程里面,先初始化好這個(gè)監(jiān)控的socket(創(chuàng)建socket,設(shè)置addrreuse等選項(xiàng),綁定到指定的ip地址端口,再listen),然后再fork出多個(gè)子進(jìn)程出來(lái),然后子進(jìn)程會(huì)競(jìng)爭(zhēng)accept新的連接。此時(shí),客戶端就可以向nginx發(fā)起連接了。當(dāng)客戶端與服務(wù)端通過(guò)三次握手建立好一個(gè)連接后,nginx的某一個(gè)子進(jìn)程會(huì)accept成功,得到這個(gè)建立好的連接的socket,然后創(chuàng)建nginx對(duì)連接的封裝,即ngx_connection_t結(jié)構(gòu)體。接著,設(shè)置讀寫(xiě)事件處理函數(shù)并添加讀寫(xiě)事件來(lái)與客戶端進(jìn)行數(shù)據(jù)的交換。最后,nginx或客戶端來(lái)主動(dòng)關(guān)掉連接,到此,一個(gè)連接就壽終正寢了。
當(dāng)然,nginx也是可以作為客戶端來(lái)請(qǐng)求其它server的數(shù)據(jù)的(如upstream模塊),此時(shí),與其它server創(chuàng)建的連接,也封裝在ngx_connection_t中。作為客戶端,nginx先獲取一個(gè)ngx_connection_t結(jié)構(gòu)體,然后創(chuàng)建socket,并設(shè)置socket的屬性( 比如非阻塞)。然后再通過(guò)添加讀寫(xiě)事件,調(diào)用connect/read/write來(lái)調(diào)用連接,最后關(guān)掉連接,并釋放ngx_connection_t。
在nginx中,每個(gè)進(jìn)程會(huì)有一個(gè)連接數(shù)的上限,這個(gè)上限與系統(tǒng)對(duì)fd的限制不一樣。在操作系統(tǒng)中,通過(guò)ulimit -n,我們可以得到一個(gè)進(jìn)程所能夠打開(kāi)的fd的數(shù),即nofile,因?yàn)槊總€(gè)socket連接會(huì)占用掉一個(gè)fd,所以這也會(huì)限制我們進(jìn)程的連接數(shù),當(dāng)然也會(huì)直接影響到我們程序所能支持的并發(fā)數(shù),當(dāng)fd用完后,再創(chuàng)建socket時(shí),就會(huì)失敗。nginx通過(guò)設(shè)置worker_connectons來(lái)設(shè)置每個(gè)進(jìn)程支持的連接數(shù)。如果該值大于nofile,那么實(shí)際的連接數(shù)是nofile,nginx會(huì)有警告。nginx在實(shí)現(xiàn)時(shí),是通過(guò)一個(gè)連接池來(lái)管理的,每個(gè)worker進(jìn)程都有一個(gè)獨(dú)立的連接池,連接池的大小是worker_connections。這里的連接池里面保存的其實(shí)不是真實(shí)的連接,它只是一個(gè)worker_connections大小的一個(gè)ngx_connection_t結(jié)構(gòu)的數(shù)組。并且,nginx會(huì)通過(guò)一個(gè)鏈表free_connections來(lái)保存所有的空閑ngx_connection_t,每次獲取一個(gè)連接時(shí),就從空閑連接鏈表中獲取一個(gè),用完后,再放回空閑連接鏈表里面。
在這里,很多人會(huì)誤解worker_connections這個(gè)參數(shù)的意思,認(rèn)為這個(gè)值就是nginx所能建立連接的值。其實(shí)不然,這個(gè)值是表示每個(gè)worker進(jìn)程所能建立連接的值,所以,一個(gè)nginx能建立的連接數(shù),應(yīng)該是worker_connections * worker_processes。當(dāng)然,這里說(shuō)的是連接數(shù),對(duì)于HTTP請(qǐng)求本地資源來(lái)說(shuō),能夠支持的并發(fā)數(shù)量是worker_connections * worker_processes,而如果是HTTP作為反向代理來(lái)說(shuō),并發(fā)數(shù)量應(yīng)該是worker_connections * worker_processes/2。因?yàn)樽鳛榉聪虼矸?wù)器,每個(gè)并發(fā)會(huì)建立與客戶端的連接和與后端服務(wù)的連接,會(huì)占用兩個(gè)連接。
那么,我們前面有說(shuō)過(guò)一個(gè)客戶端連接過(guò)來(lái)后,多個(gè)空閑的進(jìn)程,會(huì)競(jìng)爭(zhēng)這個(gè)連接,很容易看到,這種競(jìng)爭(zhēng)會(huì)導(dǎo)致不公平,如果某個(gè)進(jìn)程得到accept的機(jī)會(huì)比較多,它的空閑連接很快就用完了,如果不提前做一些控制,當(dāng)accept到一個(gè)新的tcp連接后,因?yàn)闊o(wú)法得到空閑連接,而且無(wú)法將此連接轉(zhuǎn)交給其它進(jìn)程,最終會(huì)導(dǎo)致此tcp連接得不到處理,就中止掉了。很顯然,這是不公平的,有的進(jìn)程有空余連接,卻沒(méi)有處理機(jī)會(huì),有的進(jìn)程因?yàn)闆](méi)有空余連接,卻人為地丟棄連接。那么,如何解決這個(gè)問(wèn)題呢?首先,nginx的處理得先打開(kāi)accept_mutex選項(xiàng),此時(shí),只有獲得了accept_mutex的進(jìn)程才會(huì)去添加accept事件,也就是說(shuō),nginx會(huì)控制進(jìn)程是否添加accept事件。nginx使用一個(gè)叫ngx_accept_disabled的變量來(lái)控制是否去競(jìng)爭(zhēng)accept_mutex鎖。在第一段代碼中,計(jì)算ngx_accept_disabled的值,這個(gè)值是nginx單進(jìn)程的所有連接總數(shù)的八分之一,減去剩下的空閑連接數(shù)量,得到的這個(gè)ngx_accept_disabled有一個(gè)規(guī)律,當(dāng)剩余連接數(shù)小于總連接數(shù)的八分之一時(shí),其值才大于0,而且剩余的連接數(shù)越小,這個(gè)值越大。再看第二段代碼,當(dāng)ngx_accept_disabled大于0時(shí),不會(huì)去嘗試獲取accept_mutex鎖,并且將ngx_accept_disabled減1,于是,每次執(zhí)行到此處時(shí),都會(huì)去減1,直到小于0。不去獲取accept_mutex鎖,就是等于讓出獲取連接的機(jī)會(huì),很顯然可以看出,當(dāng)空余連接越少時(shí),ngx_accept_disable越大,于是讓出的機(jī)會(huì)就越多,這樣其它進(jìn)程獲取鎖的機(jī)會(huì)也就越大。不去accept,自己的連接就控制下來(lái)了,其它進(jìn)程的連接池就會(huì)得到利用,這樣,nginx就控制了多進(jìn)程間連接的平衡了。
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n; if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; } if (ngx_accept_mutex_held) { flags |= NGX_POST_EVENTS; } else { if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) { timer = ngx_accept_mutex_delay; } } }
好了,連接就先介紹到這,本章的目的是介紹基本概念,知道在nginx中連接是個(gè)什么東西就行了,而且連接是屬于比較高級(jí)的用法,在后面的模塊開(kāi)發(fā)高級(jí)篇會(huì)有專(zhuān)門(mén)的章節(jié)來(lái)講解連接與事件的實(shí)現(xiàn)及使用。
Requests請(qǐng)求處理這節(jié)我們講request,在nginx中我們指的是http請(qǐng)求,具體到nginx中的數(shù)據(jù)結(jié)構(gòu)是ngx_http_request_t。ngx_http_request_t是對(duì)一個(gè)http請(qǐng)求的封裝。 我們知道,一個(gè)http請(qǐng)求,包含請(qǐng)求行、請(qǐng)求頭、請(qǐng)求體、響應(yīng)行、響應(yīng)頭、響應(yīng)體。
http請(qǐng)求是典型的請(qǐng)求-響應(yīng)類(lèi)型的的網(wǎng)絡(luò)協(xié)議,而http是文件協(xié)議,所以我們?cè)诜治稣?qǐng)求行與請(qǐng)求頭,以及輸出響應(yīng)行與響應(yīng)頭,往往是一行一行的進(jìn)行處理。如果我們自己來(lái)寫(xiě)一個(gè)http服務(wù)器,通常在一個(gè)連接建立好后,客戶端會(huì)發(fā)送請(qǐng)求過(guò)來(lái)。然后我們讀取一行數(shù)據(jù),分析出請(qǐng)求行中包含的method、uri、http_version信息。然后再一行一行處理請(qǐng)求頭,并根據(jù)請(qǐng)求method與請(qǐng)求頭的信息來(lái)決定是否有請(qǐng)求體以及請(qǐng)求體的長(zhǎng)度,然后再去讀取請(qǐng)求體。得到請(qǐng)求后,我們處理請(qǐng)求產(chǎn)生需要輸出的數(shù)據(jù),然后再生成響應(yīng)行,響應(yīng)頭以及響應(yīng)體。在將響應(yīng)發(fā)送給客戶端之后,一個(gè)完整的請(qǐng)求就處理完了。當(dāng)然這是最簡(jiǎn)單的webserver的處理方式,其實(shí)nginx也是這樣做的,只是有一些小小的區(qū)別,比如,當(dāng)請(qǐng)求頭讀取完成后,就開(kāi)始進(jìn)行請(qǐng)求的處理了。nginx通過(guò)ngx_http_request_t來(lái)保存解析請(qǐng)求與輸出響應(yīng)相關(guān)的數(shù)據(jù)。
那接下來(lái),簡(jiǎn)要講講nginx是如何處理一個(gè)完整的請(qǐng)求的。對(duì)于nginx來(lái)說(shuō),一個(gè)請(qǐng)求是從ngx_http_init_request開(kāi)始的,在這個(gè)函數(shù)中,會(huì)設(shè)置讀事件為ngx_http_process_request_line,也就是說(shuō),接下來(lái)的網(wǎng)絡(luò)事件,會(huì)由ngx_http_process_request_line來(lái)執(zhí)行。從ngx_http_process_request_line的函數(shù)名,我們可以看到,這就是來(lái)處理請(qǐng)求行的,正好與之前講的,處理請(qǐng)求的第一件事就是處理請(qǐng)求行是一致的。通過(guò)ngx_http_read_request_header來(lái)讀取請(qǐng)求數(shù)據(jù)。然后調(diào)用ngx_http_parse_request_line函數(shù)來(lái)解析請(qǐng)求行。nginx為提高效率,采用狀態(tài)機(jī)來(lái)解析請(qǐng)求行,而且在進(jìn)行method的比較時(shí),沒(méi)有直接使用字符串比較,而是將四個(gè)字符轉(zhuǎn)換成一個(gè)整型,然后一次比較以減少cpu的指令數(shù),這個(gè)前面有說(shuō)過(guò)。很多人可能很清楚一個(gè)請(qǐng)求行包含請(qǐng)求的方法,uri,版本,卻不知道其實(shí)在請(qǐng)求行中,也是可以包含有host的。比如一個(gè)請(qǐng)求GEThttp://www.taobao.com/uriHTTP/1.0這樣一個(gè)請(qǐng)求行也是合法的,而且host是www.taobao.com,這個(gè)時(shí)候,nginx會(huì)忽略請(qǐng)求頭中的host域,而以請(qǐng)求行中的這個(gè)為準(zhǔn)來(lái)查找虛擬主機(jī)。另外,對(duì)于對(duì)于http0.9版來(lái)說(shuō),是不支持請(qǐng)求頭的,所以這里也是要特別的處理。所以,在后面解析請(qǐng)求頭時(shí),協(xié)議版本都是1.0或1.1。整個(gè)請(qǐng)求行解析到的參數(shù),會(huì)保存到ngx_http_request_t結(jié)構(gòu)當(dāng)中。
在解析完請(qǐng)求行后,nginx會(huì)設(shè)置讀事件的handler為ngx_http_process_request_headers,然后后續(xù)的請(qǐng)求就在ngx_http_process_request_headers中進(jìn)行讀取與解析。ngx_http_process_request_headers函數(shù)用來(lái)讀取請(qǐng)求頭,跟請(qǐng)求行一樣,還是調(diào)用ngx_http_read_request_header來(lái)讀取請(qǐng)求頭,調(diào)用ngx_http_parse_header_line來(lái)解析一行請(qǐng)求頭,解析到的請(qǐng)求頭會(huì)保存到ngx_http_request_t的域headers_in中,headers_in是一個(gè)鏈表結(jié)構(gòu),保存所有的請(qǐng)求頭。而HTTP中有些請(qǐng)求是需要特別處理的,這些請(qǐng)求頭與請(qǐng)求處理函數(shù)存放在一個(gè)映射表里面,即ngx_http_headers_in,在初始化時(shí),會(huì)生成一個(gè)hash表,當(dāng)每解析到一個(gè)請(qǐng)求頭后,就會(huì)先在這個(gè)hash表中查找,如果有找到,則調(diào)用相應(yīng)的處理函數(shù)來(lái)處理這個(gè)請(qǐng)求頭。比如:Host頭的處理函數(shù)是ngx_http_process_host。
當(dāng)nginx解析到兩個(gè)回車(chē)換行符時(shí),就表示請(qǐng)求頭的結(jié)束,此時(shí)就會(huì)調(diào)用ngx_http_process_request來(lái)處理請(qǐng)求了。ngx_http_process_request會(huì)設(shè)置當(dāng)前的連接的讀寫(xiě)事件處理函數(shù)為ngx_http_request_handler,然后再調(diào)用ngx_http_handler來(lái)真正開(kāi)始處理一個(gè)完整的http請(qǐng)求。這里可能比較奇怪,讀寫(xiě)事件處理函數(shù)都是ngx_http_request_handler,其實(shí)在這個(gè)函數(shù)中,會(huì)根據(jù)當(dāng)前事件是讀事件還是寫(xiě)事件,分別調(diào)用ngx_http_request_t中的read_event_handler或者是write_event_handler。由于此時(shí),我們的請(qǐng)求頭已經(jīng)讀取完成了,之前有說(shuō)過(guò),nginx的做法是先不讀取請(qǐng)求body,所以這里面我們?cè)O(shè)置read_event_handler為ngx_http_block_reading,即不讀取數(shù)據(jù)了。剛才說(shuō)到,真正開(kāi)始處理數(shù)據(jù),是在ngx_http_handler這個(gè)函數(shù)里面,這個(gè)函數(shù)會(huì)設(shè)置write_event_handler為ngx_http_core_run_phases,并執(zhí)行ngx_http_core_run_phases函數(shù)。ngx_http_core_run_phases這個(gè)函數(shù)將執(zhí)行多階段請(qǐng)求處理,nginx將一個(gè)http請(qǐng)求的處理分為多個(gè)階段,那么這個(gè)函數(shù)就是執(zhí)行這些階段來(lái)產(chǎn)生數(shù)據(jù)。因?yàn)閚gx_http_core_run_phases最后會(huì)產(chǎn)生數(shù)據(jù),所以我們就很容易理解,為什么設(shè)置寫(xiě)事件的處理函數(shù)為ngx_http_core_run_phases了。在這里,我簡(jiǎn)要說(shuō)明了一下函數(shù)的調(diào)用邏輯,我們需要明白最終是調(diào)用ngx_http_core_run_phases來(lái)處理請(qǐng)求,產(chǎn)生的響應(yīng)頭會(huì)放在ngx_http_request_t的headers_out中,這一部分內(nèi)容,我會(huì)放在請(qǐng)求處理流程里面去講。nginx的各種階段會(huì)對(duì)請(qǐng)求進(jìn)行處理,最后會(huì)調(diào)用filter來(lái)過(guò)濾數(shù)據(jù),對(duì)數(shù)據(jù)進(jìn)行加工,如truncked傳輸、gzip壓縮等。這里的filter包括header filter與body filter,即對(duì)響應(yīng)頭或響應(yīng)體進(jìn)行處理。filter是一個(gè)鏈表結(jié)構(gòu),分別有header filter與body filter,先執(zhí)行header filter中的所有filter,然后再執(zhí)行body filter中的所有filter。在header filter中的最后一個(gè)filter,即ngx_http_header_filter,這個(gè)filter將會(huì)遍歷所有的響應(yīng)頭,最后需要輸出的響應(yīng)頭在一個(gè)連續(xù)的內(nèi)存,然后調(diào)用ngx_http_write_filter進(jìn)行輸出。ngx_http_write_filter是body filter中的最后一個(gè),所以nginx首先的body信息,在經(jīng)過(guò)一系列的body filter之后,最后也會(huì)調(diào)用ngx_http_write_filter來(lái)進(jìn)行輸出(有圖來(lái)說(shuō)明)。
這里要注意的是,nginx會(huì)將整個(gè)請(qǐng)求頭都放在一個(gè)buffer里面,這個(gè)buffer的大小通過(guò)配置項(xiàng)client_header_buffer_size來(lái)設(shè)置,如果用戶的請(qǐng)求頭太大,這個(gè)buffer裝不下,那nginx就會(huì)重新分配一個(gè)新的更大的buffer來(lái)裝請(qǐng)求頭,這個(gè)大buffer可以通過(guò)large_client_header_buffers來(lái)設(shè)置,這個(gè)large_buffer這一組buffer,比如配置4 8k,就是表示有四個(gè)8k大小的buffer可以用。注意,為了保存請(qǐng)求行或請(qǐng)求頭的完整性,一個(gè)完整的請(qǐng)求行或請(qǐng)求頭,需要放在一個(gè)連續(xù)的內(nèi)存里面,所以,一個(gè)完整的請(qǐng)求行或請(qǐng)求頭,只會(huì)保存在一個(gè)buffer里面。這樣,如果請(qǐng)求行大于一個(gè)buffer的大小,就會(huì)返回414錯(cuò)誤,如果一個(gè)請(qǐng)求頭大小大于一個(gè)buffer大小,就會(huì)返回400錯(cuò)誤。在了解了這些參數(shù)的值,以及nginx實(shí)際的做法之后,在應(yīng)用場(chǎng)景,我們就需要根據(jù)實(shí)際的需求來(lái)調(diào)整這些參數(shù),來(lái)優(yōu)化我們的程序了。處理流程圖:
以上這些,就是nginx中一個(gè)http請(qǐng)求的生命周期了。我們?cè)倏纯磁c請(qǐng)求相關(guān)的一些概念吧。
KeepAlive長(zhǎng)連接當(dāng)然,在nginx中,對(duì)于http1.0與http1.1也是支持長(zhǎng)連接的。什么是長(zhǎng)連接呢?我們知道,http請(qǐng)求是基于TCP協(xié)議之上的,那么,當(dāng)客戶端在發(fā)起請(qǐng)求前,需要先與服務(wù)端建立TCP連接,而每一次的TCP連接是需要三次握手來(lái)確定的,如果客戶端與服務(wù)端之間網(wǎng)絡(luò)差一點(diǎn),這三次交互消費(fèi)的時(shí)間會(huì)比較多,而且三次交互也會(huì)帶來(lái)網(wǎng)絡(luò)流量。當(dāng)然,當(dāng)連接斷開(kāi)后,也會(huì)有四次的交互,當(dāng)然對(duì)用戶體驗(yàn)來(lái)說(shuō)就不重要了。而http請(qǐng)求是請(qǐng)求應(yīng)答式的,如果我們能知道每個(gè)請(qǐng)求頭與響應(yīng)體的長(zhǎng)度,那么我們是可以在一個(gè)連接上面執(zhí)行多個(gè)請(qǐng)求的,這就是所謂的長(zhǎng)連接,但前提條件是我們先得確定請(qǐng)求頭與響應(yīng)體的長(zhǎng)度。對(duì)于請(qǐng)求來(lái)說(shuō),如果當(dāng)前請(qǐng)求需要有body,如POST請(qǐng)求,那么nginx就需要客戶端在請(qǐng)求頭中指定content-length來(lái)表明body的大小,否則返回400錯(cuò)誤。也就是說(shuō),請(qǐng)求體的長(zhǎng)度是確定的,那么響應(yīng)體的長(zhǎng)度呢?先來(lái)看看http協(xié)議中關(guān)于響應(yīng)body長(zhǎng)度的確定:
對(duì)于http1.0協(xié)議來(lái)說(shuō),如果響應(yīng)頭中有content-length頭,則以content-length的長(zhǎng)度就可以知道body的長(zhǎng)度了,客戶端在接收body時(shí),就可以依照這個(gè)長(zhǎng)度來(lái)接收數(shù)據(jù),接收完后,就表示這個(gè)請(qǐng)求完成了。而如果沒(méi)有content-length頭,則客戶端會(huì)一直接收數(shù)據(jù),直到服務(wù)端主動(dòng)斷開(kāi)連接,才表示body接收完了。 而對(duì)于http1.1協(xié)議來(lái)說(shuō),如果響應(yīng)頭中的Transfer-encoding為chunked傳輸,則表示body是流式輸出,body會(huì)被分成多個(gè)塊,每塊的開(kāi)始會(huì)標(biāo)識(shí)出當(dāng)前塊的長(zhǎng)度,此時(shí),body不需要通過(guò)長(zhǎng)度來(lái)指定。如果是非chunked傳輸,而且有content-length,則按照content-length來(lái)接收數(shù)據(jù)。否則,如果是非chunked,并且沒(méi)有content-length,則客戶端接收數(shù)據(jù),直到服務(wù)端主動(dòng)斷開(kāi)連接。從上面,我們可以看到,除了http1.0不帶content-length以及http1.1非chunked不帶content-length外,body的長(zhǎng)度是可知的。此時(shí),當(dāng)服務(wù)端在輸出完body之后,會(huì)可以考慮使用長(zhǎng)連接。能否使用長(zhǎng)連接,也是有條件限制的。如果客戶端的請(qǐng)求頭中的connection為close,則表示客戶端需要關(guān)掉長(zhǎng)連接,如果為keep-alive,則客戶端需要打開(kāi)長(zhǎng)連接,如果客戶端的請(qǐng)求中沒(méi)有connection這個(gè)頭,那么根據(jù)協(xié)議,如果是http1.0,則默認(rèn)為close,如果是http1.1,則默認(rèn)為keep-alive。如果結(jié)果為keepalive,那么,nginx在輸出完響應(yīng)體后,會(huì)設(shè)置當(dāng)前連接的keepalive屬性,然后等待客戶端下一次請(qǐng)求。當(dāng)然,nginx不可能一直等待下去,如果客戶端一直不發(fā)數(shù)據(jù)過(guò)來(lái),豈不是一直占用這個(gè)連接?所以當(dāng)nginx設(shè)置了keepalive等待下一次的請(qǐng)求時(shí),同時(shí)也會(huì)設(shè)置一個(gè)等待時(shí)間,這個(gè)時(shí)間是通過(guò)選項(xiàng)keepalive_timeout來(lái)配置的,如果配置為0,則表示關(guān)掉keepalive,此時(shí),http版本無(wú)論是1.1還是1.0,客戶端的connection不管是close還是keepalive,都會(huì)強(qiáng)制為close。
如果服務(wù)端最后的決定是keepalive打開(kāi),那么在響應(yīng)的http頭里面,也會(huì)包含有connection頭域,其值是”Keep-Alive”,否則就是”Close”。如果connection值為close,那么在nginx響應(yīng)完數(shù)據(jù)后,會(huì)主動(dòng)關(guān)掉連接。所以,對(duì)于請(qǐng)求量比較大的nginx來(lái)說(shuō),關(guān)掉keepalive最后會(huì)產(chǎn)生比較多的time-wait狀態(tài)的socket。一般來(lái)說(shuō),當(dāng)客戶端的一次訪問(wèn),需要多次訪問(wèn)同一個(gè)server時(shí),打開(kāi)keepalive的優(yōu)勢(shì)非常大,比如圖片服務(wù)器,通常一個(gè)網(wǎng)頁(yè)會(huì)包含很多個(gè)圖片。打開(kāi)keepalive也會(huì)大量減少time-wait的數(shù)量。
Pipe流水線作業(yè)在http1.1中,引入了一種新的特性,即pipeline。那么什么是pipeline呢?pipeline其實(shí)就是流水線作業(yè),它可以看作為keepalive的一種升華,因?yàn)閜ipeline也是基于長(zhǎng)連接的,目的就是利用一個(gè)連接做多次請(qǐng)求。如果客戶端要提交多個(gè)請(qǐng)求,對(duì)于keepalive來(lái)說(shuō),那么第二個(gè)請(qǐng)求,必須要等到第一個(gè)請(qǐng)求的響應(yīng)接收完全后,才能發(fā)起,這和TCP的停止等待協(xié)議是一樣的,得到兩個(gè)響應(yīng)的時(shí)間至少為2*RTT。而對(duì)pipeline來(lái)說(shuō),客戶端不必等到第一個(gè)請(qǐng)求處理完后,就可以馬上發(fā)起第二個(gè)請(qǐng)求。得到兩個(gè)響應(yīng)的時(shí)間可能能夠達(dá)到1*RTT。nginx是直接支持pipeline的,但是,nginx對(duì)pipeline中的多個(gè)請(qǐng)求的處理卻不是并行的,依然是一個(gè)請(qǐng)求接一個(gè)請(qǐng)求的處理,只是在處理第一個(gè)請(qǐng)求的時(shí)候,客戶端就可以發(fā)起第二個(gè)請(qǐng)求。這樣,nginx利用pipeline減少了處理完一個(gè)請(qǐng)求后,等待第二個(gè)請(qǐng)求的請(qǐng)求頭數(shù)據(jù)的時(shí)間。其實(shí)nginx的做法很簡(jiǎn)單,前面說(shuō)到,nginx在讀取數(shù)據(jù)時(shí),會(huì)將讀取的數(shù)據(jù)放到一個(gè)buffer里面,所以,如果nginx在處理完前一個(gè)請(qǐng)求后,如果發(fā)現(xiàn)buffer里面還有數(shù)據(jù),就認(rèn)為剩下的數(shù)據(jù)是下一個(gè)請(qǐng)求的開(kāi)始,然后就接下來(lái)處理下一個(gè)請(qǐng)求,否則就設(shè)置keepalive。
Lingering_close延遲關(guān)閉lingering_close,字面意思就是延遲關(guān)閉,也就是說(shuō),當(dāng)nginx要關(guān)閉連接時(shí),并非立即關(guān)閉連接,而是先關(guān)閉tcp連接的寫(xiě),再等待一段時(shí)間后再關(guān)掉連接的讀。為什么要這樣呢?我們先來(lái)看看這樣一個(gè)場(chǎng)景。nginx在接收客戶端的請(qǐng)求時(shí),可能由于客戶端或服務(wù)端出錯(cuò)了,要立即響應(yīng)錯(cuò)誤信息給客戶端,而nginx在響應(yīng)錯(cuò)誤信息后,大分部情況下是需要關(guān)閉當(dāng)前連接。nginx執(zhí)行完write()系統(tǒng)調(diào)用把錯(cuò)誤信息發(fā)送給客戶端,write()系統(tǒng)調(diào)用返回成功并不表示數(shù)據(jù)已經(jīng)發(fā)送到客戶端,有可能還在tcp連接的write buffer里。接著如果直接執(zhí)行close()系統(tǒng)調(diào)用關(guān)閉tcp連接,內(nèi)核會(huì)首先檢查tcp的read buffer里有沒(méi)有客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù)留在內(nèi)核態(tài)沒(méi)有被用戶態(tài)進(jìn)程讀取,如果有則發(fā)送給客戶端RST報(bào)文來(lái)關(guān)閉tcp連接丟棄write buffer里的數(shù)據(jù),如果沒(méi)有則等待write buffer里的數(shù)據(jù)發(fā)送完畢,然后再經(jīng)過(guò)正常的4次分手報(bào)文斷開(kāi)連接。所以,當(dāng)在某些場(chǎng)景下出現(xiàn)tcp write buffer里的數(shù)據(jù)在write()系統(tǒng)調(diào)用之后到close()系統(tǒng)調(diào)用執(zhí)行之前沒(méi)有發(fā)送完畢,且tcp read buffer里面還有數(shù)據(jù)沒(méi)有讀,close()系統(tǒng)調(diào)用會(huì)導(dǎo)致客戶端收到RST報(bào)文且不會(huì)拿到服務(wù)端發(fā)送過(guò)來(lái)的錯(cuò)誤信息數(shù)據(jù)。那客戶端肯定會(huì)想,這服務(wù)器好霸道,動(dòng)不動(dòng)就reset我的連接,連個(gè)錯(cuò)誤信息都沒(méi)有。
在上面這個(gè)場(chǎng)景中,我們可以看到,關(guān)鍵點(diǎn)是服務(wù)端給客戶端發(fā)送了RST包,導(dǎo)致自己發(fā)送的數(shù)據(jù)在客戶端忽略掉了。所以,解決問(wèn)題的重點(diǎn)是,讓服務(wù)端別發(fā)RST包。再想想,我們發(fā)送RST是因?yàn)槲覀冴P(guān)掉了連接,關(guān)掉連接是因?yàn)槲覀儾幌朐偬幚泶诉B接了,也不會(huì)有任何數(shù)據(jù)產(chǎn)生了。對(duì)于全雙工的TCP連接來(lái)說(shuō),我們只需要關(guān)掉寫(xiě)就行了,讀可以繼續(xù)進(jìn)行,我們只需要丟掉讀到的任何數(shù)據(jù)就行了,這樣的話,當(dāng)我們關(guān)掉連接后,客戶端再發(fā)過(guò)來(lái)的數(shù)據(jù),就不會(huì)再收到RST了。當(dāng)然最終我們還是需要關(guān)掉這個(gè)讀端的,所以我們會(huì)設(shè)置一個(gè)超時(shí)時(shí)間,在這個(gè)時(shí)間過(guò)后,就關(guān)掉讀,客戶端再發(fā)送數(shù)據(jù)來(lái)就不管了,作為服務(wù)端我會(huì)認(rèn)為,都這么長(zhǎng)時(shí)間了,發(fā)給你的錯(cuò)誤信息也應(yīng)該讀到了,再慢就不關(guān)我事了,要怪就怪你RP不好了。當(dāng)然,正常的客戶端,在讀取到數(shù)據(jù)后,會(huì)關(guān)掉連接,此時(shí)服務(wù)端就會(huì)在超時(shí)時(shí)間內(nèi)關(guān)掉讀端。這些正是lingering_close所做的事情。協(xié)議棧提供 SO_LINGER 這個(gè)選項(xiàng),它的一種配置情況就是來(lái)處理lingering_close的情況的,不過(guò)nginx是自己實(shí)現(xiàn)的lingering_close。lingering_close存在的意義就是來(lái)讀取剩下的客戶端發(fā)來(lái)的數(shù)據(jù),所以nginx會(huì)有一個(gè)讀超時(shí)時(shí)間,通過(guò)lingering_timeout選項(xiàng)來(lái)設(shè)置,如果在lingering_timeout時(shí)間內(nèi)還沒(méi)有收到數(shù)據(jù),則直接關(guān)掉連接。nginx還支持設(shè)置一個(gè)總的讀取時(shí)間,通過(guò)lingering_time來(lái)設(shè)置,這個(gè)時(shí)間也就是nginx在關(guān)閉寫(xiě)之后,保留socket的時(shí)間,客戶端需要在這個(gè)時(shí)間內(nèi)發(fā)送完所有的數(shù)據(jù),否則nginx在這個(gè)時(shí)間過(guò)后,會(huì)直接關(guān)掉連接。當(dāng)然,nginx是支持配置是否打開(kāi)lingering_close選項(xiàng)的,通過(guò)lingering_close選項(xiàng)來(lái)配置。 那么,我們?cè)趯?shí)際應(yīng)用中,是否應(yīng)該打開(kāi)lingering_close呢?這個(gè)就沒(méi)有固定的推薦值了,如Maxim Dounin所說(shuō),lingering_close的主要作用是保持更好的客戶端兼容性,但是卻需要消耗更多的額外資源(比如連接會(huì)一直占著)。
這節(jié),我們介紹了nginx中,連接與請(qǐng)求的基本概念,下節(jié),我們講基本的數(shù)據(jù)結(jié)構(gòu)。
基本數(shù)據(jù)結(jié)構(gòu)
網(wǎng)頁(yè)題目:nginx基礎(chǔ)概念(二)
網(wǎng)站URL:http://chinadenli.net/article4/cpcpoe.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站改版、Google、建站公司、標(biāo)簽優(yōu)化、服務(wù)器托管、網(wǎng)頁(yè)設(shè)計(jì)公司
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)