網絡編程常見問題總結 串講(一)
網絡編程常見問題總結
6 I& I! E- x8 Z+ p- U- B
在網絡程序中遇到的一些問題進行了總結, 這裏主要針對的是咱們經常使用的
TCP socket
相關的總結, 可能會存在錯誤, 有任何問題歡迎你們提出.
. e3 Y0 @* _- e1 G- B% R
對於網絡編程的更多詳細說明建議參考下面的書籍
《UNIX網絡編程》 《TCP/IP 詳解》 《Unix環境高級編程》 $ ^, `# d2 h9 r6 ~, b* Z: F. L' n
非阻塞IO和阻塞IO: % k, j L4 b1 q3 d9 m+ D x% I
在網絡編程中對於一個網絡句柄會遇到阻塞IO和非阻塞IO的概念, 這裏對於這兩種socket先作一下說明
5 /% b8 U! i; /) `
基本概念:
socket的阻塞模式意味着必需要作完IO操做(包括錯誤)纔會返回。 非阻塞模式下不管操做是否完成都會馬上返回,須要經過其餘方式來判斷具體操做是否成功。
設置:
通常對於一個socket是阻塞模式仍是非阻塞模式有兩種方式 fcntl設置和recv,send系列的參數.
' J% f& o: ?; S$ w2 V) p
fcntl函數能夠將一個socket句柄設置成非阻塞模式:
flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
設置以後每次的對於sockfd的操做都是非阻塞的
6 B$ b8 i" _' k: U5 w$ B
recv, send函數的最後有一個flag參數能夠設置成MSG_DONTWAIT
臨時
將sockfd設置爲非阻塞模式,而不管原有是阻塞仍是非阻塞。 recv(sockfd, buff, buff_size, MSG_DONTWAIT); send(scokfd, buff, buff_size, MSG_DONTWAIT);
* l( V- |' G1 U
區別:
讀:
讀本質來講其實不能是讀,在實際中, 具體的接收數據不是由這些調用來進行,是因爲系統底層自動完成的,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不能起做用。
寫:
/ E/ m& A+ B+ r
寫的本質也不是進行發送操做
,而是把用戶態的數據copy到系統底層去
,
而後再由系統進行發送操做,返回成功只表示數據已經copy到底層緩衝,而不表示數據以及發出,更不能表示對端已經接收到數據
.
對於write(或 者send)而言,在阻塞的狀況是會一直等待直到write徹底部的數據再返回.這點行爲上與讀操做有所不一樣,究其緣由主要是讀數據的時候咱們並不知道對端到底有沒有數據,數據是在何時結束髮送的,若是一直等待就可能會形成死循環,因此並無去進行這方面的處理;而對於write, 因爲須要寫的長度是已知的,因此能夠一直再寫,直到寫完.不過問題是write是可能被打斷形成write一次只write一部分數據, 因此write的過程仍是須要考慮循環write, 只不過多數狀況下一次write調用就可能成功.
非阻塞寫的狀況下,是採用能夠寫多少就寫多少的策略.與讀不同的地方在於,有多少讀多少是由網絡發送的那一端是否有數據傳輸到爲標準,可是對於能夠寫多少是由本地的網絡堵塞狀況爲標準的,在網絡阻塞嚴重的時候,網絡層沒有足夠的內存來進行寫操做,這時候就會出現寫不成功的狀況,阻塞狀況下會盡量(有可能被中斷)等待到數據所有發送完畢,對於非阻塞的狀況就是一次寫多少算多少,沒有中斷的狀況下也仍是會出現write到一部分的狀況.
網絡編程常見問題總結 串講(二)
超時控制: * Z5 a- [0 {, v: w
對於網絡IO,咱們通常狀況下都須要超時機制來避免進行操做的線程被handle住,經典的作法就是採用select+非阻塞IO進行判斷,select在超時時間內判斷是否能夠讀寫操做,而後採用非堵塞讀寫,不過通常實現的時候讀操做不須要設置爲非堵塞,上面已經說過讀操做只有在沒有數據的 時候纔會阻塞,select的判斷成功說明存在數據,因此即便是阻塞讀在這種狀況下也是能夠作到非阻塞的效果,就沒有必要設置成非阻塞的狀況了.
這部分的代碼能夠參考ullib中ul_sreado_ms_ex和ul_swriteo_ms_ex.
% G0 J d: g% C4
採用ul_sreado_ms_ex讀數據也是不能保證返回大於0就必定讀到指定的數據長度, 對於讀寫操做, 都是須要判斷返回的讀長度或者寫長度是不是須要的長度, 不能簡單的判斷一下返回值是否小於0. 對於ul_sreado_ms_ex的狀況若是出現了發送端數據發送一半就被close掉的狀況就有可能致使接收端讀不到完整的數據包.
errno 只有在函數返回值爲負的時候纔有效,若是返回0或者大於0的數, errno 的結果是無心義的. 有些時候 會出現read到0, 可是咱們認爲是錯誤的狀況而後輸出errno形成誤解,通常建議在這種狀況要同時輸出返回值和errno的結果,有些狀況因爲只有errno形成了對於問 題的判斷失誤。
; j; W& H* d6 _
8 |* J$ m. |$ n;
長鏈接和短鏈接的各類可能的問題及相應的處理 ' N9 C; f! {% R& ]" [
這裏主要是發起鏈接的客戶端的問題,這裏列出的問題主要是在採用同步模型的狀況下才會存在的問題.
短鏈接:
J/ E. u5 V: L
採用短鏈接的狀況通常是考慮到下面的一些問題:
後端服務的問題, 考慮最簡單的狀況下一個線程一個鏈接, 若是這個鏈接採用了長鏈接那麼就須要咱們處理鏈接的線程和後端保持一一對應,而後按照某些原則進行處理(n對n的關係), 但因爲一方面服務器可能增長,這樣致使須要先後端保持一致,帶來了更多的麻煩,另外一方面線程數上不去對應處理能力也會產生影響,而短鏈接每次鏈接的時候只 須要關注當前的機器,問題相對會少一些. 其實這個問題能夠採用鏈接池的方式來解決,後面會提到. 不須要考慮因爲異常帶來的髒數據。負載均衡方面能夠簡單考慮, 不管線程數是多少仍是後端服務器的數量是多少都沒有關係, 每次考慮單個鏈接就能夠了. 固然若是負載邏輯簡單,而且機器相對固定,一個線程一個長鏈接問題也不大.
規避一些問題, 在過去有些狀況下出現長鏈接大延時,數據沒響應等問題, 測試的時候發現換短鏈接問題就解決了,因爲時間關係就沒有再繼續追查, 事實上這些問題如今基本上都已經定位而且有相關的解決方案了.
不足:
效率不足, 因爲鏈接操做通常會有50ns~200ns的時間消耗,致使短鏈接須要消耗更多的時間會產生TIME_WAIT問題,須要作更多的守護
長鏈接:
長鏈接相比短鏈接減小了鏈接的時間消耗, 能夠承受更高的負載. 但在使用的時候須要考慮一些問題髒數據, 在一些特殊狀況(特別是邏輯錯誤的狀況下) 會存在一些咱們並不須要的數據. 這個時候的處理比較安全的方式是一旦檢測到就關閉鏈接, 檢測的方式在在發起請求前用前面爲何socket寫錯誤,但用recv檢查依然成功? 介紹的方式進行檢查. 不過有些程序會採用繼續讀把全部不須要的數據讀完畢(讀到 EAEGIN), 不過這種方式過度依賴邏輯了,存在了必定的風險. 不如直接斷開來的簡單 後端鏈接, 前面也提到了 在這種狀況咱們通常會採用鏈接池的方式來解決問題好比(public/connectpool中就能夠維護不一樣的鏈接,使每一個線程均可以均勻的獲取到句 柄) 服務端的處理這個時候須要考慮鏈接的數量,簡單的方式就是一個長鏈接一個線程, 可是線程也不能無限增長( 增長了,可能形成大量的上下文切換使的性能降低). 咱們通常在長鏈接的狀況採用pendingpool的模型, 經過一個異步隊列來緩衝, 這樣不須要考慮客戶端和服務端的線程數問題,能夠任意配置(能夠經過線下測試選擇合適的線程數)
一些特殊的問題, 主要是長鏈接的延時 在後面的FAQ中會有詳細的說明.
2 A( }! ^5 ~1 O9 B+ V) /
通常來講,對於咱們多數的內部業務邏輯都是能夠採用長鏈接模式,不會產生太多的問題.
網絡編程常見問題總結 串講(三)
主要線程模型優缺點和注意事項
這裏所列出的線程模型,目前在咱們的public/ub下都有相關的實現,在 ubFAQ中也有相關的說明,這裏主要針對這些模 型的使用作相關的說明
# X9 s# ^! a! k( X( ^6 w
最簡單的線程模型 1 P% c; W) N+ M* e8 L- x6 y 同時啓動多個線程,
每一個線程都採用accept的方式進行阻塞獲取鏈接(具體實現上通常是先select在accept, 一方面規避低內核的驚羣效應,另外一方面能夠作到優雅退出). 多個線程競爭一個鏈接, 拿到鏈接的線程就進行本身的邏輯處理, 包括讀寫IO所有都在一個線程中進行. 短鏈接每次從新accept, 長鏈接,第一次的時候accept而後反覆使用.通常來講在總鏈接數不多的狀況下效果會比較好,相對適用於少許短鏈接(能夠容許比線程數多一些)和不超過線程總數的長鏈接(超過的那些鏈接,除非 accept的鏈接斷開,不然不可能會有線程對它進行accept).
& }( r# p3 ? Y+ ^8 A: ^9 t
但若是同一時候鏈接數過多會形成沒有工做線程與
客戶端進行鏈接,客戶端會出現大量的鏈接失敗, 由於這個時候線程可能存在不能及時accept形成超時問題, 在有重試機制的狀況下可能致使問題更糟糕. 有些程序在出現幾回超時以後會長時間一直有鏈接超時每每就是在這種狀況下發生的.
3 x) V3 l! o1 c1 ^
這種模型的最大優勢在於編寫簡單, 在正常狀況下工做效果不錯. 在public/ub中的xpool就是屬於這種模型,建議針對鏈接數少的服務進行使用,好比一些一對一的業務邏輯.
" t' X- p) Z( u% c: @
生產者消費者模型
普通線程模型在長鏈接方面存在使用限制(須要對於線程數進行變化, 而線程又不是無限的), 短鏈接在處理同時大量鏈接(好比流量高峯期)的時候存在問題.
6 N" t9 m5 j" J0 C) p8 x- G
生產者消費者模型是能夠把這種影響減小.
2 |5 v& p) b( h! M
對於有數據的活動鏈接放到異步隊列中, 其餘線程競爭這個隊列獲取句柄而後進行相關的操做. 因爲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都是採用種模式
- g2 k& T) [! a7 h' H* O2 E- x
異步模型
這裏只作一些簡單的介紹。
上 面二者模型本質都是同步的處理業務邏輯,在一個線程中處理了讀請求,業務邏輯和寫回響應三個過程(不少業務更復雜,可是都是能夠作相應的拆封的), 可是讀和寫這兩個IO的處理每每須要阻塞等待, 這樣形成了線程被阻塞, 若是要應付慢鏈接(好比外圍抓取等待的時間是秒級的甚至更多), 在等待的時候其實CPU沒有幹多少事情, 這個時候就形成了浪費. 一種考慮是增長線程數,經過提升併發來解決這個問題, 可是咱們目前的線程數仍是有限的,不可能無限增長. 並且線程的增長會帶來cpu對於上下文切換的代價,另外一方面多個線程從一個隊列中獲取可用鏈接, 這裏存在互斥線程多的時候會致使性能降低,固然這裏能夠經過把一個隊列改多隊列減小互斥來實現.
, Q; R# Q' O4 j2 V0 E! K$ W v; ^! ?
引入異步化的處理, 就是把對於IO的等待採用IO複用的方式,專門放入到一個或者若干個線程中去, 處理主邏輯的程序能夠被釋放出來, 只有在IO處理完畢才進行處理, 這樣能夠提升CPU的使用率,減小等待的時間. 通常狀況下幾個線程(通常和CPU的核數至關)能夠應付很大的流量請求 public/kylin , ub/ub(ub事件模型)都是基於純異步思想的異步框架。而ub中的appool是簡化版本將本來ub框架中網絡IO處理進行了異步化,不過目前只支持 採用nshead頭的模式。
網絡編程常見問題總結 串講(四)
爲何網絡程序會沒有任何預兆的就退出了 , ~& |- h; d2 ^, }- Q; T- ^$ G: b
通常狀況都是沒有設置忽略PIPE信號 ,
在咱們的環境中當網絡觸發broken pipe (通常狀況是write的時候,沒有write完畢, 接受端異常斷開了), 系統默認的行爲是直接退出。在咱們的程序中通常都要在啓動的時候加上 signal(SIGPIPE, SIG_IGN); 來強制忽略這種錯誤
write出去的數據, read的時候知道長度嗎?
嚴格來講, 交互的兩端, 一端write調用write出去的長度, 接收端是不知道具體要讀多長的. 這裏有幾個方面的問題
write 長度爲n的數據, 一次write不必定能成功(雖然小數據絕大多數都會成功), 須要循環屢次write
0 }% M5 t/ }3 o7 ,
write雖然成功,可是在網絡中仍是可能須要拆包和組包, write出來的一塊數據, 在接收端底層接收的時候可能早就拆成一片一片的多個數據包. TCP層中對於接收到的數據都是把它們放到緩衝中, 而後read的時候一次性copy, 這個時候是不區分一次write仍是屢次write的。因此對於網絡傳輸中 咱們不能經過簡單的read調用知道發送端在此次交互中實際傳了多少數據. 通常來講對於具體的交互咱們通常採起下面的方式來保證交互的正確,事先約定好長度, 雙方都採用固定長度的數據進行交互, read, write的時候都是讀取固定的長度.可是這樣的話升級就必須考慮兩端同時升級的問題。特殊的結束符或者約定結束方式, 好比http頭中採用連續的/r/n來作頭部的結束標誌. 也有一些採用的是短鏈接的方式, 在read到0的時候,傳輸變長數據的時候通常採用定長頭部+變長數據的方式, 這個時候在定長的頭部會有一個字段來表示後面的變長數據的長度, 這種模式下通常須要讀取兩次肯定長度的數據. 咱們如今內部用的不少都是這樣的模式. 好比public/nshead就是這樣處理, 不過nshead做爲通用庫另外考慮了採用 通用定長頭+用戶自定義頭+變長數據的接口。
總的來講read讀數 據的時候不能只經過read的返回值來判斷到底須要讀多少數據, 咱們須要額外的約定來支持, 當這種約定存在錯誤的時候咱們就能夠認爲已經出現了問題. 另外對於write數據來講, 若是相應的數據都是已經準備好了那這個時候也是能夠把數據一次性發送出去,不須要調用了屢次write. 通常來講write次數過多也會對性能產生影響,另外一個問題就是屢次連續可能會產生延時問題,這個參看下面有關長鏈接延時的部分問題.
& O ~9 E# T0 g, G% @, g% G
小提示
上面提到的都是TCP的狀況, 不必定適合其餘網絡協議. 好比在UDP中 接收到連續2個UDP包, 須要分別讀來次纔讀的出來, 不能像TCP那樣,一個read可能就能夠成功(假設buff長度都是足夠的)。
0 q4 S' U4 W6 h! y) {6
如何查看和觀察句柄泄露問題 通常狀況句柄只有1024個可使用,因此通常狀況下比較容易出現, 也能夠經過觀察/proc/進程號/fd來觀察。
( Y1 b$ ]6 m/ N7 _
另外能夠採用valgrind來檢查, valgrind參數中加上 --track-fds = yes 就能夠看到最後退出的時候沒有被關閉的句柄,以及打開句柄的位置
爲何socket寫錯誤,但用recv檢查依然成功?
7 J: s, F- `) r, I
首先採用recv檢查鏈接的是基於咱們目前的一個請求一個應答的狀況對於客戶端的請求,邏輯通常是這樣 創建鏈接->發起請求->接受應答->長鏈接繼續發請求
$ O7 e9 j; M, T9 i6 Q$ B
recv檢查通常是這樣採用下面的方式: ret = recv(sock, buf, sizeof(buf), MSG_DONTWAIT);
經過判斷ret 是否爲-1而且errno是EAGAIN 在非堵塞方式下若是這個時候網絡沒有收到數據, 這個時候認爲網絡是正常的。
這是因爲在網絡交換模式下 咱們做爲一個客戶端在發起請求前, 網絡中是不該該存在上一次請求留下來的髒數據或者被服務端主動斷開(服務端主動斷開會收到FIN包,這個時候是recv返回值爲0), 異常斷開會返回錯誤. 固然這種方式來判斷鏈接是否存在並非很是完善,在特殊的交互模式(好比異步全雙工模式)或者延時比較大的網絡中都是存在問題的,不過對於咱們目前內網中的交互模式仍是基本適用的. 這種方式和socket寫錯誤並不矛盾, 寫數據超時多是因爲網慢或者數據量太大等問題, 這時候並不能說明socket有錯誤, recv檢查徹底可能會是正確的.
通常來講遇到socket錯誤,不管是寫錯誤還讀錯誤都是須要關閉重連.
爲何接收端失敗,但客戶端仍然是write成功
+ n. i/ B' N: T& g' M
這個是正常現象,
write數據成功不能表示數據已經被接收端接收致使,只能表示數據已經被複制到系統底層的緩衝(不必定發出), 這個時候的網絡異常都是會形成接收端接收失敗的.
長鏈接的狀況下出現了不一樣程度的 延時 在一些長鏈接的條件下, 發送一個小的數據包,結果會發現從數據write成功到接收端須要等待必定的時間後才能接收到, 而改爲短鏈接這個現象就消失了(若是沒有消失,那麼可能網絡自己確實存在延時的問題,特別是跨機房的狀況下) 在長鏈接的處理中出現了延時,並且時間固定,基本都是40ms, 出現40ms延時最大的可能就是因爲沒有設置TCP_NODELAY 在長鏈接的交互中,有些時候一個發送的數據包很是的小,加上一個數據包的頭部就會致使浪費,並且因爲傳輸的數據多了,就可能會形成網絡擁塞的狀況, 在系統底層默認採用了Nagle算法,能夠把連續發送的多個小包組裝爲一個更大的數據包而後再進行發送. 可是對於咱們交互性的應用程序意義就不大了,在這種狀況下咱們發送一個小數據包的請求,就會馬上進行等待,不會還有後面的數據包一塊兒發送, 這個時候Nagle算法就會產生負做用,在咱們的環境下會產生40ms的延時,這樣就會致使客戶端的處理等待時間過長, 致使程序壓力沒法上去. 在代碼中不管是服務端仍是客戶端都是建議設置這個選項,避免某一端形成延時
。因此對於長鏈接的狀況咱們建議都須要設置TCP_NODELAY
, 在咱們的ub框架下這個選項是默認設置的.
5 y# /" L) o: s& ^% h8 L7 _1 G
小提示: $ r) s/ X; n' z% Q. X: K% c3 e9 h
對於服務端程序而言, 採用的模式通常是
/ Q) h7 t% B7 p5 }5 B6 _9 |' f
bind-> listen -> accept, 這個時候accept出來的句柄的各項屬性實際上是從listen的句柄中繼承, 因此對於多數服務端程序只須要對於listen進行監聽的句柄設置一次TCP_NODELAY就能夠了,不須要每次都accept一次.
z: I, O) C+ w2 a3 _: y
設置了NODELAY選項但仍是時不時出現10ms(或者某個固定值)的延時 這種狀況最有可能的就是服務端程序存在長鏈接處理的缺陷. 這種狀況通常會發生在使用咱們的pendingpool模型(ub中的cpool)狀況下,在 模型的說明中有提到. 因爲select沒有及時跳出致使一直在浪費時間進行等待.
上面的2個問題都處理了,仍是發現了40ms延時?
協議棧在發送包的時候,其實不只受到TCP_NODELAY的影響,還受到協議棧裏面擁塞窗口大小的影響. 在鏈接發送多個小數據包的時候會致使數據沒有及時發送出去.
這裏的40ms延時實際上是兩方面的問題:
: L) Z s# f0 G& Q7 B
對於發送端, 因爲擁塞窗口的存在,在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也會一塊兒跟着發送
* m. y3 y: u8 Q# ] @4 L3 c
若是都不知足上面的條件,接收方會延時40ms再發送ACK, 這個時候就形成了延時。
可是對於上面的狀況即便是採用TCP_QUICKACK,服務端也不能保證能夠及時返回ack包,由於快速回復模式在一些狀況下是會失效(只能經過修改內核來實現)
目前的解決方案只能是經過修改內核來解決這個問題,STL的同窗在 內核中增長了參數能夠控制這個問題。
會出現這種狀況的主要是鏈接發送多個小數據包或者採用了一些異步雙工的編程模式,主要的解決方案有下面幾種
3 O( /! U$ O( n! Q6 `; A
對於連續的多個小數據包, 儘可能把他們打到一個buffer中間, 不過會有內存複製的問題
6 i8 D! h0 W. H8 k& H(
採用writev方式發送多個小數據包, 不過writev也存在一個問題就是發送的數據包個數有限制,若是超過了IOV_MAX(咱們的限制通常是1024), 依然可能會出現問題,由於writev只能保證在IOV_MAX範圍內的數據是按照連續發送的。
& `8 O, e. K. P- K
writev或者大buffer的方式在異步雙工模式下是沒法工做,這個時候只能經過系統方式來解決。 客戶端 不設置TCP_NODELAY選項, 發送數據前先打開TCP_CORK選項,發送完後再關閉TCP_CORK,服務端開啓TCP_QUICKACK選項
採用STL修改的內核5-6-0-0,打開相關參數
網絡編程常見問題總結 串講(五)
5 q! C2 e, S' x; P+ E" {5 A TIME_WAIT有什麼樣的影響?
對於TIME_WAIT的出現具體能夠參考<<UNIX網絡編程>>中的章節,
總的來講對於一個已經創建的鏈接若是是主動 close, 那麼這個鏈接的端口(注意:不是socket)就會進入到TIME_WAIT狀態,在咱們的機器上須要等待60s的時間(有些書上可能提到的是 2MSL,1MSL爲1分鐘,但咱們的linux實現是按照1分鐘的). 在這一段時間內,這個端口將不會被釋放,新創建的鏈接就沒法使用這個端口
(鏈接的時候會報Cannot assign requested address的錯誤).
7 G% j5 b0 Q9 [* b I( Z1 Z! H# u
能夠經過/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選項更加安全。
! f' H( V$ E* ^8 g' b- C1 m
對於服務器端若是出現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鏈接上以後設置纔有效果
: O% B: j3 o/ A
什麼狀況下會出現CLOSE_WAIT狀態? ' n4 P$ d1 K8 n7 X2 M" A
通常來講,鏈接的一端在被動關閉的狀況下,已經接收到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,致使了處理不及時.
網絡編程常見問題總結 串講(七)
8 c, ?9 X0 n: C" F% R; X
listen的時候的backlog有什麼影響?
4 n1 b% K2 Y* V: I) I
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庫中的超時鏈接函數, 那麼會發現這個時候鏈接超時了。
長鏈接和短鏈接混用是否會有問題?
雖然這種方式並不合適,但嚴格來講若是程序中作好相關的守護操做(包括一些狀況下系統參數的調整) 是不會出現問 題,基原本說在長短鏈接混用狀況下出現的問題都是因爲咱們的程序存在不一樣程度上的缺陷形成的.
可能出現的問題:
2 D9 P" M* z# C2 X
只要有一端採用了短鏈接,那麼就能夠認爲整體是短鏈接模式。
2 S# O1 /$ Q+ j/ o+ N* T! ]6 T
服務端長鏈接, 客戶端短鏈接
客戶端主動關 閉, 服務端須要接收到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沒有什麼問題。
2 `: @1 g! ~+ L; X! B8 |% r
每次老是客戶端主動斷開,這致使客戶端出現了TIME_WIAT的狀態,在沒有設置SO_LINGER或者改變系統參數的狀況下,比較容易出現客戶端端口不夠用的狀況。
服務端短鏈接,客戶端長鏈接這個時候的問 題相對比較少, 可是若是客戶端在發送數據前(或者收完數據後)沒有對髒數據進行檢查,在寫的時候都會出現大量寫錯誤或者讀錯誤,作一次無用的操做,浪費系統資源 通常的建議是採用長鏈接仍是短鏈接,兩端保持一致, 但採用配置的方式並不合適,這個須要在上線的時候檢查這些問題。比較好的方式是把採用長鏈接仍是短鏈接放到數據包頭部中。客戶端發送的時候標記本身是採用 短鏈接仍是長鏈接,服務端接收到後按照客戶端的狀況採起相應的措施,而且告知客戶端。特別的若是服務端不支持長鏈接,也能夠告知客戶端,服務採用了短連 接
要注意的是,若是採用了一些框架或者庫, 在read到0的狀況下可能會多打日誌,這個對性能的影響可能會比較大。
網絡編程常見問題總結 串講(八)
% D* h& /* ~, V7 i
select, epoll使用上的注意
5 {4 B( E! c; {/ {$ R$ }: [9 x
select, epoll實現上的區別能夠參考, 本質上來講 select, poll的實現是同樣的,epoll因爲內部採用了樹的結構來維護句柄數,而且使用了通知機制,省去了輪詢的過程,在對於須要大量鏈接的狀況下在CPU上會有必定的優點.
select默認狀況下能夠支持句柄數 是1024, 這個能夠看/usr/include/bits/typesizes.h 中的__FD_SETSIZE, 在咱們的編譯機(不是開發機,是SCMPF平臺的機器)這個值已經被修改成51200, 若是select在處理fd超過1024的狀況下出現問題可用檢查一下編譯程序的機器上__FD_SETSIZE是否正確.
epoll在句柄數的限制沒有像select那樣須要經過改變系統環境中的宏來實現對更多句柄的支持
# Y" ^, K" W6 {5 T5 z
另外咱們發現有些程序在使用epoll的時候打開了邊緣觸發模式(EPOLLET), 採用邊緣觸發實際上是存在風險的,在代碼中須要很當心,避免因爲鏈接兩次數據到達,而被只讀出一部分的數據. EPOLLET的本意是在數據狀況發生變化的時候激活(好比不可讀進入可讀狀態), 但問題是這個時候若是在一次處理完畢後不能保證fd已經進入了不可讀狀態(通常來講是讀到EAGIN的狀況), 後續可能就一直不會被激活. 通常狀況下建議使用EPOLLET模式.一個最典型的問題就是監聽的句柄被設置爲EPOLLET, 當同時多個鏈接創建的時候, 咱們只accept出一個鏈接進行處理, 這樣就可能致使後來的鏈接不能被及時處理,要等到下一次鏈接纔會被激活.
% c! v2 q' E2 A/ Z' @
小提示: 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之後版本)
- q$ G6 r. T- Y+ k
一個進程的socket句柄數只能是1024嗎?
, L. `) S. o2 R
答案是否認的, 一臺機器上可使用的socket句柄數是由系統參數 /proc/sys/fs/file-max 來決定的.這裏的1024只不 過是系統對於一個進程socket的限制,咱們徹底能夠採用ulimit的參數把這個值增大,不過增大須要採用root權限,這個不是每一個工程師均可以採 用的.因此 在公司內採用了一個limit的程序,咱們的全部的機器上都有預裝這個程序,這個程序已經經過了提權能夠以root的身份設置ulimit的 結果.使用的時候 limit ./myprogram 進行啓動便可, 默認是能夠支持51200個句柄,採用limit -n num 能夠設置實際的句柄數. 若是還須要更多的鏈接就須要用ulimit進行專門的操做.
( T% B# y. G, j4 F/ E' J/ r1 h# K9 v
另外就是對於內核中還有一個宏NR_OPEN會限制fd的作大個數,目前這個值是1024*1024
小提示: linux系統中socket句柄和文件句柄是不區分的,若是文件句柄+socket句柄的個數超過1024一樣也會出問題,這個時候也須要limit提升句柄數.
ulimit對於非root權限的賬戶而言只能往小的值去設置, 在終端上的設置的結果通常是針對本次shell的, 要還原退出終端從新進入就能夠了。
用limit方式啓動,程序讀寫的時候出core?
0 M; G# S2 W v
這個又是另一個問題,前面已經提到了在網絡程序中對於超時的控制是每每會採用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來控制的.因此這個時候服務端徹底能夠創建更多的鏈接,這個 時候的主要問題在於如何維護和管理這麼多的鏈接,經典的一個鏈接對應一個線程的處理方式這個時候已經不適用了,須要考慮採用一些異步處理的方式來解決, 畢竟線程數的影響放在那邊
}$ u( [; s( N4 J9 d
小提示: 通常的服務模式都是服務端一個端口,客戶端使用不一樣的端口進行鏈接,可是其實咱們也是能夠把這個過程倒過來,咱們客戶端只用一個端可是服務端確是不一樣的端 口,客戶端作下面的修改原有的方式 socket分配句柄-> connect 分配的句柄 改成 socket分配句柄 ->對socket設置SO_REUSEADDR選項->像服務端同樣bind某個端口->connect 就能夠實現
不過這種應用相對比較少,對於像網絡爬蟲這種狀況可能相對會比較適用,只不過6w鏈接已經夠多了,繼續增長的意義不必定那麼大就是了.
' l- ?) D* @3 R, x) q7 Z' L' f
對於一個不存在的ip創建鏈接是超時仍是立刻返回?
8 a; k9 H. e7 o' m 這個要根據狀況來看, 通常狀況connect一個不存在的ip地址,發起鏈接的服務須要等待ack的返回,因爲ip地址不存在,不會有返回,這個時候會一直等到超時才返回。如 果鏈接的是一個存在的ip,可是相應的端口沒有服務,這個時候會立刻獲得返回,收到一個ECONNREFUSED(Connection refused)的結果。 可是在咱們的網絡會存在一些有限制的路由器,好比咱們一些機器不容許訪問外網,這個時候若是訪問的ip是一個外網ip(不管是否存在),這個時候也會立刻返回獲得一個Network is unreachable的錯誤,不須要等待。