服務器開發中網絡故障排查

原文鏈接:https://me.csdn.net/analogous_love

一、操作系統提供的接口

1.常見的網絡接口:socket,connect,send,recv,accept,shutdown

  • 爲了能更好的排查網絡通信問題,我們需要熟悉操作系統提供的以下網絡接口函數,列表如下:
接口函數名稱 接口函數描述 接口函數
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等函數與此類似。

下面討論一下以上函數的一些使用注意事項:

2.網絡接口返回值的說明

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

3.阻塞模式的套接字和非阻塞模式的套接字

  • 默認使用的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函數情形一樣。

4.send和recv函數完成的是一次數據拷貝操作

  • send函數雖然名稱叫“send”,但是其並不是將數據發送到網絡上去,只是將數據從應用層緩衝區中拷貝到協議棧內核緩衝區中,具體什麼時候發送到網絡上去,與協議棧本身行爲有關係 (socket選項nagle算法與這個有關係,下文介紹常見套接字選項時會介紹);
    這點需要特別注意,所以即使send函數返回一個大於0的值n,也不能表明已經有n個字節發送到網絡上去了。
  • 同樣的道理,recv函數也不是從網絡上收取數據,只是從協議棧內核緩衝區拷貝數據至應用層緩衝區,並不是真正地從網絡上收數據,所以,調用recv時,操作系統的協議棧已經將數據從網絡上收到自己的內核緩衝區中了,recv僅僅是一次數據拷貝操作而已。

5.優雅關閉:shutdown+close

(1)由於套接字實現是收發全雙工的,收和發通道相互獨立,不會相互影響,shutdown函數是用來選擇關閉socket收發通道中某一路(當然,也可以兩路都關閉)
其how參數取值一般有三個:SHUT_RD/SHUT_WR/SHUT_RDWR:

  • SHUT_RD表示關閉收消息鏈路,即該套接字不能再收取數據;
  • SHUT_WR表示關閉套接字發消息鏈路;
  • SHUT_RDWR同時關閉收消息鏈路和發消息鏈路。

(2)但是這裏有個問題,有時候我們需要等待緩衝區中數據發送完後再關閉連接怎麼辦?
這裏就要用到套接字選項LINGER,關於這個選項請參考下文常見的套接字選項介紹。

(3)通過上面的分析,我們得出結論,shutdown函數並不會要求操作系統底層回收套接字等資源,真正會回收資源是close函數,這個函數會要求操作系統回收相關套接字資源,並釋放對ip地址與端口號二元組的佔用,但是由於tcp四次揮手最後一個階段有個TIME_WAIT狀態(關於這個狀態下文介紹tcp三次握手和四次回收時會詳細介紹),導致與該socket相關的端口號資源不會被立即釋放,有時候爲了達到釋放端口用來複用,我們會設置套接字選項SOL_REUSEPORT(關於這個選項,下文會介紹)。

綜合起來,我們關閉一個套接字,一般會先調用shutdown函數再調用close函數,這就是所謂的優雅關閉:
在這裏插入圖片描述

6.常見的套接字選項

嚴格意義上說套接字選項是有不同層級的(level),如socket級別、TCP級別、IP級別,這裏我們不區分具體的級別。
(1)SO_SNDTIMEO與SO_RCVTIMEO

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

(2)TCP_NODELAY

  • 操作系統底層協議棧默認有這樣一個機制,爲了減少網絡通信次數,會將send等函數提交給tcp協議棧的多個小的數據包合併成一個大的數據包,最後再一次性發出去,也就是說,如果你調用send函數往內核協議棧緩衝區拷貝了一個數據,這個數據也許不會馬上發到網絡上去,而是要等到協議棧緩衝區積累到一定量的數據

(3)SO_LINGER

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

(4)SO_REUSEADDR/SO_REUSEPORT

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

(5)SO_KEEPALIVE

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

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

1.ping和telnet

(1)

  • ping命令可用於測試網絡是否連通
  • 命令使用格式:
    telnet ip或域名 port
    例如:
    telnet 120.55.94.78 8888
    telnet www.baidu.com 80
    結合ping和telnet命令我們就可以判斷一個服務器地址上的某個端口號是否可以對外提供服務。

(2)windows打開telnet
由於我們使用的開發機器以windows居多,默認情況下,windows系統的telnet命令是沒有打開的,我們可以在【控制面板】- 【程序】- 【程序和功能】- 【打開或關閉Windows功能】中打開telnet功能。
在這裏插入圖片描述

2.host命令:解析域名得到對應的ip地址

  • host 命令可以解析域名得到對應的ip地址。例如,我們要得到www.baidu.com這個域名的ip地址,可以輸入:
    在這裏插入圖片描述
  • 得到www.google.com的ip地址可以輸入:
    在這裏插入圖片描述

3.netstat命令:對各個協議進行統計

常見的選項有:

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

4.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狀態。

5.pstack:查看某個進程的線程數量和線程調用堆棧

嚴格意義上來說,這個不算網絡排查故障和調試命令,但是我們可以利用這個命令來查看某個進程的線程數量和線程調用堆棧是否運行正常。

  • 指令使用格式:
    pstack pid,即:pstack 進程號,如:
    在這裏插入圖片描述

6.nc命令:指定客戶端或服務器端ip和端口

即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命令發消息和發文件
    客戶端
    在這裏插入圖片描述
    服務器端
    在這裏插入圖片描述

7.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三次握手和四次揮手過程的每一個細節是我們排查網絡問題的基礎。
在這裏插入圖片描述

1.首先看正常的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選項後就變成絕對序列號了。
    在這裏插入圖片描述

2.不正常的TCP連接狀態1

假如我們連接的服務器ip地址存在,但監聽端口號並不存在
在這裏插入圖片描述
這個時候客戶端發送SYN,服務器應答ACK+RST:
在這裏插入圖片描述
這個應答包會導致客戶端的connect連接失敗。

3.不正常的TCP連接狀態2

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

  • 我們先將防火牆的已有規則都清理掉: iptables -F

  • 然後給防火牆的INPUT鏈上增加一個規則,丟棄本地網卡lo(也就是127.0.0.1這個迴環地址)上的所有SYN包。
    在這裏插入圖片描述
    在這裏插入圖片描述

  • 接着,我們看到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]}’

  • 得到結果可能類似:
    在這裏插入圖片描述

4.三種重要的TCP狀態

在這裏插入圖片描述

(1)SYN_RECV

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

  • net.ipv4.tcp_synack_retries,整形,默認值是5
    對於遠端的連接請求SYN,內核會發送SYN + ACK數據報,以確認收到上一個 SYN連接請求包。這是三次握手機制的第二個步驟。這裏決定內核在放棄連接之前所送出的 SYN+ACK 數目。不應該大於255,默認值是5,對應於180秒左右時間。
    通常我們不對這個值進行修改,因爲我們希望TCP連接不要因爲偶爾的丟包而無法建立。

  • 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,對於一個高併發連接的通訊服務器,這個數字比較正常。

(2)CLOSE_WAIT

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

(3)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,表示關閉。

5.我們這裏總結一下這些與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
    關於這兩個選項會影響在NAT網絡中的,局域網服務器組之間通信,而在非NAT網絡中不影響服務端與客戶端的通信,所以在NAT網絡中不建議開啓。
    至於原因,可參見:http://www.cnxct.com/coping-with-the-tcp-time_wait-state-on-busy-linux-servers-in-chinese-and-dont-enable-tcp_tw_recycle/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章