简介
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