Nginx与TCP协议的关系
作為Web服務(wù)器的nginx,主要任務(wù)當(dāng)然是處理好基于TCP的HTTP協(xié)議,本節(jié)將深入TCP協(xié)議的實(shí)現(xiàn)細(xì)節(jié)(linux下)以更好地理解Nginx事件處理機(jī)制。
TCP是一個(gè)面向連接的協(xié)議,它必須基于建立好的TCP連接來(lái)為通信的兩方提供可靠的字節(jié)流服務(wù)。建立TCP連接是我們耳熟能詳?shù)娜挝帐?
- 客戶端向服務(wù)器發(fā)起連接(SYN)。
- 服務(wù)器確認(rèn)收到并向客戶端也發(fā)起連接(ACK+SYN)。
- 客戶端確認(rèn)收到服務(wù)器發(fā)起的連接(ACK)。
這個(gè)建立連接的過(guò)程是在操作系統(tǒng)內(nèi)核中完成的,而如Nginx這樣的應(yīng)用程序只是從內(nèi)核中取出已經(jīng)建立好的TCP連接。大多時(shí)候,Nginx是作為連接的服務(wù)器方存在的,我們看一看Linux內(nèi)核是怎樣處理TCP連接建立的,如圖1所示。
圖1簡(jiǎn)單地表達(dá)了一個(gè)觀點(diǎn):內(nèi)核在我們調(diào)用listen方法時(shí),就已經(jīng)為這個(gè)監(jiān)聽(tīng)端口建立了SYN隊(duì)列和ACCEPT隊(duì)列,當(dāng)客戶端使用connect方法向服務(wù)器發(fā)起TCP連接,隨后圖中1.1步驟客戶端的SYN包到達(dá)了服務(wù)器后,內(nèi)核會(huì)把這一信息放到SYN隊(duì)列(即未完成握手隊(duì)列)中,同時(shí)回一個(gè)SYN+ACK包給客戶端。2.1步驟中客戶端再次發(fā)來(lái)了針對(duì)服務(wù)器SYN包的ACK網(wǎng)絡(luò)分組時(shí),內(nèi)核會(huì)把連接從SYN隊(duì)列中取出,再把這個(gè)連接放到ACCEPT隊(duì)列(即已完成握手隊(duì)列)中。而服務(wù)器在第3步調(diào)用accept方法建立連接時(shí),其實(shí)就是直接從ACCEPT隊(duì)列中取出已經(jīng)建好的連接而已。
這樣,如果大量連接同時(shí)到來(lái),而應(yīng)用程序不能及時(shí)地調(diào)用accept方法,就會(huì)導(dǎo)致以上兩個(gè)隊(duì)列滿(ACCEPT隊(duì)列滿,進(jìn)而也會(huì)導(dǎo)致SYN隊(duì)列滿),從而導(dǎo)致連接無(wú)法建立。這其實(shí)很常見(jiàn),比如Nginx的每個(gè)worker進(jìn)程都負(fù)責(zé)調(diào)用accept方法,如果一個(gè)Nginx模塊在處理請(qǐng)求時(shí)長(zhǎng)時(shí)間陷入了某個(gè)方法的執(zhí)行中(如執(zhí)行計(jì)算或者等待IO),就有可能導(dǎo)致新連接無(wú)法建立。
建立好連接后,TCP提供了可靠的字節(jié)流服務(wù)。怎么理解所謂的“可靠”呢?可以簡(jiǎn)單概括為以下4點(diǎn):
從以上4點(diǎn)可以看到,內(nèi)核為每一個(gè)TCP連接都分配了內(nèi)存分別充當(dāng)發(fā)送、接收緩沖這與Nginx這種應(yīng)用程序中的用戶態(tài)緩存不同。搞清楚內(nèi)核的TCP讀寫(xiě)緩存區(qū),對(duì)于我們斷Nginx的處理能力很有幫助,畢竟無(wú)論內(nèi)核還是應(yīng)用程序都在搶物理內(nèi)存。
先來(lái)看看調(diào)用send這樣的方法發(fā)送TCP字節(jié)流時(shí),內(nèi)核到底做了哪些事。下圖2是一個(gè)簡(jiǎn)的send方法調(diào)用時(shí)的流程示意圖。
TCP連接建立時(shí),就可以判斷出雙方的網(wǎng)絡(luò)間最適宜的、不會(huì)被再次切分的報(bào)文大小,TCP層把它叫做MSS最大報(bào)文段長(zhǎng)度(當(dāng)然,MSS是可變的)。在圖2的場(chǎng)景中,假定待發(fā)送的內(nèi)存將按照MSS被切分為3個(gè)報(bào)文,應(yīng)用程序在第1步調(diào)用send方法、第10步send方法返回之間,內(nèi)核的主要任務(wù)是把用戶態(tài)的內(nèi)存內(nèi)容拷貝到內(nèi)核態(tài)的TCP緩沖區(qū)上,在第5步時(shí)假定內(nèi)核緩存區(qū)暫時(shí)不足,在超時(shí)時(shí)間內(nèi)又等到了足夠的空閑空間。從圖中可以看到,send方法成功返回并不等于就把報(bào)文發(fā)送出去了(當(dāng)然更不等于對(duì)方接收到了報(bào)文)。
當(dāng)調(diào)用recv這樣的方法接收?qǐng)?bào)文時(shí),Nginx是基于事件驅(qū)動(dòng)的,也就是說(shuō)只有epoll通知worker進(jìn)程收到了網(wǎng)絡(luò)報(bào)文,recv才會(huì)被調(diào)用(socket也被設(shè)為非阻塞模式〉。圖3就是一個(gè)這樣的場(chǎng)景,在第1~4步表示接收到了無(wú)序的報(bào)文后,內(nèi)核是怎樣重新排序的。第5步開(kāi)始,應(yīng)用程序調(diào)用了recv方法,內(nèi)核開(kāi)始把TCP讀緩沖區(qū)的內(nèi)容拷貝到應(yīng)用程序的用戶態(tài)內(nèi)存中,第13步recv方法返回拷貝的字節(jié)數(shù)。圖中用到了linux內(nèi)核中為T(mén)CP準(zhǔn)備的2個(gè)隊(duì)列:receive隊(duì)列是允許用戶進(jìn)程直接讀取的,它是將已經(jīng)接收到的TCP報(bào)文,去除了TCP頭部、排好序放入的、用戶進(jìn)程可以直接按序讀取的隊(duì)列; out_of_order隊(duì)列存放亂序的報(bào)文。
回過(guò)頭來(lái)看,Nginx使用好TCP協(xié)議主要在于如何有效率地使用CPU和內(nèi)存。只在必要時(shí)才調(diào)用TCP的sendrecv方法,這樣就避免了無(wú)謂的CPU浪費(fèi)。例如,只有接收到報(bào)文,甚至只有接收到足夠多的報(bào)文(SO_RCVLOWAT闕值〉,worker進(jìn)程才有可能調(diào)用recv方法。同樣,只在發(fā)送緩沖區(qū)有空閑空間時(shí)才去調(diào)用send方法。這樣的調(diào)用才是有效率的。Nginx對(duì)內(nèi)存的分配是很節(jié)儉的,但Linux內(nèi)核使用的內(nèi)存又如何控制呢?
首先,我們可以控制內(nèi)存緩存的上限,例如基于setsockopt方法實(shí)現(xiàn)的SO_SNDBUF、SO_RCVBUF (Nginx的listen配置里的sndbuf和rcvbuf也是在改它們)。
So_SNDBUF表示這個(gè)連接上的內(nèi)核寫(xiě)緩存上限(事實(shí)上SO_SNDBUF也并不是精確的上限,在內(nèi)核中會(huì)把這個(gè)值翻一倍再作為寫(xiě)緩存上限使用)。它受制于系統(tǒng)級(jí)配置的上下限net.core.wmem_max()。SO_RCVBUF同理。讀寫(xiě)緩存的實(shí)際內(nèi)存大小與場(chǎng)景有關(guān)。對(duì)讀緩存來(lái)說(shuō),接收到一個(gè)來(lái)自連接對(duì)端的TCP報(bào)文時(shí),會(huì)導(dǎo)致讀緩存增加,如果超過(guò)了讀緩存上限,那么這個(gè)報(bào)文會(huì)被丟棄。當(dāng)進(jìn)程調(diào)用read、recv這樣的方法讀取字節(jié)流時(shí),讀緩存就會(huì)減少。因此,讀緩存是一個(gè)動(dòng)態(tài)變化的、實(shí)際用到多少才分配多少的緩沖內(nèi)存。當(dāng)用戶進(jìn)程調(diào)用send方法發(fā)送TCP字節(jié)流時(shí),就會(huì)造成寫(xiě)緩存增大。當(dāng)然,如果寫(xiě)緩存已經(jīng)到達(dá)上限,那么寫(xiě)緩存維持不變,向用戶進(jìn)程返回失敗。而每當(dāng)接收到連接對(duì)端發(fā)來(lái)的ACK,確認(rèn)了報(bào)文的成功發(fā)送時(shí),寫(xiě)緩存就會(huì)減少。可見(jiàn)緩存上限所起作用為:丟棄新報(bào)文,防止這個(gè)TCP連接消耗太多的內(nèi)存。
總結(jié)
以上是生活随笔為你收集整理的Nginx与TCP协议的关系的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Qt —— QWebEngineView
- 下一篇: 26.Nginx详解