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