伺服器開發中網路資料分析與故障排查經驗漫談

范蠡同學發表於2019-02-18
伺服器開發中網路資料分析與故障排查經驗漫談

寫在前面的話

“聽見學生時代愛聽的歌,加上太累,回家路上一下子想了好多,腳步慢了,眼眶溼了,不是感傷,而是生活呀,需要這麼多力量。過去那些跌跌撞撞忙碌的日子,怎麼說呢,多少有點像在逃避吧,聽起來不像是真的。”
以上這段話訴說了我的經歷,我也曾迷惘和無助過。也有很多朋友找到我,希望我做一些經驗分享和職業規劃指導。為此我特地開辦了一個微信公眾號『easyserverdev』。如果有任何技術或者職業方面的問題需要我提供幫助,可通過這個公眾號與我取得聯絡,此公眾號不僅分享高效能伺服器開發經驗和故事,同時也免費為廣大技術朋友提供技術答疑和職業解惑助,你有任何問題都可以在微信公眾號直接留言,我會盡快回復您。

正文

一、 作業系統提供的網路介面

為了能更好的排查網路通訊問題,我們需要熟悉作業系統提供的以下網路介面函式,列表如下:

介面函式名稱 介面函式描述 介面函式簽名
socket 建立套接字 int socket(int domain, int type, int protocol);
connect 連線一個伺服器地址 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
send 傳送資料 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv 收取資料 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
accept 接收連線 int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
shutdown 關閉收發鏈路 int shutdown(int sockfd, int how);
close 關閉套接字 int close(int fd);
setsockopt 設定套接字選項 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

注意:這裡以bekeley提供的標準為例,不包括特定作業系統上特有的介面函式(如Windows平臺的WSASend,linux的accept4),也不包括實際與網路資料來往不相關的函式(如select、linux的epoll),這裡只討論與tcp相關的介面函式,像與udp相關的函式sendto/recvfrom等函式與此類似。
下面討論一下以上函式的一些使用注意事項

  1. 以上函式如果呼叫出錯後,返回值均為-1;但是返回值是-1,不一定代表出錯,這還得根據對應的套接字模式(阻塞與非阻塞模式)。

  2. 預設使用的socket函式建立的套接字是阻塞模式的,可以呼叫相關介面函式將其設定為非阻塞模式(Windows平臺可以使用ioctlsocket函式,linux平臺可以使用fcntl函式,具體設定方法可以參考這裡。)。阻塞模式和非阻塞模式的套接字,對伺服器的連線伺服器和網路資料的收發行為影響很大。詳情如下:

    • 阻塞模式下,connect函式如果不能立刻連上伺服器,會導致執行流阻塞在那裡一會兒,直到connect連線成功或失敗或網路超時;而非阻塞模式下,無論是否連線成功connect將立即返回,此時如果未連線成功,返回值將是-1,錯誤碼是EINPROGRESS,表示連線操作仍然在進行中。Linux平臺後續可以通過使用select/poll等函式檢測該socket是否可寫來判斷連線是否成功。
    • 阻塞套接字模式下,send函式如果由於對端tcp視窗太小,不足以將全部資料傳送出去,將阻塞執行流,直到出錯或超時或者全部傳送出去為止;同理recv函式如果當前協議棧系統緩衝區中無資料可讀,也會阻塞執行流,直到出錯或者超時或者讀取到資料。send和recv函式的超時時間可以參考下文關於常用socket選項的介紹。
    • 非阻塞套接字模式下,如果由於對端tcp視窗太小,不足以將資料發出去,它將立刻返回,不會阻塞執行流,此時返回值為-1,錯誤碼是EAGAIN或EWOULDBLOCK,表示當前資料發不出去,希望你下次再試。但是返回值如果是-1,也可能是真正的出錯了,也可能得到錯誤碼EINTR,表示被linux訊號中斷了,這點需要注意一下。recv函式與send函式情形一樣。
  3. send函式雖然名稱叫“send”,但是其並不是將資料傳送到網路上去,只是將資料從應用層緩衝區中拷貝到協議棧核心緩衝區中,具體什麼時候傳送到網路上去,與協議棧本身行為有關係(socket選項nagle演算法與這個有關係,下文介紹常見套接字選項時會介紹),這點需要特別注意,所以即使send函式返回一個大於0的值n,也不能表明已經有n個位元組傳送到網路上去了。同樣的道理,recv函式也不是從網路上收取資料,只是從協議棧核心緩衝區拷貝資料至應用層緩衝區,並不是真正地從網路上收資料,所以,呼叫recv時,作業系統的協議棧已經將資料從網路上收到自己的核心緩衝區中了,recv僅僅是一次資料拷貝操作而已。

  4. 由於套接字實現是收發全雙工的,收和發通道相互獨立,不會相互影響,shutdown函式是用來選擇關閉socket收發通道中某一路(當然,也可以兩路都關閉),其how引數取值一般有三個:SHUT_RD/SHUT_WR/SHUT_RDWR,SHUT_RD表示關閉收訊息鏈路,即該套接字不能再收取資料,同理SHUT_WR表示關閉套接字發訊息鏈路,但是這裡有個問題,有時候我們需要等待緩衝區中資料傳送完後再關閉連線怎麼辦?這裡就要用到套接字選項LINGER,關於這個選項請參考下文常見的套接字選項介紹。最後,SHUT_RDWR同時關閉收訊息鏈路和發訊息鏈路。通過上面的分析,我們得出結論,shutdown函式並不會要求作業系統底層回收套接字等資源,真正會回收資源是close函式,這個函式會要求作業系統回收相關套接字資源,並釋放對ip地址與埠號二元組的佔用,但是由於tcp四次揮手最後一個階段有個TIME_WAIT狀態(關於這個狀態下文介紹tcp三次握手和四次回收時會詳細介紹),導致與該socket相關的埠號資源不會被立即釋放,有時候為了達到釋放埠用來複用,我們會設定套接字選項SOL_REUSEPORT(關於這個選項,下文會介紹)。綜合起來,我們關閉一個套接字,一般會先呼叫shutdown函式再呼叫close函式,這就是所謂的優雅關閉:

伺服器開發中網路資料分析與故障排查經驗漫談
  1. 常見的套接字選項
    嚴格意義上說套接字選項是有不同層級的(level),如socket級別、TCP級別、IP級別,這裡我們不區分具體的級別。
  • SO_SNDTIMEO與SO_RCVTIMEO

    這兩個選項用於設定阻塞模式下套接字,SO_SNDTIMEO用於在send資料由於對端tcp視窗太小,發不出去而最大的阻塞時長;SO_RCVTIMEO用於recv函式因接受緩衝區無資料而阻塞的最大阻塞時長。如果你需要獲取它們的預設值,請使用getsockopt函式。

  • TCP_NODELAY

    作業系統底層協議棧預設有這樣一個機制,為了減少網路通訊次數,會將send等函式提交給tcp協議棧的多個小的資料包合併成一個大的資料包,最後再一次性發出去,也就是說,如果你呼叫send函式往核心協議棧緩衝區拷貝了一個資料,這個資料也許不會馬上發到網路上去,而是要等到協議棧緩衝區積累到一定量的資料後才會一次性發出去,我們把這種機制叫做nagle演算法。預設開啟了這個機制,有時候我們希望關閉這種機制,讓send的資料能夠立刻發出去,我們可以選擇關閉這個演算法,這就可以通過設定套接字選項TCP_NODELAY,即關閉nagle演算法。

  • SO_LINGER

    linger這個單詞本身的意思,是“暫停、逗留”。這個選項的用處是用於解決,當需要關閉套接字時,協議棧傳送緩衝區中尚有未傳送出去的資料,等待這些資料發完的最長等待時間。

  • SO_REUSEADDR/SO_REUSEPORT

    一個埠,尤其是作為伺服器端埠在四次揮手的最後一步,有一個為TIME_WAIT的狀態,這個狀態一般持續2MSL(MSL,maximum segment life, 最大生存週期,RFC上建議是2分鐘)。這個狀態存在原因如下:1. 保證發出去的ack能被送達(超時會重發ack)2. 讓遲來的報文有足夠的時間被丟棄,反過來說,如果不存在這個狀態,那麼可以立刻複用這個地址和埠號,那麼可能會收到老的連線遲來的資料,這顯然是不好的。為了立即回收複用埠號,我們可以通過開啟套接字SO_REUSEADDR/SO_REUSEPORT。

  • SO_KEEPALIVE

    預設情況下,當一個連線長時間沒有資料來往,會被系統防火牆之類的服務關閉。為了避免這種現象,尤其是一些需要長連線的應用場景下,我們需要使用心跳包機制,即定時從兩端定時發一點資料,這種行為叫做“保活”。而tcp協議棧本身也提供了這種機制,那就是設定套接字SO_KEEPALIVE選項,開啟這個選項後,tcp協議棧會定時傳送心跳包探針,但是這個預設時間比較長(2個小時),我們可以繼續通過相關選項改變這個預設值。

二、常用的網路故障排查工具

1. ping

ping命令可用於測試網路是否連通。

2. telnet

命令使用格式:

telnet  ip或域名  port
複製程式碼

例如:

telnet 120.55.94.78 8888
telnet www.baidu.com 80
複製程式碼

結合ping和telnet命令我們就可以判斷一個伺服器地址上的某個埠號是否可以對外提供服務。

由於我們使用的開發機器以windows居多,預設情況下,windows系統的telnet命令是沒有開啟的,我們可以在【控制皮膚】- 【程式】-【程式和功能】- 【開啟或關閉Windows功能】中開啟telnet功能。

伺服器開發中網路資料分析與故障排查經驗漫談

3. host命令

host 命令可以解析域名得到對應的ip地址。例如,我們要得到www.baidu.com這個域名的ip地址,可以輸入:

伺服器開發中網路資料分析與故障排查經驗漫談

得到www.google.com的ip地址可以輸入:

伺服器開發中網路資料分析與故障排查經驗漫談

4. netstat命令

常見的選項有:

-a (all)顯示所有選項,netstat預設不顯示LISTEN相關
-t (tcp)僅顯示tcp相關選項
-u (udp)僅顯示udp相關選項
-n 拒絕顯示別名,能顯示數字的全部轉化成數字。(重要)
-l 僅列出有在 Listen (監聽) 的服務狀態
-p 顯示建立相關連結的程式名(macOS中表示協議 -p protocol)
-r 顯示路由資訊,路由表
-e 顯示擴充套件資訊,例如uid等
-s 按各個協議進行統計 (重要)
-c 每隔一個固定時間,執行該netstat命令。
複製程式碼

5. lsof命令

lsof,即list opened filedescriptor,即列出當前作業系統中開啟的所有檔案描述符,socket也是一種file descriptor,常見的選項是:

-i 列出系統開啟的socket fd
-P 不要顯示埠號別名
-n 不要顯示ip地址別名(如localhost會用127.0.0.1來代替)
+c w 程式列名稱最大可以顯示到w個字元。
複製程式碼

常見的選項組合為lsof –i –Pn, 可以看到列出了當前偵聽的socket,和連線socket的tcp狀態。如下圖所示:

伺服器開發中網路資料分析與故障排查經驗漫談

6. pstack

嚴格意義上來說,這個不算網路排查故障和除錯命令,但是我們可以利用這個命令來檢視某個程式的執行緒數量和執行緒呼叫堆疊是否執行正常。指令使用格式:

pstack pid
複製程式碼

即, pstack 程式號,如下圖所示:

伺服器開發中網路資料分析與故障排查經驗漫談

7. nc命令

netcat命令,這個工具在排查網路故障時非常有用,因而被業績稱為網路界的“瑞士軍刀”。常見的用法如下:

  • 模擬伺服器端在指定ip地址和埠號上偵聽
nc –l 0.0.0.0 8888
複製程式碼
  • 模擬客戶端連線到指定ip地址和埠號
nc 0.0.0.0 8888
複製程式碼

我們知道客戶端連線伺服器一般都是作業系統隨機分配一個可用的埠號連線到伺服器上去,這個指令甚至可以指定使用哪個埠號連線,如:

nc –p 12345 127.0.0.1 8888
複製程式碼

客戶端使用埠12345去連線伺服器127.0.0.1::8888。

  • 使用nc命令發訊息和發檔案

    客戶端

伺服器開發中網路資料分析與故障排查經驗漫談

伺服器

伺服器開發中網路資料分析與故障排查經驗漫談

8. tcpdump

這個是linux系統自帶的抓包工具,功能非常強大,預設需要開啟root許可權才能使用。

伺服器開發中網路資料分析與故障排查經驗漫談

其常見的選項有:

-i 指定網路卡
-X –XX 列印十六進位制的網路資料包
-n –nn 不顯示ip地址和埠的別名
-S 以絕對值顯示包的ISN號(包序列號)
複製程式碼

常用的過濾條件有如下形式:

tcpdump –i any ‘port 8888’
tcpdump –i any ‘tcp port 8888’
tcpdump –i any ‘tcp src port 8888’
tcpdump –i any ‘tcp src port 8888 and udp dst port 9999’
tcpdump -i any `src host 127.0.0.1 and tcp src port 12345` -XX -nn -vv
複製程式碼

關於tcpdump命令接下來將會以對tcp三次握手和四次揮手的包資料進行抓包來分析。

三、tcp三次握手和四次揮手過程解析

熟練地掌握tcp三次握手和四次揮手過程的每一個細節是我們排查網路問題的基礎。

伺服器開發中網路資料分析與故障排查經驗漫談

下面我們來通過tcpdump抓包能實戰一下三次握手的過程,假設我的伺服器端的地址是 127.0.0.0.1:12345,使用nc命令建立一個伺服器程式並在這個地址上進行偵聽:

nc –v -l 127.0.0.0 112345
複製程式碼
伺服器開發中網路資料分析與故障排查經驗漫談

然後在客戶端機器上開啟tcpdump工具:

伺服器開發中網路資料分析與故障排查經驗漫談

然後在客戶端使用nc命令建立一個客戶端去連線伺服器:

伺服器開發中網路資料分析與故障排查經驗漫談

我們抓到的包如下:

伺服器開發中網路資料分析與故障排查經驗漫談

圖片看不清,可以放大來看。上面我們需要注意的是:
三次握手過程是客戶端先給伺服器傳送一個SYN,然後伺服器應答一個SYN+ACK,應答的序列號是遞增1的,表示應答哪個請求,即從4004096087遞增到4004096088,接著客戶端再應答一個ACK。這個時候,我們發現發包序列號和應答序列號都變成1了,這是tcpdump使用相對序號,我們加上-S選項後就變成絕對序列號了。

伺服器開發中網路資料分析與故障排查經驗漫談

這是正常的tcp三次握手,假如我們連線的伺服器ip地址存在,但監聽埠號並不存在,我們看下tcpdump抓包結果:

伺服器開發中網路資料分析與故障排查經驗漫談

這個時候客戶端傳送SYN,伺服器應答ACK+RST:

伺服器開發中網路資料分析與故障排查經驗漫談

這個應答包會導致客戶端的connect連線失敗。

還有一種情況就是客戶端訪問一個很遙遠的ip,或者網路繁忙,伺服器對客戶端傳送的網路SYN報文沒有應答,會出現什麼情況呢?

伺服器開發中網路資料分析與故障排查經驗漫談

我們先將防火牆的已有規則都清理掉: iptables -F
然後給防火牆的INPUT鏈上增加一個規則,丟棄本地網路卡lo(也就是127.0.0.1這個迴環地址)上的所有SYN包:

iptables -I INPUT -p tcp --syn -i lo -j DROP
複製程式碼
伺服器開發中網路資料分析與故障排查經驗漫談

接著,我們看到tcpdump抓到的資料包如下:

伺服器開發中網路資料分析與故障排查經驗漫談

連線不上,一共重試了5次,重試的時間間隔是1秒,2秒,4秒,8秒,16秒,最後返回失敗。這個重試次數在/proc/sys/net/ipv4/tcp_syn_retries 核心引數中設定,預設為6。

四次揮手與三次握手基本上類似,這裡就不貼出tcpdump抓包的詳情了。實際的網路開發中,尤其是高QPS的伺服器程式,可能在在伺服器程式所在的系統上留下大量非ESTABLISHED的中間狀態,如CLOSE_WAIT/TIME_WAIT,我們可以使用以下指令來統計這些狀態資訊:

netstat -n | awk `/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}` 
複製程式碼

得到結果可能類似:

伺服器開發中網路資料分析與故障排查經驗漫談

讓我們再貼一張tcp三次握手和四次揮手更清晰的圖吧。

伺服器開發中網路資料分析與故障排查經驗漫談

下面看下一般比較關心的三種TCP狀態:

  • SYN_RECV

服務端收到建立連線的SYN沒有收到ACK包的時候處在SYN_RECV狀態。有兩個相關係統配置:

  1. net.ipv4.tcp_synack_retries,整形,預設值是5

    對於遠端的連線請求SYN,核心會傳送SYN + ACK資料包,以確認收到上一個 SYN連線請求包。這是三次握手機制的第二個步驟。這裡決定核心在放棄連線之前所送出的 SYN+ACK 數目。不應該大於255,預設值是5,對應於180秒左右時間。通常我們不對這個值進行修改,因為我們希望TCP連線不要因為偶爾的丟包而無法建立。

  2. net.ipv4.tcp_syncookies

    一般伺服器都會設定net.ipv4.tcp_syncookies=1來防止SYN Flood攻擊。假設一個使用者向伺服器傳送了SYN報文後突然當機或掉線,那麼伺服器在發出SYN+ACK應答報文後是無法收到客戶端的ACK報文的(第三次握手無法完成),這種情況下伺服器端一般會重試(再次傳送SYN+ACK給客戶端)並等待一段時間後丟棄這個未完成的連線,這段時間的長度我們稱為SYN Timeout,一般來說這個時間是分鐘的數量級(大約為30秒-2分鐘)。這些處在SYNC_RECV的TCP連線稱為半連線,並儲存在核心的半連線佇列中,在核心收到對端傳送的ack包時會查詢半連線佇列,並將符合的requst_sock資訊儲存到完成三次握手的連線的佇列中,然後刪除此半連線。大量SYNC_RECV的TCP連線會導致半連線佇列溢位,這樣後續的連線建立請求會被核心直接丟棄,這就是SYN Flood攻擊。能夠有效防範SYN Flood攻擊的手段之一,就是SYN Cookie。SYN Cookie原理由D. J. Bernstain和 Eric Schenk發明。SYN Cookie是對TCP伺服器端的三次握手協議作一些修改,專門用來防範SYN Flood攻擊的一種手段。它的原理是,在TCP伺服器收到SYN包並返回SYN+ACK包時,不分配一個專門的資料區,而是根據這個SYN包計算出一個cookie值。在收到ACK包時,TCP伺服器在根據那個cookie值檢查這個TCP ACK包的合法性。如果合法,再分配專門的資料區進行處理未來的TCP連線。觀測服務上SYN_RECV連線個數為:7314,對於一個高併發連線的通訊伺服器,這個數字比較正常。

  • CLOSE_WAIT

    發起TCP連線關閉的一方稱為client,被動關閉的一方稱為server。被動關閉的server收到FIN後,但未發出ACK的TCP狀態是CLOSE_WAIT。出現這種狀況一般都是由於server端程式碼的問題,如果你的伺服器上出現大量CLOSE_WAIT,應該要考慮檢查程式碼。

  • TIME_WAIT

    根據三次握手斷開連線規定,發起socket主動關閉的一方 socket將進入TIME_WAIT狀態。TIME_WAIT狀態將持續2MSL。TIME_WAIT狀態下的socket不能被回收使用。 具體現象是對於一個處理大量短連線的伺服器,如果是由伺服器主動關閉客戶端的連線,將導致伺服器端存在大量的處於TIME_WAIT狀態的socket, 甚至比處於Established狀態下的socket多的多,嚴重影響伺服器的處理能力,甚至耗盡可用的socket,停止服務。TIME_WAIT是TCP協議用以保證被重新分配的socket不會受到之前殘留的延遲重發報文影響的機制,是必要的邏輯保證。和TIME_WAIT狀態有關的系統引數有一般由3個,本機設定如下:

    net.ipv4.tcp_tw_recycle = 1
    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tcp_fin_timeout = 30
    net.ipv4.tcp_fin_timeout,預設60s,減小fin_timeout,減少TIME_WAIT連線數量
    net.ipv4.tcp_tw_reuse = 1表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連線,預設為0,表示關閉;
    net.ipv4.tcp_tw_recycle = 1表示開啟TCP連線中TIME-WAIT sockets的快速回收,預設為0,表示關閉。
    複製程式碼

我們這裡總結一下這些與tcp狀態的選項

  • net.ipv4.tcp_syncookies=1 表示開啟SYN Cookies。當出現SYN等待佇列溢位時,啟用cookie來處理,可防範少量的SYN攻擊。預設為0,表示關閉。
  • net.ipv4.tcp_tw_reuse=1 表示開啟重用。允許將TIME-WAIT套接字重新用於新的TCP連線。預設為0,表示關閉。
  • net.ipv4.tcp_tw_recycle=1 表示開啟TCP連線中TIME-WAIT套接字的快速回收。預設為0,表示關閉。
  • net.ipv4.tcp_fin_timeout=30 表示如果套接字由本端要求關閉,這個引數決定了它保持在FIN-WAIT-2狀態的時間。
  • net.ipv4.tcp_keepalive_time=1200 表示當keepalive啟用時,TCP傳送keepalive訊息的頻度。預設是2小時,這裡改為20分鐘。
  • net.ipv4.ip_local_port_range=1024 65000 表示向外連線的埠範圍。預設值很小:32768~61000,改為1024~65000。
  • net.ipv4.tcp_max_syn_backlog=8192 表示SYN佇列的長度,預設為1024,加大佇列長度為8192,可以容納更多等待連線的網路連線數。
  • net.ipv4.tcp_max_tw_buckets=5000 表示系統同時保持TIME_WAIT套接字的最大數量,如果超過這個數 字,TIME_WAIT套接字將立刻被清除並列印警告資訊。預設為180000,改為5000。
    注意
    上文中提到的兩個引數:
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_tw_reuse
複製程式碼

在實際linux核心引數調優時並不建議開啟。原因是關於這兩個選項會影響在NAT網路中的,區域網伺服器組之間通訊,而在非NAT網路中不影響服務端與客戶端的通訊,所以在NAT網路中不建議開啟。至於原因,可參見:www.cnxct.com/coping-with…

四、 關於跨系統與跨語言之間的網路通訊連通問題

如何在Java語言中去解析C++的網路資料包,如何在C++中解析Java的網路資料包,對於很多人來說是一件很困難的事情,所以只能變著法子使用第三方的庫。其實使用tcpdump工具可以很容易解決與分析。
首先,我們需要明確位元組序列這樣一個概念,即我們說的大端編碼(big endian)和小端編碼(little endian),x86和x64系列的cpu使用小端編碼,而資料在網路上傳輸,以及Java語言中,使用的是大端編碼。那麼這是什麼意思呢?
我們舉個例子,看一個x64機器上的32位數值在記憶體中的儲存方式:

伺服器開發中網路資料分析與故障排查經驗漫談

i在記憶體中的地址序列是0x003CF7C4~ 0x003CF7C8,值為40 e2 01 00。

伺服器開發中網路資料分析與故障排查經驗漫談

十六進位制0001e240正好等於10進位制123456,也就是說小端編碼中權重高的的位元組值儲存在記憶體地址高(地址值較大)的位置,權重值低的位元組值儲存在記憶體地址低(地址值較小)的位置,也就是所謂的高高低低。
相反,大端編碼的規則應該是高低低高,也就是說權值高位元組儲存在記憶體地址低的位置,權值低的位元組儲存在記憶體地址高的位置。
所以,如果我們一個C++程式的int32值123456不作轉換地傳給Java程式,那麼Java按照大端編碼的形式讀出來的值是:十六進位制40E20100 = 十進位制1088553216。
所以,我們要麼在傳送方將資料轉換成網路位元組序(大端編碼),要麼在接收端再進行轉換。

下面看一下如果C++端傳送一個如下資料結構,Java端該如何解析(由於Java中是沒有指標的,也無法操作記憶體地址,導致很多人無從下手),下面利用 tcpdump 來解決這個問題的思路。

我們客戶端傳送的資料包:

伺服器開發中網路資料分析與故障排查經驗漫談

其結構體定義如下:

伺服器開發中網路資料分析與故障排查經驗漫談

利用tcpdump抓到的包如下:

伺服器開發中網路資料分析與故障排查經驗漫談

放大一點:

伺服器開發中網路資料分析與故障排查經驗漫談

我們白色標識出來就是我們收到的資料包。這裡我想說明兩點:

  • 如果我們知道傳送端傳送的位元組流,再比照接收端收到的位元組流,我們就能檢測資料包的完整性,或者利用這個來排查一些問題;

  • 對於Java程式只要按照這個順序,先利用java.net.Socket的輸出流java.io.DataOutputStream物件readByte、readInt32、readInt32、readBytes、readBytes方法依次讀出一個char、int32、int32、16個位元組的位元組陣列、63個位元組陣列即可,為了還原像int32這樣的整形值,我們需要做一些小端編碼向大端編碼的轉換。

五、常見的問題總結

  1. CentOS配置資訊儲存位置
    上文中涉及到的配置資訊位於/etc/sysctl.conf,修改後執行以下命令讓配置生效:
/sbin/sysctl -p
複製程式碼
  1. 涉及到的配置資訊
  • net.ipv4.tcp_syncookies=1
    表示開啟SYN Cookies。當出現SYN等待佇列溢位時,啟用cookie來處理,可防範少量的SYN攻擊。預設為0,表示關閉。
  • net.ipv4.tcp_tw_reuse=1
    表示開啟重用。允許將TIME-WAIT套接字重新用於新的TCP連線。預設為0,表示關閉。
  • net.ipv4.tcp_tw_recycle=1
    表示開啟TCP連線中TIME-WAIT套接字的快速回收。預設為0,表示關閉。
  • net.ipv4.tcp_fin_timeout=30
    表示如果套接字由本端要求關閉,這個引數決定了它保持在FIN-WAIT-2狀態的時間。
  • net.ipv4.tcp_keepalive_time=1200
    表示當keepalive啟用時,TCP傳送keepalive訊息的頻度。預設是2小時。
  • net.ipv4.ip_local_port_range=1024
    65000 表示向外連線的埠範圍。預設值很小:32768~61000。
  • net.ipv4.tcp_max_syn_backlog=8192
    表示SYN佇列的長度,預設為1024。
  • net.ipv4.tcp_max_tw_buckets=5000
    表示系統同時保持TIME_WAIT套接字的最大數量,如果超過這個數字,TIME_WAIT套接字將立刻被清除並列印警告資訊。預設為180000。
    以上引數當前值可以通過檢視/proc/sys/net/ipv4/xx,xx為引數名稱,如檢視/proc/sys/net/ipv4/tcp_fin_timeout:
伺服器開發中網路資料分析與故障排查經驗漫談
  1. 當客戶端C連線伺服器S成功後,如果伺服器先關閉,客戶端C不關閉,伺服器S將處於FIN_WAIT_2狀態,客戶端C處於CLOSE_WAIT狀態,伺服器的FIN_WAIT_2狀態將在net.ipv4.tcp_fin_timeout後被回收,預設30秒,在這個期間不會被複用;客戶端C處於CLOSE_WAIT狀態將一直持續到程式結束或者作業系統重啟,否則作業系統不會回收CLOSE_WAIT狀態的連線,因為這個錯誤是可以避免的,其根本原因就是客戶端沒關閉連線導致,應該去檢查你的程式碼。
    同樣的道理,如果是客戶端C先關閉,伺服器S未關閉,則客戶端C處於FIN_WAIT_2狀態,伺服器器端處於CLOSE_WAIT狀態,與上面的情況類似。但是,我這裡需要強調一點是:如果兩個處於相互連線狀態的端較遠,當中間的鏈路出現故障(如路由器斷電),且該鏈路是兩端的必經之路,那麼除非傳送資料監測,否則兩端的tcp協議棧本身是監測不到這個連線斷開的問題,這個時候,我們需要使用類似於“保活”機制的心跳包來監測,並及時發現這種“死鏈”,關閉套接字或者重連。

  2. 每一路連線以(源地址,源埠號,目標地址,目標埠號)這樣一個四元組唯一確定,假設目標地址和目標埠號確定的情況下,由源地址+源埠號確定,源地址一般可以認為是固定的,所以現在連線數量由可用埠號數量來確定,這個引數由net.ipv4.ip_local_port_range確定,預設值32768~61000,大約28000個左右。

  3. 當發生網路故障時,我們需要除了需要關注機器的記憶體、磁碟、執行緒棧等狀態外,還需要關注一下,服務上的連線狀態,確認是否存在不正常的tcp三次握手或者四次揮手的中間狀態(如CLOSE_WAIT和TIME_WAIT)狀態,另外就是檢視下臨近的防火牆上來往的資料是否正常。在CentOS 7上我們可以使用iptables等命令檢視和修改相關防火牆規則。

限於作者水平和經驗有限,文中如果不當的地方,歡迎提出意見。

全文完。

張小方寫於2018年3月29日

參考資料:

相關文章