在網絡服務中,雙方的關係是臨時創建的,而且這種關係不是基於徹底信任的基礎上,服務器不能認爲全部的客戶端都是正常訪問的客戶端,客戶端也不能徹底信任服務器能正確高效的作出請求的迴應。因而就須要必定的機制判斷出這種不正常。本文主要討論網絡服務中客戶端與服務器之間的鏈接超時,發送超時,接受超時和idel超時,分別從系統層面上和應用層面上作出測試。nginx
鏈接超時主要用於客戶端,由於主動發起鏈接的一方被稱爲客戶端,當調用connect系統調用的時候,客戶端發起了三次握手操做,只有完成三次握手以後鏈接纔算是真正的創建。鏈接超時是指對於在發起鏈接開始,若是在指定時間內沒有成功創建鏈接,須要執行將控制權從connect狀態返回,通知調用者,避免沒必要要的等待。發送超時主要針對write操做,對於網絡稍微瞭解的同窗都知道,其實對於socket的write操做只是簡單的將應用層數據拷貝到內核中這個TCP鏈接的發送緩衝區中,發送操做是由TCP/IP內核協議棧完成的,而發送方發送多少數據是根據接收方的接收窗口控制的。因此當鏈接的內核緩衝區被填滿(發送緩衝區就至關於一個隊列,只有在發送方將數據發送出去而且接收方迴應ack以後發送出去的數據纔會被丟棄,因此當本地的write操做較多而且因爲網絡緣由致使的對端接受緩慢就很容易填滿發送緩衝區),而且對端的接收窗口很小甚至爲0就會致使write操做阻塞,發送超時主要針對write操做,若是在指定時間write操做不能返回就通知調用方,避免沒必要要的等待。讀超時是針對read操做的,socket的read操做會等待着該鏈接的接收緩衝區有數據到達,TCP層保證了數據的順序性和正確性,read會將內核緩衝區的數據拷貝到用戶緩衝區中,可是當接收緩衝區爲空(可能由於對端一直未發送數據)這個調用將會一直阻塞,從而影響接下來的流程,讀超時就是指在指定時間內read操做不能讀取(read一次只要讀取到數據就會返回)到任何數據須要通知調用方,避免不須要的等待。空閒超時主要是針對服務器的,因爲鏈接時客戶端創建的,因此通常鏈接關閉操做也是由客戶端控制的,可是每個鏈接都須要佔用服務器的資源,爲了節約資源,服務器須要有必定的機制保證關閉空閒鏈接,通常狀況下是指在客戶端一個請求結束後在指定時間內沒有其餘請求,服務器須要執行一些必要的操做,例如關閉鏈接,從而保護本身。後端
從這幾種超時的討論能夠看出,前三種超時都是系統層面上的,與系統調用息息相關,而空閒超時則徹底是一種應用層的操做,鏈接超時通常用於客戶端發起鏈接時設置,讀寫超時能夠用於客戶端和服務器在對數據進行讀寫時設置,空閒超時通常用於服務器對鏈接進行設置。能夠看出不管是客戶端仍是服務器都只想着本身的利益,不信任對方,若是不設置超時,那麼會致使客戶端或者服務器的資源浪費(至少浪費一個鏈接,若是使用connection per thread模型,會致使該線程接下來的任務都沒法進行),尤爲是空閒鏈接,若是服務器不會主動關閉鏈接,那麼惡意用戶就能夠經過大量創建鏈接而不發送數據,直到將服務器的資源消耗完。所以超時的設置是很是必要的。服務器
可是對於應用程序怎麼設置超時呢?也就是在實際編碼的時候如何作呢?首先,你須要一個定時器管理單元,它可以提供定時的功能,在每個註冊的定時器超時以後通知調用方。對於前三種系統層面的超時,這些操做默認都是阻塞方式進行的,也就是說線程將會處於睡眠狀態,直到這三種系統調用返回,這時候須要對這些系統調用進行一層封裝,例如connect操做:網絡
int Connect(int fd, struc sockaddr* addr, int len) { int state = 0; //建立定時器,回調函數用於喚醒該線程,而且將stata變量置爲1表示超時 int id = register_timer(thread_id , callback , &state); int ret = connect(fd , addr , len); //定時器在函數返回以後被刪除,回調函數只負責喚醒該線程 delete_timer(id); if(state != 0) { ret = TIMEOUT; } return ret; }
在註冊定時器的時候指定回調函數,超時以後回調函數負責喚醒睡眠的線程(能夠經過發送信號的方式,呼呼,線程間發送信號...),並將用戶傳入的私有參數值爲1表示connect操做是因爲超時返回的,對於阻塞式的read和write操做也須要這樣的封裝可是這樣須要頻繁的執行定時器的建立和刪除,爲了不這些開銷,能夠爲每個線程綁定一個定時器,建立和刪除操做轉換成定時器的激活和暫停狀態。負載均衡
這些系統調用還可使用非阻塞方式調用,這時候就須要將它們的fd註冊到一種多路複用機制上監聽,使用這種方式會致使這幾個系統調用當即返回(若是未就緒就返回EAGAIN錯誤或者EINPROGRESS),而多路複用機制會在這些fd的事件就緒以後通知調用方,可是若是在指定時間內仍未就緒,就須要通知調用方超時,具體的實現方式也能夠在調用以前註冊定時器,在定時器的回調函數中將該事件從多路複用中delete,這樣該事件就不會再被監聽了。一樣,這種方式也須要大量的定時器的建立和銷燬操做,須要高效的定時器支撐。curl
一樣,對於空閒超時,也能夠在每次處理完成一個請求以後就設置一個定時器,在定時器超時以後就關閉這個鏈接,執行一些清理工做。socket
好了,上面說到了設置超時的緣由,重要性,實現方式,咱們不能光說不練,下面分別對這些系統調用在系統層面上的超時和應用軟件(這裏使用nginx)設置的超時,測試環境以下:tcp
測試一:系統調用的超時ide
1. connect函數的超時函數
測試方式:使用C語言直接調用connect系統調用,server執行socket/bind/listen操做以後,阻塞在accept調用上,客戶端執行socket操做以後調用connect函數,在啓動client以前在server上使用iptables建立規則,丟棄server端口上接收到的全部數據,這包括丟棄全部的SYN報文。而後啓動服務器和客戶端。
經過觀察能夠看出客戶端一直沒有從connect函數返回,使用tcpdump抓包能夠看出客戶端在一直重試:
能夠看出在客戶端發送SYN報文以後若是在指定時間內沒有收到對端的SYN+ACK,它就會重試(這是內核協議棧作的),重試的時間依次是1s/2s/4s/8s/16s/32s,最終在75秒以後connect返回:
能夠看出,內核對於connect是有超時的,因此在應用層作超時須要低於內核對connect設置的超時時間(默認爲75s),不然應用層的超時就沒有任何意義了。
2. write函數的超時
測試方式:使用以前的程序,不過不須要在啓動cllient以前啓動server端的iptables,而是在connect以後sleep時間,而後循環調用write操做,每次寫出16KB的數據,每次返回打印出已經寫出去的字節數。在sleep時間內再啓動server機器上的iptables,一樣是丟棄全部的報文,這樣write操做開始還可以正常返回,可是當本地的發送緩衝區被填充滿以後該操做將被阻塞。client程序的反應以下:
前面的被省略了,能夠看出write操做一共寫出去了624KB的數據,因爲接收方將全部的報文都丟棄了,所也不會發送ACK,這樣發送方就看成是數據包丟失了,因而不斷的重試,經過tcpdump轉包能夠證明這一點:
能夠看出,第一次PSH發送方發送了16384byte的數據到對端,接着又發送了11815byte的數據,這兩份數據都沒有獲得對方的ack,因而內核協議棧不斷的進行重試,重試只針對第一份報文,能夠看出間隔時間依次是0.2s/0.4s/0.8s/1.6s,成倍增加直到102.4秒以後,每隔2分鐘重試一次。最終client進程的write操做仍是返回的,write函數返回錯誤:Connection timed out,這個時間距離第一次發送數據的時間大概過去了15分鐘30秒,能夠看出該操做一共嘗試了15次(從0.2s開始成倍增加直到102.4s,而後每隔2s重試一次,嘗試了5次,在最後一次結束以後仍沒有發送出去就返回錯誤)。因此係統層面上的寫超時時間是這樣的,應用程序不要設置大於這樣的超時時間,不然就沒有意義了。
可是經過netstat查看發送緩衝區發現一個不一致的狀況。
在這裏看到客戶端的這個鏈接的發送緩衝區的大小爲649889byte,比程序打印出來的write操做返回的總字節數少了1萬多byte,不知道爲何,只能猜想對於套接字的write操做會將全部的數據所有拷貝以後纔會返回?也就是可以保證write操做的原子性?只能猜想了...
3. read函數的超時、
因爲默認狀況下read操做是阻塞式的,它會阻塞直到套接字的接收緩衝區有任何數據到達,操做系統的協議棧不會對接收緩衝區爲空作任何重試操做,因此read操做將會一直阻塞,除非在應用層有外力的做用,這裏再也不進行測試。
測試二:nginx做爲代理服務器的各類超時
nginx有強大的代理轉發功能,因此常用它做爲反向代理服務器,而且能夠經過它提供的負載均衡能力對後端的業務服務器進行負載均衡,做爲代理服務器須要和後端的多個服務器創建多個鏈接,通常客戶端會使用鏈接池管理鏈接,通常有兩種實現方式:1. 每個新的請求都建立一個鏈接並加入到池子中,等待池子滿了從中選取一個發送請求;2. 儘量的少建立鏈接,直到一個鏈接hold不住的時候再建立新的鏈接。不論哪一種方式都須要多個請求複用一個tcp鏈接,這就須要對每個請求進行標識,從而可以在獲得回覆以後繼續該請求的流程。下面看一下nginx做爲反向代理的超時機制,在nginx做爲代理服務器時,提供了三種超時參數,分別是鏈接超時,讀超時和發送超時。參數配置以下:
把這三個超時分別設置爲30s/40s和50s,在後臺服務器中有idle鏈接的超時時間,能夠設置keepalive_timeout來設置服務器的空閒超時,這裏設置爲60s。
1. 鏈接超時
測試方法:在終端使用curl向代理服務器發送一個GET請求,代理服務器收到這個GET請求以後經過匹配location會將該請求轉發給後臺的nginx服務器,這兩個服務器在一臺主機上,分別配置兩個nginx虛擬主機,端口號分別起6162和8181,前者是代理服務器,後者是後臺服務器。在啓動這兩個服務器以後先使用iptables設置規則以丟棄全部發往8181端口的數據包,而後再執行curl命令,最終curl的結果以下:
代理服務器發生了超時,經過tcpdump抓包能夠看出代理服務器一直在重試向後臺服務器(8181)發送SYN報文:
一共嘗試了5次,可是從nginx代理服務器的日誌中能夠看到這個請求的結束時間:
能夠看出,從代理服務器第一次向8181服務器發送請求,到這個請求被回覆正好通過了30秒,connect重試了5次,這也說明代理服務器中配置的鏈接超時起了做用。
2. 發送超時
測試方式:發送數據以前須要首先創建鏈接,可是iptables只能丟棄一個端口上的全部報文,而不能選擇性的丟棄指定類型的報文(至少我沒有找到),因此就只好使用了代理服務器的鏈接池,這樣每次發送真正的數據以前首先發送一個成功的GET請求,使得代理服務器和後臺服務器創建了鏈接,而後再發送一個真正的測試請求,爲了填滿代理服務器的發送緩衝區,我使用PUT請求上傳一個大小爲4MB的文件,這樣複用以前的鏈接,數據並不能被真正發送到8181服務器上,最終curl仍然返回504 Gateway Time-out(使用curl命令,上傳一個4MB大小的文件,使用PUT命令,可是第一次操做失敗了,返回的錯誤是413 Request Entity Too Large,這是由於未設置代理服務器的client_max_body_size參數,以致於請求的主體部分過大,將這個參數設置爲10MB,再次重試就能夠了)。
tcpdump的抓包狀況以下:
因爲複用以前的鏈接,因此發送的報文序列號再也不是從1開始的了,發送操做也會根據write調用的策略按期的重試,直到最後請求被返回,可是請求被返回的時間和最終8181關閉鏈接的時間不同的,至於8181關閉鏈接的時間,是因爲空閒超時時間決定的。
從日誌中查看返回curl請求的時間和第一次發送數據的時間正好相差了50s,這說明以前配置的nginx發送超時生效了。
3. 讀超時:
測試方式:測試讀超時時須要丟棄全部發往代理服務器的包,可是這個端口號是在connect創建鏈接時隨機選擇的,因此須要首先複用以前創建的鏈接,而後獲取該鏈接的客戶端端口號,而後使用iptables根據丟棄全部發往這個端口的報文,在終端使用curl命令向代理服務器請求,最終獲得的結果仍然是504 Gateway Time-out。tcpdump抓包以下:
發現兩個方向都有數據流通,由於在curl以前就禁止了全部發往60762端口的報文,它會有兩個影響:1.全部8181端口向它發送的PSH報文被丟棄;2. 全部8181端口向它發送的ACK保溫被丟棄。所以8181將不斷的重試向它的PSH操做,而60762雖然向8181端口發送數據成功(沒有被丟棄)了,可是對端回覆的ACK被丟棄了,這也會致使它不斷的重試,因此看到雙方都在重試的狀況。可是最終這個請求被返回了,返回的時間能夠根據代理服務器的日誌得到:
能夠計算出,最終恢復的時間距離該請求的讀操做的時間爲40秒,正好說明讀超時有效。
4. 空閒超時
測試方式:在8181上配置keepalive_timeout爲60s,而後清除全部的iptables的規則,使用curl發起一個GET請求,等待一段時間,觀察tcpdump的輸出:
能夠看出在GET請求成功完成1分鐘以後,這個鏈接被8181服務器主動關閉了,這也就說明了nginx的keepalive_timeout超時設置是有效的,當在這個時間內一個鏈接上無請求到達就關閉該鏈接。
上面,咱們對四中超時進行了討論,而且從系統調用層面上和應用層面上分別對它們進行測試和驗證,從這裏能夠看出在設計網絡程序中不管是客戶端仍是服務器,尤爲是代理服務器做爲客戶端時須要對每個鏈接甚至請求設置超時,這樣保證資源部被浪費,請求可以最後獲得回覆。可是這樣的超時仍是有一些不夠完善,例如一個鏈接上每隔1s中發送一個請求,這樣不會形成寫操做的超時,也不會形成服務器的空閒超時,可是對於這樣緩慢的鏈接時應該被提早關閉的,因此能夠進一步使用curl庫中的超時設置:在指定時間內該鏈接的讀寫速度小於一個閥值以後該鏈接就會被關閉,這樣能夠更好的保護服務器。
文中不足之處和疑問但願可以被指出和解答,但願你能從中獲得收穫...