網絡編程常見問題總結 串講 收藏
網絡編程常見問題總結
在網絡程序中遇到的一些問題進行了總結, 這裡主要針對的是我們常用的TCP socket相關的總結,可能會存在錯誤,有任何問題歡迎大家提出.
對於網絡編程的更多詳細說明建議參考下面的書籍
《UNIX網絡編程》 《TCP/IP 詳解》 《Unix環境高級編程》
非阻塞IO和阻塞IO:
在網絡編程中對於一個網絡句柄會遇到阻塞IO和非阻塞IO的概念, 這裡對於這兩種socket先做一下說明
基本概念: socket的阻塞模式意味著必須要做完IO操作(包括錯誤)才會返回。 非阻塞模式下無論操作是否完成都會立刻返回,需要通過其他方式來判斷具體操作是否成功。
設置:
一般對於一個socket是阻塞模式還是非阻塞模式有兩種方式 fcntl設置和recv,send系列的參數.
fcntl函數可以將一個socket句柄設置成非阻塞模式:
flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); 設置之後每次的對於sockfd的操作都是非阻塞的
recv, send函數的最後有一個flag參數可以設置成MSG_DONTWAIT臨時將sockfd設置為非阻塞模式,而無論原有是阻塞還是非阻塞。 recv(sockfd, buff, buff_size, MSG_DONTWAIT); send(scokfd, buff, buff_size, MSG_DONTWAIT);
區別:
讀:
讀本質來說其實不能是讀,在實際中, 具體的接收數據不是由這些調用來進行,是由於系統底層自動完成的,read也好,recv也好只負責把數據從底層緩沖copy到我們指定的位置. 對於讀來說(read, 或者 recv) ,在阻塞條件下如果沒有發現數據在網絡緩沖中會一直等待,當發現有數據的時候會把數據讀到用戶指定的緩沖區,但是如果這個時候讀到的數據量比較少,比參數中指定的長度要小,read並不會一直等待下去,而是立刻返回。read的原則是數據在不超過指定的長度的時候有多少讀多少,沒有數據就會一直等待。所以一般情況下我們讀取數據都需要采用循環讀的方式讀取數據,一次read完畢不能保證讀到我們需要長度的數據,read完一次需要判斷讀到的數據長度再決定是否還需要再次讀取。在非阻塞的情況下,read的行為是如果發現沒有數據就直接返回,如果發現有數據那麼也是采用有多少讀多少的進行處理.對於讀而言,阻塞和非阻塞的區別在於沒有數據到達的時候是否立刻返回.
recv中有一個MSG_WAITALL的參數 recv(sockfd, buff, buff_size, MSG_WAITALL), 在正常情況下 recv是會等待直到讀取到buff_size長度的數據,但是這裡的WAITALL也只是盡量讀全,在有中斷的情況下recv還是可能會被打斷,造成沒有讀完指定的buff_size的長度。所以即使是采用recv + WAITALL參數還是要考慮是否需要循環讀取的問題,在實驗中對於多數情況下recv還是可以讀完buff_size,所以相應的性能會比直接read 進行循環讀要好一些。不過要注意的是這個時候的sockfd必須是處於阻塞模式下,否則WAITALL不能起作用。
寫:
寫的本質也不是進行發送操作,而是把用戶態的數據copy到系統底層去,然後再由系統進行發送操作,返回成功只表示數據已經copy到底層緩沖,而不表示數據以及發出,更不能表示對端已經接收到數據.
對於write(或者send)而言,在阻塞的情況是會一直等待直到write完全部的數據再返回.這點行為上與讀操作有所不同,究其原因主要是讀數據的時候我們並不知道對端到底有沒有數據,數據是在什麼時候結束發送的,如果一直等待就可能會造成死循環,所以並沒有去進行這方面的處理;而對於write, 由於需要寫的長度是已知的,所以可以一直再寫,直到寫完.不過問題是write是可能被打斷造成write一次只write一部分數據, 所以write的過程還是需要考慮循環write, 只不過多數情況下一次write調用就可能成功.
非阻塞寫的情況下,是采用可以寫多少就寫多少的策略.與讀不一樣的地方在於,有多少讀多少是由網絡發送的那一端是否有數據傳輸到為標准,但是對於可以寫多少是由本地的網絡堵塞情況為標准的,在網絡阻塞嚴重的時候,網絡層沒有足夠的內存來進行寫操作,這時候就會出現寫不成功的情況,阻塞情況下會盡可能(有可能被中斷)等待到數據全部發送完畢,對於非阻塞的情況就是一次寫多少算多少,沒有中斷的情況下也還是會出現write到一部分的情況.
超時控制:
對於網絡IO,我們一般情況下都需要超時機制來避免進行操作的線程被handle住,經典的做法就是采用select+非阻塞IO進行判斷,select在超時時間內判斷是否可以讀寫操作,然後采用非堵塞讀寫,不過一般實現的時候讀操作不需要設置為非堵塞,上面已經說過讀操作只有在沒有數據的時候才會阻塞,select的判斷成功說明存在數據,所以即使是阻塞讀在這種情況下也是可以做到非阻塞的效果,就沒有必要設置成非阻塞的情況了.
這部分的代碼可以參考ullib中ul_sreado_ms_ex和ul_swriteo_ms_ex.
采用ul_sreado_ms_ex讀數據也是不能保證返回大於0就一定讀到指定的數據長度, 對於讀寫操作, 都是需要判斷返回的讀長度或者寫長度是否是需要的長度, 不能簡單的判斷一下返回值是否小於0. 對於ul_sreado_ms_ex的情況如果出現了發送端數據發送一半就被close掉的情況就有可能導致接收端讀不到完整的數據包.
errno 只有在函數返回值為負的時候才有效,如果返回0或者大於0的數, errno 的結果是無意義的. 有些時候 會出現read到0,但是我們認為是錯誤的情況然後輸出errno造成誤解,一般建議在這種情況要同時輸出返回值和errno的結果,有些情況由於只有errno造成了對於問題的判斷失誤。
長連接和短連接的各種可能的問題及相應的處理
這裡主要是發起連接的客戶端的問題,這裡列出的問題主要是在采用同步模型的情況下才會存在的問題.
短連接:
采用短連接的情況一般是考慮到下面的一些問題:
後端服務的問題, 考慮最簡單的情況下一個線程一個連接, 如果這個連接采用了長連接那麼就需要我們處理連接的線程和後端保持一一對應,然後按照某些原則進行處理(n對n的關系), 但由於一方面服務器可能增加,這樣導致需要前後端保持一致,帶來了更多的麻煩,另一方面線程數上不去對應處理能力也會產生影響,而短連接每次連接的時候只需要關注當前的機器,問題相對會少一些. 其實這個問題可以采用連接池的方式來解決,後面會提到. 不需要考慮由於異常帶來的髒數據。負載均衡方面可以簡單考慮, 無論線程數是多少還是後端服務器的數量是多少都沒有關系, 每次考慮單個連接就可以了. 當然如果負載邏輯簡單,並且機器相對固定,一個線程一個長連接問題也不大.
規避一些問題, 在過去有些情況下出現長連接大延時,數據沒響應等問題, 測試的時候發現換短連接問題就解決了,由於時間關系就沒有再繼續追查, 事實上這些問題現在基本上都已經定位並且有相關的解決方案了.
不足:
效率不足, 由於連接操作一般會有50ns~200ns的時間消耗,導致短連接需要消耗更多的時間會產生TIME_WAIT問題,需要做更多的守護
長連接:
長連接相比短連接減少了連接的時間消耗, 可以承受更高的負載. 但在使用的時候需要考慮一些問題髒數據, 在一些特殊情況(特別是邏輯錯誤的情況下) 會存在一些我們並不需要的數據. 這個時候的處理比較安全的方式是一旦檢測到就關閉連接, 檢測的方式在在發起請求前 用 前面 為什麼socket寫錯誤,但用recv檢查依然成功? 介紹的方式進行檢查. 不過有些程序會采用繼續讀把所有不需要的數據讀完畢(讀到 EAEGIN), 不過這種方式過分依賴邏輯了,存在了一定的風險. 不如直接斷開來的簡單後端連接, 前面也提到了在這種情況我們一般會采用連接池的方式來解決問題比如(public/connectpool中就可以維護不同的連接,使每個線程都可以均勻的獲取到句柄) 服務端的處理這個時候需要考慮連接的數量,簡單的方式就是一個長連接一個線程, 但是線程也不能無限增加( 增加了,可能造成大量的上下文切換使的性能下降). 我們一般在長連接的情況采用pendingpool的模型, 通過一個異步隊列來緩沖, 這樣不需要考慮客戶端和服務端的線程數問題,可以任意配置(可以通過線下測試選擇合適的線程數)
一些特殊的問題, 主要是長連接的延時 在後面的FAQ中會有詳細的說明.
一般來說,對於我們多數的內部業務邏輯都是可以采用長連接模式,不會產生太多的問題.
主要線程模型優缺點和注意事項
這裡所列出的線程模型,目前在我們的public/ub下都有相關的實現,在 ubFAQ中也有相關的說明,這裡主要針對這些模 型的使用做相關的說明
最簡單的線程模型
同時啟動多個線程, 每個線程都采用accept的方式進行阻塞獲取連接(具體實現上一般是先select在accept, 一方面規避低內核的驚群效應,另一方面可以做到優雅退出). 多個線程競爭一個連接, 拿到連接的線程就進行自己的邏輯處理, 包括讀寫IO全部都在一個線程中進行. 短連接每次重新accept, 長連接,第一次的時候accept然後反復使用. 一般來說在總連接數很少的情況下效果會比較好,相對適用於少量短連接(可以允許比線程數多一些)和不超過線程總數的長連接(超過的那些連接,除非 accept的連接斷開,否則不可能會有線程對它進行accept).
但如果同一時候連接數過多會造成沒有工作線程與客戶端進行連接,客戶端會出現大量的連接失敗, 因為這個時候線程可能存在不能及時accept造成超時問題, 在有重試機制的情況下可能導致問題更糟糕. 有些程序在出現幾次超時之後會長時間一直有連接超時往往就是在這種情況下發生的.
這種模型的最大優點在於編寫簡單, 在正常情況下工作效果不錯. 在public/ub中的xpool就是屬於這種模型,建議針對連接數少的服務進行使用,比如一些一對一的業務邏輯.
生產者消費者模型 普通線程模型在長連接方面存在使用限制(需要對於線程數進行變化, 而線程又不是無限的), 短連接在處理同時大量連接(比如流量高峰期)的時候存在問題.
生產者消費者模型是可以把這種影響減少.
對於有數據的活動連接放到異步隊列中, 其他線程競爭這個隊列獲取句柄然後進行相關的操作. 由於accept是專門的線程進行處理, 出現被handle的情況比較少,不容易出現連接失敗的情況.在大流量的情況下有一定的緩沖,雖然有些請求會出現延時,但只要在可以接受的范圍內,服務還是可以正常進行. 一般來說隊列的長度主要是考慮可以接受的延時程度.
這種模式也是我們現在許多服務比較常用的模型.可以不用關心客戶端和服務的線程數對應關系,業務邏輯上也是比較簡單的。
但這種模式在編程的時候,對於長連接有一個陷阱,判斷句柄是否可讀寫以前一般采用的是select, 如果長連接的連接數比工作線程還少,當所有的連接都被處理了,有連接需要放回pool中,而這個時候如果正常建立連接的監聽線程正好處於select狀態,這個時候必須要等到 select超時才能重新將連接放入select中進行監聽,因為這之前被放入select進行監聽的處理socket為空,不會有響應,這個時候由於時間的浪費造成l長連接的性能下降。一般來說某個連接數少,某個連接特別活躍就可能造成問題. 過去的一些做法是控制連接數和服務端的工作線程數以及通過監聽一個管道fd,在工作線程結束每次都激活這個fd跳出這次select來控制。現在的2.6 內核中的epoll在判斷可讀寫的時候不會存在這個問題(epoll在進行監聽的時候,其它線程放入或者更改, 在epoll_wait的時候是可以馬上激活的), 我們現在的服務多采用epoll代替select來解決這個, 但是主要的邏輯沒有變化. ub_server中epool和public/ependingpool都是采用種模式
異步模型 這裡只做一些簡單的介紹。
上面兩者模型本質都是同步的處理業務邏輯,在一個線程中處理了讀請求,業務邏輯和寫回響應三個過程(很多業務更復雜,但是都是可以做相應的拆封的), 但是讀和寫這兩個IO的處理往往需要阻塞等待, 這樣造成了線程被阻塞, 如果要應付慢連接(比如外圍抓取等待的時間是秒級的甚至更多), 在等待的時候其實CPU沒有干多少事情, 這個時候就造成了浪費. 一種考慮是增加線程數,通過提高並發來解決這個問題, 但是我們目前的線程數還是有限的,不可能無限增加. 而且線程的增加會帶來cpu對於上下文切換的代價,另一方面多個線程從一個隊列中獲取可用連接, 這裡存在互斥線程多的時候會導致性能下降,當然這裡可以通過把一個隊列改多隊列減少互斥來實現.
引入異步化的處理, 就是把對於IO的等待采用IO復用的方式,專門放入到一個或者若干個線程中去, 處理主邏輯的程序可以被釋放出來, 只有在IO處理完畢才進行處理, 這樣可以提高CPU的使用率,減少等待的時間. 一般情況下幾個線程(一般和CPU的核數相當)可以應付很大的流量請求 public/kylin , ub/ub(ub事件模型)都是基於純異步思想的異步框架。而ub中的appool是簡化版本將原本ub框架中網絡IO處理進行了異步化,不過目前只支持采用nshead頭的模式。
為什麼網絡程序會沒有任何預兆的就退出了
一般情況都是沒有設置忽略PIPE信號
在我們的環境中當網絡觸發broken pipe (一般情況是write的時候,沒有write完畢,接受端異常斷開了),系統默認的行為是直接退出。在我們的程序中一般都要在啟動的時候加上 signal(SIGPIPE, SIG_IGN); 來強制忽略這種錯誤
write出去的數據, read的時候知道長度嗎?
嚴格來說, 交互的兩端, 一端write調用write出去的長度, 接收端是不知道具體要讀多長的. 這裡有幾個方面的問題
write 長度為n的數據, 一次write不一定能成功(雖然小數據絕大多數都會成功), 需要循環多次write
write 雖然成功,但是在網絡中還是可能需要拆包和組包, write出來的一塊數據, 在接收端底層接收的時候可能早就拆成一片一片的多個數據包. TCP層中對於接收到的數據都是把它們放到緩沖中, 然後read的時候一次性copy, 這個時候是不區分一次write還是多次write的。所以對於網絡傳輸中 我們不能通過簡單的read調用知道發送端在這次交互中實際傳了多少數據. 一般來說對於具體的交互我們一般采取下面的方式來保證交互的正確,事先約定好長度, 雙方都采用固定長度的數據進行交互, read, write的時候都是讀取固定的長度.但是這樣的話升級就必須考慮兩端同時升級的問題。特殊的結束符或者約定結束方式, 比如http頭中采用連續的/r/n來做頭部的結束標志. 也有一些采用的是短連接的方式, 在read到0的時候,傳輸變長數據的時候一般采用定長頭部+變長數據的方式, 這個時候在定長的頭部會有一個字段來表示後面的變長數據的長度, 這種模式下一般需要讀取兩次確定長度的數據. 我們現在內部用的很多都是這樣的模式. 比如public/nshead就是這樣處理, 不過nshead作為通用庫另外考慮了采用 通用定長頭+用戶自定義頭+變長數據的接口。
總的來說read讀數據的時候不能只通過read的返回值來判斷到底需要讀多少數據, 我們需要額外的約定來支持, 當這種約定存在錯誤的時候我們就可以認為已經出現了問題. 另外對於write數據來說, 如果相應的數據都是已經准備好了那這個時候也是可以把數據一次性發送出去,不需要調用了多次write. 一般來說write次數過多也會對性能產生影響,另一個問題就是多次連續可能會產生延時問題,這個參看下面有關長連接延時的部分問題.
小提示
上面提到的都是TCP的情況, 不一定適合其他網絡協議. 比如在UDP中 接收到連續2個UDP包, 需要分別讀來次才讀的出來, 不能像TCP那樣,一個read可能就可以成功(假設buff長度都是足夠的)。
如何查看和觀察句柄洩露問題 一般情況句柄只有1024個可以使用,所以一般情況下比較容易出現,也可以通過觀察/proc/進程號/fd來觀察。
另外可以采用valgrind來檢查,valgrind參數中加上 --track-fds = yes 就可以看到最後退出的時候沒有被關閉的句柄,以及打開句柄的位置
為什麼socket寫錯誤,但用recv檢查依然成功?
首先采用recv檢查連接的是基於我們目前的一個請求一個應答的情況對於客戶端的請求,邏輯一般是這樣 建立連接->發起請求->接受應答->長連接繼續發請求
recv檢查一般是這樣采用下面的方式: ret = recv(sock, buf, sizeof(buf), MSG_DONTWAIT);
通過判斷ret 是否為-1並且errno是EAGAIN 在非堵塞方式下如果這個時候網絡沒有收到數據, 這個時候認為網絡是正常的
這是由於在網絡交換模式下 我們作為一個客戶端在發起請求前, 網絡中是不應該存在上一次請求留下來的髒數據或者被服務端主動斷開(服務端主動斷開會收到FIN包,這個時候是recv返回值為0), 異常斷開會返回錯誤. 當然這種方式來判斷連接是否存在並不是非常完善,在特殊的交互模式(比如異步全雙工模式)或者延時比較大的網絡中都是存在問題的,不過對於我們目前內網中的交互模式還是基本適用的. 這種方式和socket寫錯誤並不矛盾, 寫數據超時可能是由於網慢或者數據量太大等問題, 這時候並不能說明socket有錯誤, recv檢查完全可能會是正確的. 一般來說遇到socket錯誤,無論是寫錯誤還讀錯誤都是需要關閉重連.
為什麼接收端失敗,但客戶端仍然是write成功
這個是正常現象,write數據成功不能表示數據已經被接收端接收導致,只能表示數據已經被復制到系統底層的緩沖(不一定發出), 這個時候的網絡異常都是會造成接收端接收失敗的.
長連接的情況下出現了不同程度的延時 在一些長連接的條件下, 發送一個小的數據包,結果會發現從數據write成功到接收端需要等待一定的時間後才能接收到, 而改成短連接這個現象就消失了(如果沒有消失,那麼可能網絡本身確實存在延時的問題,特別是跨機房的情況下)在長連接的處理中出現了延時,而且時間固定,基本都是40ms, 出現40ms延時最大的可能就是由於沒有設置TCP_NODELAY 在長連接的交互中,有些時候一個發送的數據包非常的小,加上一個數據包的頭部就會導致浪費,而且由於傳輸的數據多了,就可能會造成網絡擁塞的情況, 在系統底層默認采用了Nagle算法,可以把連續發送的多個小包組裝為一個更大的數據包然後再進行發送. 但是對於我們交互性的應用程序意義就不大了,在這種情況下我們發送一個小數據包的請求,就會立刻進行等待,不會還有後面的數據包一起發送, 這個時候Nagle算法就會產生負作用,在我們的環境下會產生40ms的延時,這樣就會導致客戶端的處理等待時間過長, 導致程序壓力無法上去. 在代碼中無論是服務端還是客戶端都是建議設置這個選項,避免某一端造成延時。所以對於長連接的情況我們建議都需要設置TCP_NODELAY, 在我們的ub框架下這個選項是默認設置的.
小提示:
對於服務端程序而言, 采用的模式一般是
bind-> listen -> accept, 這個時候accept出來的句柄的各項屬性其實是從listen的句柄中繼承, 所以對於多數服務端程序只需要對於listen進行監聽的句柄設置一次TCP_NODELAY就可以了,不需要每次都accept一次.
設置了NODELAY選項但還是時不時出現10ms(或者某個固定值)的延時 這種情況最有可能的就是服務端程序存在長連接處理的缺陷. 這種情況一般會發生在使用我們的pendingpool模型(ub中的cpool)情況下,在 模型的說明中有提到. 由於select沒有及時跳出導致一直在浪費時間進行等待.
上面的2個問題都處理了,還是發現了40ms延時?
協議棧在發送包的時候,其實不僅受到TCP_NODELAY的影響,還受到協議棧裡面擁塞窗口大小的影響. 在連接發送多個小數據包的時候會導致數據沒有及時發送出去.
這裡的40ms延時其實是兩方面的問題:
對於發送端, 由於擁塞窗口的存在,在TCP_NODELAY的情況,如果存在多個數據包,後面的數據包可能會有延時發出的問題. 這個時候可以采用 TCP_CORK參數,
TCP_CORK 需要在數據write前設置,並且在write完之後取消,這樣可以把write的數據發送出去( 要注意設置TCP_CORK的時候不能與TCP_NODELAY混用,要麼不設置TCP_NODELAY要麼就先取消TCP_NODELAY)
但是在做了上面的設置後可能還是會導致40ms的延時, 這個時候如果采用tcpdump查看可以注意是發送端在發送了數據包後,需要等待服務端的一個ack後才會再次發送下一個數據包,這個時候服務端出現了延時返回的問題.對於這個問題可以通過設置server端TCP_QUICKACK選項來解決. TCP_QUICKACK可以讓服務端盡快的響應這個ack包.
這個問題的主要原因比較復雜,主要有下面幾個方面
當TCP協議棧收到數據的時候, 是否進行ACK響應(沒有響應是不會發下一個包的),在我們linux上返回ack包是下面這些條件中的一個
接收的數據足夠多
處於快速回復模式(TCP_QUICKACK)
存在亂序的包
如果有數據馬上返回給發送端,ACK也會一起跟著發送
如果都不滿足上面的條件,接收方會延時40ms再發送ACK,這個時候就造成了延時。
但是對於上面的情況即使是采用TCP_QUICKACK,服務端也不能保證可以及時返回ack包,因為快速回復模式在一些情況下是會失效(只能通過修改內核來實現)
目前的解決方案只能是通過修改內核來解決這個問題,STL的同學在 內核中增加了參數可以控制這個問題。
會出現這種情況的主要是連接發送多個小數據包或者采用了一些異步雙工的編程模式,主要的解決方案有下面幾種
對於連續的多個小數據包,盡量把他們打到一個buffer中間, 不過會有內存復制的問題
采用writev方式發送多個小數據包,不過writev也存在一個問題就是發送的數據包個數有限制,如果超過了IOV_MAX(我們的限制一般是1024),依然可能會出現問題,因為writev只能保證在IOV_MAX范圍內的數據是按照連續發送的。
writev或者大buffer的方式在異步雙工模式下是無法工作,這個時候只能通過系統方式來解決。 客戶端 不設置TCP_NODELAY選項,發送數據前先打開TCP_CORK選項,發送完後再關閉TCP_CORK,服務端開啟TCP_QUICKACK選項
采用STL修改的內核5-6-0-0,打開相關參數
TIME_WAIT有什麼樣的影響?
對於TIME_WAIT的出現具體可以參考<<UNIX網絡編程>>中的章節,總的來說對於一個已經建立的連接如果是主動 close, 那麼這個連接的端口(注意:不是socket)就會進入到TIME_WAIT狀態,在我們的機器上需要等待60s的時間(有些書上可能提到的是 2MSL,1MSL為1分鐘,但我們的linux實現是按照1分鐘的). 在這一段時間內,這個端口將不會被釋放,新建立的連接就無法使用這個端口(連接的時候會報Cannot assign requested address的錯誤).
可以通過/proc/sys/net/ipv4/ip_local_port_range看到可用端口的范圍,我們的機器上一般是 32768 61000,不足3W個,這樣的結果就是導致如果出現500/s的短連接請求,就會導致端口不夠用連接不上。這種情況一般修改系統參數tcp_tw_reuse或者在句柄關閉前設置SO_LINGER選項來解決,也可以通過增大 ip_local_port_range來緩解,設置SO_LINGER後句柄會被系統立刻關閉,不會進入TIME_WAIT狀態,不過在一些大壓力的情況還是有可能出現連接的替身,導致數據包丟失。系統參數/proc/sys/net/ipv4/tcp_tw_reuse設為1 會復用TIME_WAIT狀態socket,如果開啟,客戶端在調用connect調用時,會自動復用TIME_WAIT狀態的端口,相比 SO_LINGER選項更加安全。
對於服務器端如果出現TIME_WAIT狀態,是不會產生端口不夠用的情況,但是TIME_WAIT過多在服務器端還是會占用一定的內存資源,在/proc/sys/net/ipv4/tcp_max_xxx 中我們可以系統默認情況下的所允許的最大TIME_WAIT的個數,一般機器上都是180000, 這個對於應付一般程序已經足夠了.但對於一些壓力非常大的程序而言,這個時候系統會不主動進入TIME_WAIT狀態而且是直接跳過,這個時候如果去看 dmsg中的信息會看到 "TCP: time wait bucket table overflow" , 一般來說這種情況是不會產生太多的負面影響, 這種情況下後來的socket在關閉時不會進入TIME_WAIT狀態,而是直接發RST包, 並且關閉socket. 不過還是需要關注為什麼會短時間內出現這麼大量的請求。
小提示: 如果需要設置SO_LINGER選項, 需要在FD連接上之後設置才有效果
什麼情況下會出現CLOSE_WAIT狀態?
一般來說,連接的一端在被動關閉的情況下,已經接收到FIN包(對端調用close)後,這個時候如果接收到FIN包的一端沒有主動close就會出現 CLOSE_WAIT的情況。 一般來說,對於普通正常的交互,處於CLOSE_WAIT的時間很短,一般的邏輯是檢測到網絡出錯,馬上關閉。但是在一些情況下會出現大量的CLOS_WAIT, 有的甚至維持很長的時間,這個主要有幾個原因:
沒有正確處理網絡異常,特別是read 0的情況,一般來說被動關閉的時候會出現read 返回0的情況。一般的處理的方式在網絡異常的情況下就主動關閉連接句柄洩露了,句柄洩露需要關閉的連接沒有關閉而對端又主動斷開的情況下也會出現這樣的問題。連接端采用了連接池技術,同時維護了較多的長連接(比如ub_client, public/connectpool),同時服務端對於空閒的連接在一定的時間內會主動斷開(比如ub_server, ependingpool都有這樣的機制). 如果服務端由於超時或者異常主動斷開,客戶端如果沒有連接檢查的機制,不會主動關閉這個連接,比如ub_client的機制就是長連接建立後除非到使用的時候進行連接檢查,否則不會主動斷開連接。這個時候在建立連接的一端就會出現CLOSE_WAIT狀態。這個時候的狀態一般來說是安全(可控的,不會超過最大連接數). 在com 的connectpool 2中這種情況下可以通過打開健康檢查線程進行主動檢查,發現斷開後主動close.
順序發送數據,接收端出現亂序接收到的情況:
網絡壓力大的情況下,有時候會出現,發送端是按照順序發送, 但是接收端接收的時候順序不對.
一般來說在正常情況下是不會出現數據順序錯誤的情況, 但某些異常情況還是有可能導致的.
在我們的協議棧中,服務端每次建立連接其實都是從accpet所在的隊列中取出一個已經建立的fd, 但是在一些異常情況下,可能會出現短時間內建立大量連接的情況, accept的隊列長度是有限制,這裡其實有兩個隊列,一個完成隊列另一個是未完成隊列,只有完成了三次握手的連接會放到完成隊列中。如果在短時間內accept中的fd沒有被取出導致隊列變滿,但未完成隊列未滿,這個時候連接會在未完成隊列中,對於發起連接的一端來說表現的情況是連接已經成功,但實際上連接本身並沒有完成,但這個時候我們依然可以發起寫操作並且成功,只是在進行讀操作的時候,由於對端沒有響應會造成讀超時。對於超時的情況我們一般就把連接直接close關閉了,但是句柄雖然被關閉了,但是由於TIME_WAIT狀態的存在,TCP還是會進行重傳。在重傳的時候,如果完成隊列有句柄被處理,那麼此時會完成三次握手建立連接,這個時候服務端照樣會進行正常的處理(不過在寫響應的時候可能會發生錯誤)。從接收上看,由於重傳成功的情況我們不能控制,對於接收端來說就可能出現亂序的情況。完成隊列的長度和未完成隊列的長度由listen時候的baklog決定((ullib庫中ul_tcplisten的最後一個參數),在我們的 linux環境中baklog是完成隊列的長度,baklog * 1.5是兩個隊列的總長度(與一些書上所說的兩個隊列長度不超過baklog有出入). 兩個隊列的總長度最大值限制是128, 既使設置的結果超過了128也會被自動改為128。128這個限制可以通過 系統參數 /proc/sys/net/core/somaxconn 來更改, 在我們 5-6-0-0 內核版本以後,STL將其提高到2048. 另外客戶端也可以考慮使用SO_LINGER參數通過強制關閉連接來處理這個問題,這樣在close以後就不啟用重傳機制。另外的考慮就是對重試機制根據業務邏輯進行改進。
連接偶爾出現超時有哪些可能?
主要幾個方面的可能
服務端確實處理能力有限,cpu idel太低, 無法承受這樣的壓力,或者 是更後端產生問題
accept隊列設置過小,而連接又特別多,需要增大baklog,建議設置為128這是我們linux系統默認的最大值由/proc/sys/net/core/somaxconn決定,可以通過修改這個值來增大(由於很多書上這個地方設置為5,那個其實是4.2BSD支持的最大值, 而不是現在的系統, 不少程序中都直接寫5了,其實可以更大, 不過超過128還是按照128來算)
程序邏輯問題導致accept處理不過來, 導致連接隊列中的連接不斷增多直到把accept隊列撐爆, 像簡單的線程模型(每個線程一個accept), 線程被其他IO一類耗時操作handle,導致accept隊列被撐爆, 這個時候默認的邏輯是服務端丟棄數據包,導致client端出現超時,但是可以通過打開/proc/sys/net/ipv4/tcp_abort_on_overflow開關讓服務端立刻返回失敗
當讀超時的時候(或者其他異常), 我們都會把連接關閉,進行重新連接,這樣的行為如果很多,也可能造成accept處理不過來
異常情況下,設置了SO_LINGER造成連接的ack包被丟失, 雖然情況極少,但大壓力下還是有存在的.
當然還是有可能是由於網絡異常或者跨機房耗時特別多產生的, 這些就不是用戶態程序可以控制的。
另外還有發現有些程序采用epoll的單線模式, 但是IO並沒有異步化,而是阻塞IO,導致了處理不及時.
listen的時候的backlog有什麼影響?
backlog代表連接的隊列, 這裡對於內核中其實會維護2個隊列
未完成隊列, 這個是服務器端接收到連接請求後會先放到這裡(第一次握手)這個時候端口會處於SYN_RCVD狀態
已完成隊列,完成三次握手的連接會放到這裡,這個時候才是連接建立
在我們的linux環境中backlog 一般是被定義為已完成隊列的長度,為完成隊列一般是按照以完成隊列長度的一半來取,backlog為5,那麼已完成隊列為5,未完成隊列為3,總共是8個。 如果這裡的8個都被占滿了,那麼後面的連接就會失敗,這裡的行為可以由 /proc/sys/net/ipv4/tcp_abort_on_overflow 參數控制,這個參數打開後隊列滿了會發送RST包給client端,client端會看到Connection reset by peer的錯誤(線上部分內核打開了這個參數), 如果是關閉的話, 服務端會丟棄這次握手, 需要等待TCP的自動重連, 這個時間一般比較長, 默認情況下第一次需要3秒鐘, 由於我們的連接超時一般都是很小的, client采用ullib庫中的超時連接函數, 那麼會發現這個時候連接超時了。
長連接和短連接混用是否會有問題?
雖然這種方式並不合適,但嚴格來說如果程序中做好相關的守護操作(包括一些情況下系統參數的調整) 是不會出現問 題,基本來說在長短連接混用情況下出現的問題都是由於我們的程序存在不同程度上的缺陷造成的.
可能出現的問題:
只要有一端采用了短連接,那麼就可以認為總體是短連接模式。
服務端長連接,客戶端短連接
客戶端主動關閉,服務端需要接收到close的FIN包,read返回0 後才知道客戶端已經被關閉。在這一段時間內其實服務端多維護了一個沒有必要連接的狀態。在同步模式(pendingpool,ub-xpool, ub-cpool, ub-epool)中由於read是在工作線程中,這個連接相當於線程多做了一次處理,浪費了系統資源。如果是IO異步模式(ub/apool或者使用 ependingpool讀回調)則可以馬上發現,不需要再讓工作線程進行處理
服務端如果采用普通線程模型(ub-xpool)那麼在異常情況下FIN包如果沒有及時到達,在這一小段時間內這個處理線程不能處理業務邏輯。如果出現問題的地方比較多這個時候可能會有連鎖反應短時間內不能相應。
服務端為長連接,對於服務提供者來說可能早期測試也是采用長連接來進行測試,這個時候accept的baklog可能設置的很小,也不會出現問題。但是一旦被大量短連接服務訪問就可能出現問題。所以建議listen的時候baklog都設置為128, 我們現在的系統支持這麼大的baklog沒有什麼問題。
每次總是客戶端主動斷開,這導致客戶端出現了TIME_WIAT的狀態,在沒有設置SO_LINGER或者改變系統參數的情況下,比較容易出現客戶端端口不夠用的情況。
服務端短連接,客戶端長連接這個時候的問題相對比較少,但是如果客戶端在發送數據前(或者收完數據後)沒有對髒數據進行檢查,在寫的時候都會出現大量寫錯誤或者讀錯誤,做一次無用的操作,浪費系統資源一般的建議是采用長連接還是短連接,兩端保持一致,但采用配置的方式並不合適,這個需要在上線的時候檢查這些問題。比較好的方式是把采用長連接還是短連接放到數據包頭部中。客戶端發送的時候標記自己是采用短連接還是長連接,服務端接收到後按照客戶端的情況采取相應的措施,並且告知客戶端。特別的如果服務端不支持長連接,也可以告知客戶端,服務采用了短連接
要注意的是,如果采用了一些框架或者庫,在read到0的情況下可能會多打日志,這個對性能的影響可能會比較大。
select, epoll使用上的注意
select, epoll實現上的區別可以參考, 本質上來說 select, poll的實現是一樣的,epoll由於內部采用了樹的結構來維護句柄數,並且使用了通知機制,省去了輪詢的過程,在對於需要大量連接的情況下在CPU上會有一定的優勢.
select 默認情況下可以支持句柄數是1024, 這個可以看/usr/include/bits/typesizes.h 中的__FD_SETSIZE,在我們的編譯機(不是開發機,是SCMPF平台的機器)這個值已經被修改為51200, 如果select在處理fd超過1024的情況下出現問題可用檢查一下編譯程序的機器上__FD_SETSIZE是否正確.
epoll在句柄數的限制沒有像select那樣需要通過改變系統環境中的宏來實現對更多句柄的支持
另外我們發現有些程序在使用epoll的時候打開了邊緣觸發模式(EPOLLET), 采用邊緣觸發其實是存在風險的,在代碼中需要很小心,避免由於連接兩次數據到達,而被只讀出一部分的數據. EPOLLET的本意是在數據情況發生變化的時候激活(比如不可讀進入可讀狀態), 但問題是這個時候如果在一次處理完畢後不能保證fd已經進入了不可讀狀態(一般來說是讀到EAGIN的情況), 後續可能就一直不會被激活. 一般情況下建議使用EPOLLET模式.一個最典型的問題就是監聽的句柄被設置為EPOLLET, 當同時多個連接建立的時候, 我們只accept出一個連接進行處理, 這樣就可能導致後來的連接不能被及時處理,要等到下一次連接才會被激活.
小提示: ullib 中常用的ul_sreado_ms_ex,ul_swriteo_ms_ex內部是采用select的機制,即使是在scmpf平台上編譯出來也還是受到 51200的限制,可用ul_sreado_ms_ex2,和ul_swriteo_ms_ex2這個兩個接口來規避這個問題,他們內部不是采用 select的方式來實現超時控制的(需要ullib 3.1.22以後版本)
一個進程的socket句柄數只能是1024嗎?
答案是否定的,一台機器上可以使用的socket句柄數是由系統參數 /proc/sys/fs/file-max 來決定的.這裡的1024只不過是系統對於一個進程socket的限制,我們完全可以采用ulimit的參數把這個值增大,不過增大需要采用root權限,這個不是每個工程師都可以采用的.所以 在公司內采用了一個limit的程序,我們的所有的機器上都有預裝這個程序,這個程序已經通過了提權可以以root的身份設置ulimit的結果.使用的時候 limit ./myprogram 進行啟動即可,默認是可以支持51200個句柄,采用limit -n num 可以設置實際的句柄數. 如果還需要更多的連接就需要用ulimit進行專門的操作.
另外就是對於內核中還有一個宏NR_OPEN會限制fd的做大個數,目前這個值是1024*1024
小提示: linux系統中socket句柄和文件句柄是不區分的,如果文件句柄+socket句柄的個數超過1024同樣也會出問題,這個時候也需要limit提高句柄數.
ulimit對於非root權限的帳戶而言只能往小的值去設置, 在終端上的設置的結果一般是針對本次shell的, 要還原退出終端重新進入就可以了。
用limit方式啟動,程序讀寫的時候出core?
這個又是另外一個問題,前面已經提到了在網絡程序中對於超時的控制是往往會采用select或者poll的方式.select的時候對於支持的FD其實是有上限的,可以看/usr/inclue/sys/select.h中對於fd_set的聲明,其實一個__FD_SETSIZE /(8*sizeof(long))的long數組,在默認情況下__FD_SETSIZE的定義是1024,這個可以看 /usr/include/bits/typesizes.h 中的聲明,如果這個時候這個宏還是1024,那麼對於采用select方式實現的讀寫超時控制程序在處理超過1024個句柄的時候就會導致內存越界出 core .我們的程序如果是線下編譯,由於許多開發機和測試這個參數都沒有修改,這個時候就會造成出core,其實不一定出core甚至有些情況下會出現有數據但還是超時的情況. 但對於我們的SCMPF平台上編譯出來的程序是正常的,SCMPF平台上這個參數已經進行了修改,所以有時會出現QA測試沒問題,RD 自測有問題的情況。
一台機器最多可以建立多少連接?
理論上來說這個是可以非常多的,取決於可以使用多少的內存.我們的系統一般采用一個四元組來表示一個唯一的連接{客戶端ip, 客戶端端口,服務端ip, 服務端端口} (有些地方算上TCP, UDP表示成5元組), 在網絡連接中對於服務端采用的一般是bind一個固定的端口,然後監聽這個端口,在有連接建立的時候進行accept操作,這個時候所有建立的連接都只用到服務端的一個端口.對於一個唯一的連接在服務端ip和服務端端口都確定的情況下,同一個ip上的客戶端如果要建立一個連接就需要分別采用不同的端,一台機器上的端口是有限,最多65535(一個 unsigned char)個,在系統文件/proc/sys/net/ipv4/ip_local_port_range 中我們一般可以看到32768 61000 的結果,這裡表示這台機器可以使用的端口范圍是32768到61000, 也就是說事實上對於客戶端機器而言可以使用的連接數還不足3W個,當然我們可以調整這個數值把可用端口數增加到6W. 但是這個時候對於服務端的程序完全不受這個限制因為它都是用一個端口,這個時候服務端受到是連接句柄數的限制,在上面對於句柄數的說明已經介紹過了,一個進程可以建立的句柄數是由/proc/sys/fs/file-max決定上限和ulimit來控制的.所以這個時候服務端完全可以建立更多的連接,這個時候的主要問題在於如何維護和管理這麼多的連接,經典的一個連接對應一個線程的處理方式這個時候已經不適用了,需要考慮采用一些異步處理的方式來解決, 畢竟線程數的影響放在那邊
小提示:一般的服務模式都是服務端一個端口,客戶端使用不同的端口進行連接,但是其實我們也是可以把這個過程倒過來,我們客戶端只用一個端但是服務端確是不同的端口,客戶端做下面的修改原有的方式 socket分配句柄-> connect 分配的句柄 改為 socket分配句柄 ->對socket設置SO_REUSEADDR選項->像服務端一樣bind某個端口->connect 就可以實現
不過這種應用相對比較少,對於像網絡爬蟲這種情況可能相對會比較適用,只不過6w連接已經夠多了,繼續增加的意義不一定那麼大就是了.
對於一個不存在的ip建立連接是超時還是馬上返回?
這個要根據情況來看,一般情況connect一個不存在的ip地址,發起連接的服務需要等待ack的返回,由於ip地址不存在,不會有返回,這個時候會一直等到超時才返回。如果連接的是一個存在的ip,但是相應的端口沒有服務,這個時候會馬上得到返回,收到一個ECONNREFUSED(Connection refused)的結果。
但是在我們的網絡會存在一些有限制的路由器,比如我們一些機器不允許訪問外網,這個時候如果訪問的ip是一個外網ip(無論是否存在),這個時候也會馬上返回得到一個Network is unreachable的錯誤,不需要等待。