1. 程式人生 > >【轉】網路程式設計常見問題總結

【轉】網路程式設計常見問題總結

網路程式設計常見問題總結

這裡對在網路程式中遇到的一些問題進行了總結, 這裡主要針對的是我們常用的TCP socket相關的總結, 可能會存在錯誤, 有任何問題歡迎大家提出. 對於網路程式設計的更多詳細說明建議參考下面的書籍 《UNIX網路程式設計》 《TCP/IP 詳解》 《Unix環境高階程式設計》

<

div>

雙週刊40期 網路程式設計

<

div>

相關說明

<

div>

非阻塞IO和阻塞IO

在網路程式設計中對於一個網路控制代碼會遇到阻塞IO和非阻塞IO的概念, 這裡對於這兩種socket先做一下說明

<

div>

基本概念

socket的阻塞模式意味著必須要做完IO操作(包括錯誤)才會返回。 非阻塞模式下無論操作是否完成都會立刻返回,需要通過其他方式來判斷具體操作是否成功。

<

div>

設定

一般對於一個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);

<

div>

區別:

<

div>

讀:

讀本質來說其實不能是讀,在實際中, 具體的接收資料不是由這些呼叫來進行,是由於系統底層自動完成的,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不能起作用。

<

div>

寫:

寫的本質也不是進行傳送操作,而是把使用者態的資料copy到系統底層去,然後再由系統進行傳送操作,返回成功只表示資料已經copy到底層緩衝,而不表示資料以及發出,更不能表示對端已經接收到資料. 對於write(或者send)而言,在阻塞的情況是會一直等待直到write完全部的資料再返回.這點行為上與讀操作有所不同,究其原因主要 是讀資料的時候我們並不知道對端到底有沒有資料,資料是在什麼時候結束髮送的,如果一直等待就可能會造成死迴圈,所以並沒有去進行這方面的處理;而對於 write, 由於需要寫的長度是已知的,所以可以一直再寫,直到寫完.不過問題是write是可能被打斷造成write一次只write一部分資料, 所以write的過程還是需要考慮迴圈write, 只不過多數情況下一次write呼叫就可能成功. 非阻塞寫的情況下,是採用可以寫多少就寫多少的策略.與讀不一樣的地方在於,有多少讀多少是由網路傳送的那一端是否有資料傳輸到為標準,但是對 於可以寫多少是由本地的網路堵塞情況為標準的,在網路阻塞嚴重的時候,網路層沒有足夠的記憶體來進行寫操作,這時候就會出現寫不成功的情況,阻塞情況下會盡 可能(有可能被中斷)等待到資料全部發送完畢, 對於非阻塞的情況就是一次寫多少算多少,沒有中斷的情況下也還是會出現write到一部分的情況.

<

div>

超時控制:

對於網路IO,我們一般情況下都需要超時機制來避免進行操作的執行緒被handle住, 經典的做法就是採用select+非阻塞IO進行判斷,  select在超時時間內判斷是否可以讀寫操作,然後採用非堵塞讀寫,不過一般實現的時候讀操作不需要設定為非堵塞,上面已經說過讀操作只有在沒有資料 的時候才會阻塞,select的判斷成功說明存在資料,所以即使是阻塞讀在這種情況下也是可以做到非阻塞的效果,就沒有必要設定成非阻塞的情況了. 這部分的程式碼可以參考ullib中ul_sreado_ms_ex和ul_swriteo_ms_ex. ullib中對於讀寫超時存在了多套的函式, 這些說明參見下表( 在ullib的wiki中也有相關說明)
介面 存在問題 是否推薦 備註
ul_sread,ul_swrite 沒有超時控制, 只是簡單的封裝
ul_sreado_ms, ul_swriteo_ms ms級超時控制,但是超時時間是針對每次read或者write操作而言,而不是總體的超時時間, 存在超時控制不準確的問題.另外存在的問題就是write前沒有每次判斷是否可寫導致在極端網路擁塞的情況下會對CPU產生負面影響 老程式碼中還大量存在, 儘量修改為ul_sreado_ms_ex, ul_swriteo_ms_ex
ul_sreado, ul_swriteo 秒級控制, 其他問題與ul_sreado_ms, ul_swriteo_ms一樣
public/mynet下的 與ul_sreado_ms_ex, ul_swriteo_ms_ex類似,write的時候是堵塞write,需要外部設定非阻塞模式 ,網路擁塞條件下如果傳送資料包很大可能有hand住的風險
ul_sreado_ms_ex, ul_swriteo_ms_ex ul_xxx_ms系列的改進版本,去除了上面列舉的問題
ul_sreado_ms_ex2, ul_swriteo_ms_ex2 採用recv, send + setsockopt改進的超時控制,在CPU表現上好於ul_xxxx_ms_ex系列 只能針對網路,不能寫磁碟
使用中的注意: 採用ul_sreado_ms_ex讀資料也是不能保證返回大於0就一定讀到指定的資料長度, 對於讀寫操作, 都是需要判斷返回的讀長度或者寫長度是否是需要的長度, 不能簡單的判斷一下返回值是否小於0. 對於ul_sreado_ms_ex的情況如果出現了傳送端資料傳送一半就被close掉的情況就有可能導致接收端讀不到完整的資料包. errno 只有在函式返回值為負的時候才有效,如果返回0或者大於0的數, errno 的結果是無意義的. 有些時候 會出現read到0, 但是我們認為是錯誤的情況然後輸出errno造成誤解,一般建議在這種情況要同時輸出返回值和errno的結果,有些情況由於只有errno造成了對於問 題的判斷失誤。

<

div>

長連線和短連線的各種可能的問題及相應的處理

這裡主要是發起連線的客戶端的問題,這裡列出的問題主要是在採用同步模型的情況下才會存在的問題.

<

div>

短連線:

採用短連線的情況一般是考慮到下面的一些問題:
  • 後端服務的問題, 考慮最簡單的情況下一個執行緒一個連線, 如果這個連線採用了長連線那麼就需要我們處理連線的執行緒和後端保持一一對應,然後按照某些原則進行處理(n對n的關係), 但由於一方面伺服器可能增加,這樣導致需要前後端保持一致,帶來了更多的麻煩,另一方面執行緒數上不去對應處理能力也會產生影響,而短連線每次連線的時候只 需要關注當前的機器,問題相對會少一些. 其實這個問題可以採用連線池的方式來解決,後面會提到.
  • 不需要考慮由於異常帶來的髒資料
  • 負載均衡方面可以簡單考慮, 無論執行緒數是多少還是後端伺服器的數量是多少都沒有關係, 每次考慮單個連線就可以了. 當然如果負載邏輯簡單,並且機器相對固定,一個執行緒一個長連線問題也不大.
  • 規避一些問題, 在過去有些情況下出現長連線大延時,資料沒響應等問題, 測試的時候發現換短連線問題就解決了,由於時間關係就沒有再繼續追查, 事實上這些問題現在基本上都已經定位並且有相關的解決方案了.
不足: 效率不足, 由於連線操作一般會有50us~200us的時間消耗,導致短連線需要消耗更多的時間 會產生TIME_WAIT問題,需要做更多的守護

<

div>

長連線:

長連線相比短連線減少了連線的時間消耗, 可以承受更高的負載. 但在使用的時候需要考慮一些問題
  1. 髒資料, 在一些特殊情況(特別是邏輯錯誤的情況下) 會存在一些我們並不需要的資料. 這個時候的處理比較安全的方式是一旦檢測到就關閉連線, 檢測的方式在在發起請求前 用 前面 為什麼socket寫錯誤,但用recv檢查依然成功? 介紹的方式進行檢查. 不過有些程式會採用繼續讀把所有不需要的資料讀完畢(讀到 EAEGIN), 不過這種方式過分依賴邏輯了,存在了一定的風險. 不如直接斷開來的簡單
  2. 後端連線, 前面也提到了 在這種情況我們一般會採用連線池的方式來解決問題比如(public/connectpool中就可以維護不同的連線,使每個執行緒都可以均勻的獲取到控制代碼)
  3. 服務端的處理這個時候需要考慮連線的數量,簡單的方式就是一個長連線一個執行緒, 但是執行緒也不能無限增加( 增加了,可能造成大量的上下文切換使的效能下降). 我們一般在長連線的情況採用pendingpool的模型, 通過一個非同步佇列來緩衝, 這樣不需要考慮客戶端和服務端的執行緒數問題,可以任意配置(可以通過線下測試選擇合適的執行緒數)
  4. 一些特殊的問題, 主要是長連線的延時 在後面的FAQ中會有詳細的說明.
一般來說,對於我們多數的內部業務邏輯都是可以採用長連線模式,不會產生太多的問題.

<

div>

主要執行緒模型優缺點和注意事項

這裡所列出的執行緒模型,目前在我們的public/ub下都有相關的實現,在 ubFAQ中也有相關的說明,這裡主要針對這些模型的使用做相關的說明

<

div>

最簡單的執行緒模型

同時啟動多個執行緒, 每個執行緒都採用accept的方式進行阻塞獲取連線(具體實現上一般是先select在accept, 一方面規避低核心的驚群效應,另一方面可以做到優雅退出). 多個執行緒競爭一個連線, 拿到連線的執行緒就進行自己的邏輯處理, 包括讀寫IO全部都在一個執行緒中進行. 短連線每次重新accept, 長連線,第一次的時候accept然後反覆使用. 一般來說在總連線數很少的情況下效果會比較好,相對適用於少量短連線(可以允許比執行緒數多一些)和不超過執行緒總數的長連線(超過的那些連線,除非 accept的連線斷開,否則不可能會有執行緒對它進行accept). 但如果同一時候連線數過多會造成沒有工作執行緒與客戶端進行連線,客戶端會出現大量的連線失敗, 因為這個時候執行緒可能存在不能及時accept造成超時問題, 在有重試機制的情況下可能導致問題更糟糕. 有些程式在出現幾次超時之後會長時間一直有連線超時往往就是在這種情況下發生的. 這種模型的最大優點在於編寫簡單, 在正常情況下工作效果不錯. 在public/ub中的xpool就是屬於這種模型,建議針對連線數少的服務進行使用,比如一些一對一的業務邏輯.

<

div>

生產者消費者模型

普通執行緒模型在長連線方面存在使用限制(需要對於執行緒數進行變化, 而執行緒又不是無限的), 短連線在處理同時大量連線(比如流量高峰期)的時候存在問題. 生產者消費者模型是可以把這種影響減少. 一般是採用下面的模式 1.bmp 對於有資料的活動連線放到非同步佇列中, 其他執行緒競爭這個佇列獲取控制代碼然後進行相關的操作. 由於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都是採用種模式

<

div>

非同步模型

這裡只做一些簡單的介紹,更多的可以參考 上面兩者模型本質都是同步的處理業務邏輯,在一個執行緒中處理了讀請求,業務邏輯和寫回響應三個過程(很多業務更復雜,但是都是可以做相應的拆封 的), 但是讀和寫這兩個IO的處理往往需要阻塞等待, 這樣造成了執行緒被阻塞, 如果要應付慢連線(比如外圍抓取等待的時間是秒級的甚至更多), 在等待的時候其實CPU沒有幹多少事情, 這個時候就造成了浪費. 一種考慮是增加執行緒數,通過提高併發來解決這個問題, 但是我們目前的執行緒數還是有限的,不可能無限增加. 而且執行緒的增加會帶來cpu對於上下文切換的代價,另一方面多個執行緒從一個佇列中獲取可用連線, 這裡存在互斥執行緒多的時候會導致效能下降,當然這裡可以通過把一個佇列改多佇列減少互斥來實現. 引入非同步化的處理, 就是把對於IO的等待採用IO複用的方式,專門放入到一個或者若干個執行緒中去, 處理主邏輯的程式可以被釋放出來, 只有在IO處理完畢才進行處理, 這樣可以提高CPU的使用率,減少等待的時間. 一般情況下幾個執行緒(一般和CPU的核數相當)可以應付很大的流量請求 public/kylin , ub/ub(ub事件模型)都是基於純非同步思想的非同步框架。而ub中的appool是簡化版本將原本ub框架中網路IO處理進行了非同步化,不過目前只支援採用nshead頭的模式

<

div>

對一些常見錯誤號的分析

錯誤號 錯誤 可能的原因
EAGAIN Try again 在讀資料的時候,沒有資料在底層緩衝的時候會遇到,一般的處理是迴圈進行讀操作,非同步模式還會等待讀事件的發生再讀
EWOULDBLOCK Operation would block 在我們的環境中和EAGAIN是一個值, 一般情況下只關心EAGAIN就可以了
EPIPE Broken pipe 接收端關閉(緩衝中沒有多餘的資料),但是傳送端還在write.
ECONNRESET Connection reset by peer 收 到RST包 可能是 接收到資料後不進行讀取或者沒有讀取完畢直接close,另一端再呼叫write或者read操作,這個時候需要檢查一下是否存在髒資料或者一端某些情況 下斷開的情況. 另外 使用了SO_LINGER後close連線,另一端也會收到這個錯誤. 另外在epoll中一般也是可能返回EPOLLHUP事件。 連線的時候也可能出現這樣的錯誤,這個參考後面的 "listen的時候的backlog有什麼影響 "中的說明
EINTR Interrupted system call 被其他的系統呼叫中斷了, 對於控制代碼進行操作比較容易出現,一般裸用recv都是需要判斷的, 處理也很簡單, 再進行一次操作就可以了
ETIMEDOUT Connection timed out 連線超時, 但是在我們ul_sread_xxx或者ul_swrite_xx系列中也被我們用來表示讀寫超時
ECONNREFUSED Connection refused 拒絕連線, 一般在機器存在但是相應的埠上沒有資料的時候出現
ENETUNREACH Network is unreachable 網路不可達,可能是由於路器的限制不能訪問,需要檢查網路
EADDRNOTAVAIL Cannot assign requested address 不能分配本地地址,一般在埠不夠用的時候會出現,很可能是短連線的TIME_WAIT問題造成
EADDRINUSE Address already in use 地址已經被使用, 已經有相應的服務程式佔用了這個埠, 或者佔用埠的程式退出了但沒有設定埠複用
ENOTCONN Transport endpoint is not connected 連線沒有鏈上。 在一個socket出來還沒有accept或者connenct, 還有一種情況就是收到對方傳送過來的RST包,系統已經確認連線被斷開了

<

div>

注意,這些錯誤號只有在相關函式返回<0的時候才有意義,否則他們的結果其實是上一次呼叫失敗的結果. 比如 呼叫ul_sreado_ms_ex,返回0,輸出結果Interrupted system call, 由於ul_sreado_ms_ex內部多次read操作,完全有可能其中有一次read返回Interrupted system call, 但是內部會進行重試, 但最後結果是read 0. 這個時候 Interrupted system call這個資訊其實是無意義的.

FAQ

<

div>

為什麼網路程式會沒有任何預兆的就退出了

一般情況都是沒有設定忽略PIPE訊號 在我們的環境中當網路觸發broken pipe (一般情況是write的時候,沒有write完畢, 接受端異常斷開了), 系統預設的行為是直接退出。在我們的程式中一般都要在啟動的時候加上 signal(SIGPIPE, SIG_IGN); 來強制忽略這種錯誤

<

div>

write出去的資料, read的時候知道長度嗎?

嚴格來說, 互動的兩端, 一端write呼叫write出去的長度, 接收端是不知道具體要讀多長的. 這裡有幾個方面的問題
  1. write 長度為n的資料, 一次write不一定能成功(雖然小資料絕大多數都會成功), 需要迴圈多次write
  2. write雖然成功,但是在網路中還是可能需要拆包和組包, write出來的一塊資料, 在接收端底層接收的時候可能早就拆成一片一片的多個數據包.
  3. TCP層中對於接收到的資料都是把它們放到緩衝中, 然後read的時候一次性copy, 這個時候是不區分一次write還是多次write的
所以對於網路傳輸中 我們不能通過簡單的read呼叫知道傳送端在這次互動中實際傳了多少資料. 一般來說對於具體的互動我們一般採取下面的方式來保證互動的正確.
  1. 事先約定好長度, 雙方都採用固定長度的資料進行互動, read, write的時候都是讀取固定的長度.但是這樣的話升級就必須考慮兩端同時升級的問題
  2. 特殊的結束符或者約定結束方式, 比如http頭中採用連續的\r 來做頭部的結束標誌. 也有一些採用的是短連線的方式, 在read到0的時候
  3. 傳輸變長資料的時候一般採用定長頭部+變長資料的方式, 這個時候在定長的頭部會有一個欄位來表示後面的變長資料的長度, 這種模式下一般需要讀取兩次確定長度的資料. 我們現在內部用的很多都是這樣的模式. 比如public/nshead就是這樣處理, 不過nshead作為通用庫另外考慮了採用 通用定長頭+使用者自定義頭+變長資料的介面
總的來說read讀資料的時候不能只通過read的返回值來判斷到底需要讀多少資料, 我們需要額外的約定來支援, 當這種約定存在錯誤的時候我們就可以認為已經出現了問題. 另外對於write資料來說, 如果相應的資料都是已經準備好了那這個時候也是可以把資料一次性發送出去,不需要呼叫了多次write. 一般來說write次數過多也會對效能產生影響,另一個問題就是多次連續可能會產生延時問題,這個參看下面有關長連線延時的部分問題. 小提示 上面提到的都是TCP的情況, 不一定適合其他網路協議. 比如在UDP中 接收到連續2個UDP包, 需要分別讀來次才讀的出來, 不能像TCP那樣,一個read可能就可以成功(假設buff長度都是足夠的)

<

div>

<

div>

如何檢視和觀察控制代碼洩露問題

一般情況控制代碼只有1024個可以使用,所以一般情況下比較容易出現, 也可以通過觀察/proc/程序號/fd來觀察。 另外可以採用valgrind來檢查, valgrind引數中加上 --track-fds = yes 就可以看到最後退出的時候沒有被關閉的控制代碼,以及開啟控制代碼的位置

<

div>

為什麼socket寫錯誤,但用recv檢查依然成功?

首先採用recv檢查連線的是基於我們目前的一個請求一個應答的情況 對於客戶端的請求,邏輯一般是這樣 建立連線->發起請求->接受應答->長連線繼續發請求 recv檢查一般是這樣採用下面的方式: ret = recv(sock, buf, sizeof(buf), MSG_DONTWAIT); 通過判斷ret 是否為-1並且errno是EAGAIN 在非堵塞方式下如果這個時候網路沒有收到資料, 這個時候認為網路是正常的 這是由於在網路交換模式下 我們作為一個客戶端在發起請求前, 網路中是不應該存在上一次請求留下來的髒資料或者被服務端主動斷開(服務端主動斷開會收到FIN包,這個時候是recv返回值為0), 異常斷開會返回錯誤. 當然這種方式來判斷連線是否存在並不是非常完善,在特殊的互動模式(比如非同步全雙工模式)或者延時比較大的網路中都是存在問題的,不過對於我們目前內網中的互動模式還是基本適用的. 這種方式和socket寫錯誤並不矛盾, 寫資料超時可能是由於網慢或者資料量太大等問題, 這時候並不能說明socket有錯誤, recv檢查完全可能會是正確的. 一般來說遇到socket錯誤,無論是寫錯誤還讀錯誤都是需要關閉重連.

<

div>

為什麼接收端失敗,但客戶端仍然是write成功

這個是正常現象, write資料成功不能表示資料已經被接收端接收導致,只能表示資料已經被複制到系統底層的緩衝(不一定發出), 這個時候的網路異常都是會造成接收端接收失敗的.

<

div>

長連線的情況下出現了不同程度的延時

在一些長連線的條件下, 傳送一個小的資料包,結果會發現從資料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的同學在 核心中增加了引數可以控制這個問題。 會出現這種情況的主要是連線傳送多個小資料包或者採用了一些非同步雙工的程式設計模式,主要的解決方案有下面幾種
  1. 對於連續的多個小資料包, 儘量把他們打到一個buffer中間, 不過會有記憶體複製的問題
  2. 採用writev方式傳送多個小資料包, 不過writev也存在一個問題就是傳送的資料包個數有限制,如果超過了IOV_MAX(我們的限制一般是1024), 依然可能會出現問題,因為writev只能保證在IOV_MAX範圍內的資料是按照連續傳送的。
  3. writev或者大buffer的方式在非同步雙工模式下是無法工作,這個時候只能通過系統方式來解決。 客戶端 不設定TCP_NODELAY選項, 傳送資料前先開啟TCP_CORK選項,傳送完後再關閉TCP_CORK,服務端開啟TCP_QUICKACK選項
  4. 採用STL修改的核心5-6-0-0,開啟相關引數
對於這些問題更詳細的參考wiki上的下列文章

<

div>

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選項更加安全。 一般情況下還可以考慮開啟/proc/sys/net/ipv4/tcp_tw_recycle, tcp_tw_recycle開啟會去計算和動態改變每次TIME_WAIT的時間,而不是採用固定值,這樣可以讓一下處於TIME_WAIT的埠提前 釋放,不過在有些情況下會出現問題

<

div>

對於伺服器端如果出現TIME_WAIT狀態,是不會產生埠不夠用的情況,所有的連線都是用同一個埠的,從一個埠上分配出多個fd給程式 accept出來使用。但是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. 不過還是需要關注為什麼會短時間內出現這麼大量的請求. 有關TIME_WAIT的問題可以參考 下面的wiki

<

div>

小提示: 如果需要設定SO_LINGER選項, 需要在FD連線上之後設定才有效果

<

div>

什麼情況下會出現CLOSE_WAIT狀態?

一般來說,連線的一端在被動關閉的情況下,已經接收到FIN包(對端呼叫close)後,這個時候如果接收到FIN包的一端沒有主動close 就會出現CLOSE_WAIT的情況。 一般來說,對於普通正常的互動,處於CLOSE_WAIT的時間很短,一般的邏輯是檢測到網路出錯,馬上關閉。 但是在一些情況下會出現大量的CLOS_WAIT, 有的甚至維持很長的時間, 這個主要有幾個原因:
  1. 沒有正確處理網路異常, 特別是read 0的情況, 一般來說被動關閉的時候會出現read 返回0的情況。一般的處理的方式在網路異常的情況下就主動關閉連線
  2. 控制代碼洩露了,控制代碼洩露需要關閉的連線沒有關閉而對端又主動斷開的情況下也會出現這樣的問題。
  3. 連線端採用了連線池技術,同時維護了較多的長連線(比如ub_client, public/connectpool),同時服務端對於空閒的連線在一定的時間內會主動斷開(比如ub_server, ependingpool都有這樣的機制). 如果服務端由於超時或者異常主動斷開, 客戶端如果沒有連線檢查的機制,不會主動關閉這個連線, 比如ub_client的機制就是長連線建立後除非到使用的時候進行連線檢查,否則不會主動斷開連線。 這個時候在建立連線的一端就會出現CLOSE_WAIT狀態。這個時候的狀態一般來說是安全(可控的,不會超過最大連線數). 在com 的connectpool 2中這種情況下可以通過開啟健康檢查執行緒進行主動檢查,發現斷開後主動close.

<

div>

<

div>

順序傳送資料,接收端出現亂序接收到的情況

網路壓力大的情況下,有時候會出現,傳送端是按照順序傳送, 但是接收端接收的時候順序不對. 一般來說在正常情況下是不會出現資料順序錯誤的情況, 但某些異常情況還是有可能導致的. 在我們的協議棧中,服務端每次建立連線其實都是從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以後就不啟用重傳機制。另外的考慮就是對重試機制根據 業務邏輯進行改進。

<

div>

連線偶爾出現超時有哪些可能

主要幾個方面的可能
  1. 服務端確實處理能力有限, cpu idel太低, 無法承受這樣的壓力, 或者 是更後端產生問題
  2. accept佇列設定過小,而連線又特別多, 需要增大baklog,建議設定為128這是我們linux系統預設的最大值 由/proc/sys/net/core/somaxconn決定,可以通過修改這個值來增大(由於很多書上這個地方設定為5,那個其實是4.2BSD支 持的最大值, 而不是現在的系統, 不少程式中都直接寫5了,其實可以更大, 不過超過128還是按照128來算, 在我們 5-6-0-0 核心版本以後,STL將其提高到2048)
  3. 程式邏輯問題導致accept處理不過來, 導致連線佇列中的連線不斷增多直到把accept佇列撐爆, 像簡單的執行緒模型(每個執行緒一個accept), 執行緒被其他IO一類耗時操作handle,導致accept佇列被撐爆, 這個時候預設的邏輯是服務端丟棄資料包,導致client端出現超時, 但是可以通過開啟/proc/sys/net/ipv4/tcp_abort_on_overflow開關讓服務端立刻返回失敗
  4. 當讀超時的時候(或者其他異常), 我們都會把連線關閉,進行重新連線,這樣的行為如果很多,也可能造成accept處理不過來
  5. 異常情況下,設定了SO_LINGER造成連線的ack包被丟失, 雖然情況極少,但大壓力下還是有存在的.
當然還是有可能是由於網路異常或者跨機房耗時特別多產生的, 這些就不是使用者態程式可以控制的。 另外還有發現有些程式採用epoll的單線模式, 但是IO並沒有非同步化,而是阻塞IO,導致了處理不及時.

<

div>

listen的時候的backlog有什麼影響

backlog代表連線的佇列, 這裡對於核心中其實會維護2個佇列
  1. 未完成佇列, 這個是伺服器端接收到連線請求後會先放到這裡(第一次握手)這個時候埠會處於SYN_RCVD狀態
  2. 已完成佇列,完成三次握手的連線會放到這裡,這個時候才是連線建立
在我們的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庫中的超時連線函式, 那麼會發現這個時候連線超時了.

<

div>

長連線和短連線混用是否會有問題?

雖然這種方式並不合適,但嚴格來說如果程式中做好相關的守護操作(包括一些情況下系統引數的調整) 是不會出現問題,基本來說在長短連線混用情況下出現的問題都是由於我們的程式存在不同程度上的缺陷造成的. 可能出現的問題: 只要有一端採用了短連線,那麼就可以認為總體是短連線模式。 服務端長連線, 客戶端短連線 客戶端主動關閉, 服務端需要接收到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的情況下可能會多打日誌,這個對效能的影響可能會比較大。

<

div>

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以後版本) ullib 中現在對於所有fd的處理在有select的地方都改成了poll, poll使用時候沒有這個巨集的限制。採用最新版本的ullib就可以避免這個巨集產生的問題

<

div>

一個程序的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的, 要還原退出終端重新進入就可以了.

<

div>

用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 自測有問題的情況. (注意,這裡的fd數量是全部的fd,包括磁碟檔案)

<

div>

一臺機器最多可以建立多少連線?

理論上來說這個是可以非常多的,取決於可以使用多少的記憶體.我們的系統一般採用一個四元組來表示一個唯一的連線{客戶端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連線已經夠多了,繼續增加的意義不一定那麼大就是了.

<

div>

對於一個不存在的ip建立連線是超時還是馬上返回?

這個要根據情況來看, 一般情況connect一個不存在的ip地址,發起連線的服務需要等待ack的返回,由於ip地址不存在,不會有返回,這個時候會一直等到超時才返回。如 果連線的是一個存在的ip,但是相應的埠沒有服務,這個時候會馬上得到返回,收到一個ECONNREFUSED(Connection refused)的結果。 但是在我們的網路會存在一些有限制的路由器,比如我們一些機器不允許訪問外網,這個時候如果訪問的ip是一個外網ip(無論是否存在),這個時候也會馬上返回得到一個Network is unreachable的錯誤,不需要等待。

<

div>

各種超時怎麼設定?

對於超時時間的設定相對比較複雜,取決於各種業務不同的邏輯 外網情況比較複雜, 這裡主要針對內網小資料包的情況進行分析
  • 連線超時: 一般來說連線超時主要發生在下游服務的accept佇列滿掉和下游服務掛掉的情況下. 對於accept佇列滿的情況下參考前面 對於backlog的說明,可以通過下游設定tcp_abort_on_overflow來解決. 對於TCP來說預設情況下如果出現丟包,那麼需要3s的時間才能重發, 對於內網來說設定超時1s和2s沒有多大區別考慮到執行緒的使用可以減少這樣的超時時間.
  • 寫超時: 一般情況寫超時是很少見的,只有在網路壓力過大的情況下才容易出現。 寫的資料 如果在K級,write的時間基本可以忽略不記。TCP由於網路丟包,對於write來說需要200ms才可以重發,這個時間內50ms, 100ms也沒有多大的差別。對於小資料包這個時間建議設定小於200ms, 因為write超時的情況一般不容易出現
  • 讀超時: 服務性程式的讀超時,其實不是讀超時, 是在等待後端資料的資料時間。這個超時取決於後端的處理業務邏輯處理消耗的時間

<

div>對於大資料量,由於本身資料傳輸就存在大量時間消耗,可以適當的