簡介
Wireshark是一個網絡抓包分析軟件,當線上出現各種連接相關的問題,如連接不復用,大量CLOSE_WAIT時,可以方便的使用Wireshark抓包軟件進行抓包分析
安裝
Wirewark在windows系統上默認使用的是WinPcap來抓包的,只能看到經過網卡的流量,看不到訪問localhost的流量,可先安裝Npcap,安裝Wirewark時再選擇不安裝WInPcap即可抓localhost的包
基本使用
window下,直接只用wireshark客戶端進行抓包
Linux下,使用tcpdump產生pcap文件,再通過wireshark導入分析
tcpdump -s0 host 192.168.162.103 and port 9999 -w my.pcap
典型場景分析
服務端代碼:
public class SocketServer {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress((InetAddress)null, 9999));
Socket socket = server.accept();
// socket.setKeepAlive(true); 默認情況下不進行心跳檢測
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
Runtime.getRuntime().addShutdownHook(new Thread(()-> {
try {
socket.close();
server.close();
} catch (IOException e) {
System.out.println("close exception" + e);
}
}));
while(true) {
byte[] bytes = new byte[1024];
int size = in.read(bytes);
System.out.println("server read " + new String(Arrays.copyOf(bytes, size)));
out.write("hello client".getBytes());
out.flush();
}
}
}
客戶端代碼:
public class SocketClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("192.168.162.105", 9999);
OutputStream out = socket.getOutputStream();
out.write("hello server".getBytes());
out.flush();
InputStream in = socket.getInputStream();
byte[] bytes = new byte[1024];
int size = in.read(bytes);
System.out.println("server read " + new String(Arrays.copyOf(bytes, size)));
// Thread.sleep(22 * 1000); 休眠一段時間使得服務端進行心跳檢測
out.close();
in.close();
socket.close();
}
}
1 觀察三次握手、四次揮手
其中四次揮手只有3個tcp分組原因:四次揮手的時候,兩個方向的斷開是獨立的,每個方向發送一個FIN,對方回復一個ACK,但同時,TCP規定ACK可以捎帶在其他數據包當中,所以你看到的主動斷開連接一方本應收到的ACK,是被對方的FIN包捎帶過來的,就變成了三個包。並不是所有的情況下都是這樣,典型的一種情況是,主動斷開的一方發送FIN之后,被動一方仍然有數據要繼續發送,就會先ACK這個FIN,然后繼續發送數據(在此過程中主動斷開一方仍然會繼續ACK這些數據),直到數據發送完畢之后再發送FIN並接收對方的ACK。
2 觀察tcp心跳檢測機制(放開注釋)
tcp心跳服務端參數說明
2.1 模擬客戶端一段時間不傳輸數據
服務器net.ipv4.tcp_keepalive_intvl = 10,以上代碼客戶端sleep了22s,因此服務端進行了2次心跳檢測
2.2 模擬MySQL Client突然掉線,抓取Server端
- 最后一次正常請求后10s,服務端開始發送心跳包
- 心跳包間隔3秒,發送3次
- 3次后,服務端關閉連接
3 線上CLOSE_WAIT問題
在被動關閉連接情況下,在已經接收到FIN,但是還沒有發送自己的FIN的時刻,連接處於CLOSE_WAIT狀態,由此分析可知通常是被動關閉方代碼問題。
線上應用程序引入連接池后,訪問clickhouse出現CLOSE_WAIT
- 可以看到客戶端一直沒有回復FIN,研究clickhouse客戶端源碼實現發現clickhouse底層基於HttpClient實現JDBC接口,由於Http服務端並不會永久保持連接,當服務端超過Keep-Alive時間后會主動關閉連接,而客戶端使用連接池后,不會釋放關閉連接,導致客戶端CLOSE_WAIT
- 因此通過增加服務端和客戶端的http層Keep-Alive時間,可以緩解這個問題,但是並不能根本解決
- 由於HttpClient本身可以支持多個連接,所以對一個Connection進行管理,即可支持連接池,后續舍棄了Druid連接池,自己進行了客戶端JDBC封裝
此問題中客戶端沒有關閉連接,發送FIN導致客戶端處於CLOSE_WAIT狀態,理論上服務端沒有收到FIN,應該處於FIN_WAIT_2的狀態,但實際觀察發現服務端已經完全關閉了,查看TCP配置發現,FIN_WAIT_2可通過tcp_fin_timeout配置FIN_WAIT_2超時時間,一旦超時會直接進入CLOSED狀態,而不經過TIME_WAIT
4 服務端TIME_WAIT
主動關閉TIME_WAIT,被動關閉CLOSE_WAIT
TIME_WAIT時間配置內核沒有透出,如果要改需重新編譯內核
查看內核源碼發現,默認TIME_WAIT時間為60s
https://yq.aliyun.com/ziliao/256040
Java客戶端Socket常用配置(進程級別配置)
- socket.setKeepAlive(true);
是否開啟tcp心跳檢測機制,默認不開啟,開啟后會根據OS tcp_keepalive_time、tcp_keepalive_intvl、tcp_keepalive_probes進行心跳檢測 - socket.setReuseAddress(true);
允許復用處於TIME_WAIT的socket - socket.setTcpNoDelay(true);在默認情況下,客戶端向服務器發送數據時,會根據數據包的大小決定是否立即發送。當數據包中的數據很少時,如只有1個字節,而數據包的頭卻有幾十個字節(IP頭+TCP頭)時,系統會在發送之前先將較小的包合並到較大的包后,一起將數據發送出去。在發送下一個數據包時,系統會等待服務器對前一個數據包的響應,當收到服務器的響應后,再發送下一個數據包,這就是所謂的Nagle算法;在默認情況下,Nagle算法是開啟的。
這種算法雖然可以有效地改善網絡傳輸的效率,但對於網絡速度比較慢,而且對實現性的要求比較高的情況下(如游戲、Telnet等),使用這種方式傳輸數據會使得客戶端有明顯的停頓現象。因此,最好的解決方案就是需要Nagle算法時就使用它,不需要時就關閉它。而使用setTcpToDelay正好可以滿足這個需求。當使用setTcpNoDelay(true)將Nagle算法關閉后,客戶端每發送一次數據,無論數據包的大小都會將這些數據發送出 - socket.setSoLinger(true, 0);
- socket.setSoTimeout(soTimeout);
配置inputstream一個阻塞read的超時時間
Linux常用TCP參數(OS級別配置)
- net.ipv4.tcp_keepalive_time
當keepalive起用的時候,TCP發送keepalive消息的頻度,單位為秒,缺省是7200秒(即2小時) - net.ipv4.tcp_keepalive_intvl
keepalive探測包的發送間隔 - net.ipv4.tcp_keepalive_probes
如果對方不予應答,探測包的發送次數 - net.ipv4.tcp_timestamps
為1表示開啟TCP時間戳,用來計算往返時間RTT(Round-Trip Time)和防止序列號回繞 - net.ipv4.tcp_tw_reuse
為1表示允許將TIME-WAIT的句柄重新用於新的TCP連接 - net.ipv4.tcp_tw_recycle
為1表示開啟TCP連接中TIME-WAIT的快速回收,NAT環境可能導致DROP掉SYN包(回復RST),不要輕易與net.ipv4.tcp_timestamps一起開啟 - net.ipv4.tcp_fin_timeout
FIN_WAIT_2狀態的超時時長 - net.ipv4.tcp_syncookies
為1時SYN Cookies,當SYN等待隊列溢出時啟用cookies來處理,可防范少量SYN攻擊 - net.ipv4.tcp_max_tw_buckets
保持TIME_WAIT套接字的最大個數,超過這個數字TIME_WAIT套接字將立刻被清除並打印警告信息 - net.ipv4.ip_local_port_range
設定tcp客戶端發起連接隨機端口范圍,默認32768,61000,這個配置限制了此機器訪問外部機器的連接數目 - net.ipv4.tcp_max_syn_backlog
端口最大backlog內核限制,防止占用過大內核內存 - net.ipv4.tcp_syn_retries
對一個新建連接,內核要發送多少個SYN連接請求才決定放棄,不應該大於255 - net.ipv4.tcp_retries1
放棄回應一個TCP連接請求前﹐需要進行多少次重試,RFC規定最低的數值是3,這也是默認值 - net.ipv4.tcp_retries2
在丟棄激活(已建立通訊狀況)的TCP連接之前﹐需要進行多少次重試,默認值為15 - net.ipv4.tcp_synack_retries
TCP三次握手的SYN/ACK階段重試次數,缺省5 - net.ipv4.tcp_max_orphans
不屬於任何進程(已經從進程上下文中刪除)的sockets最大個數,超過這個值會被立即RESET,並同時顯示警告信息 - net.ipv4.tcp_orphan_retries
孤兒sockets廢棄前重試的次數,缺省值是7 - net.ipv4.tcp_mem
內核分配給TCP連接的內存,單位是page:
第一個數字表示TCP使用的page少於此值時,內核不進行任何處理(干預),
第二個數字表示TCP使用的page超過此值時,內核進入“memory pressure”壓力模式,
第三個數字表示TCP使用的page超過些值時,報“Out of socket memory”錯誤,TCP 連接將被拒絕 - net.ipv4.tcp_rmem
為每個TCP連接分配的讀緩沖區內存大小,單位是byte - net.ipv4.tcp_wmem
為每個TCP連接分配的寫緩沖區內存大小,單位是byte:
第一個數字表示,為TCP連接分配的最小內存,
第二個數字表示,為TCP連接分配的缺省內存,
第三個數字表示,為TCP連接分配的最大內存(net.core.wmem_max可覆蓋該值)
參考文檔:
https://www.cnblogs.com/wangjq19920210/p/8440824.htm
https://www.zhihu.com/question/55890292
https://yq.aliyun.com/articles/581106
http://elf8848.iteye.com/blog/1739598
https://segmentfault.com/a/1190000012345710
TIME_WAIT很好的文章:
https://jin-yang.github.io/post/network-tcpip-timewait.html
tcp_timestamps抓包分析文章:
http://www.bubuko.com/infodetail-1650846.html