Java Socket网络编程常见的异常有哪些,然后通过一个实验来重现其中的Connection reset异常,并且通过配置Tomcat的参数来解决这个问题。

异常场景

java.net.SocketTimeoutException

超时异常,超时分为

  • 连接超时
    在调用Socket.connect方法的时候超时,大多因为网络不稳定
  • 读取超时
    调用Socket.read方法时超时。不一定是因为网络延迟,很可能下游服务的响应时间过长

java.net.BindException: Address already in use: JVM_Bind

端口被占用。
当服务器端调用

  • new ServerSocket(port)
  • 或Socket.bind函数

若端口已被占用,就会抛该异常。

可以用

netstat –an

Tomcat各种网络异常场景解决方案及优化_客户端
查看端口被谁占用了,换个空闲端口即可。

java.net.ConnectException: Connection refused: connect

连接被拒绝。
当客户端调用

  • new Socket(ip, port)
  • 或Socket.connect函数

原因是:

  • 未找到指定IP的机器
  • 机器存在,但该机器上没有开启指定监听端口

解决方案

从客户端机器ping一下服务端IP:

  • ping不通,看看IP是不是写错了?
  • ping通,需要确认服务端的服务是不是挂了?

java.net.SocketException: Socket is closed

连接已关闭。

通信的一方主动关闭了Socket连接(调用了Socket的close方法),接着又对Socket连接进行了读写操作,这时os会报“Socket连接已关闭”。

java.net.SocketException: Connection reset/Connect reset by peer: Socket write error

连接被重置。

  • 通信的一方已将Socket关闭,可能是主动关闭或是因为异常退出,这时如果通信的另一方还在写数据,就会触发这个异常(Connect reset by peer)
  • 若对方还在尝试从TCP连接中读数据,则会抛出Connection reset异常。

为了避免这些异常发生,在编写网络通信程序时要确保:

  • 程序退出前要主动关闭所有的网络连接
  • 检测通信的另一方的关闭连接操作,当发现另一方关闭连接后自己也要关闭该连接。

java.net.SocketException: Broken pipe

通信管道已坏。

发生这个异常的场景是,通信的一方在收到“Connect reset by peer: Socket write error”后,如果再继续写数据则会抛出Broken pipe异常,解决方法同上。

java.net.SocketException: Too many open files

进程打开文件句柄数超过限制。

触发场景

当并发用户数比较大时。
因为每创建一个Socket连接就需一个文件句柄,而且服务端程序在处理请求时可能也需要打开一些文件。

可通过

lsof -p pid

查看进程打开了哪些文件,是否有资源泄露,即进程打开的这些文件本应该被关闭,但由于程序的Bug而没有被关闭。

若无资源泄露,可通过设置增加最大文件句柄数:通过

ulimit -a

查看系统目前资源限制,通过

ulimit -n 10240

修改最大文件数。

Tomcat网络参数
  • maxConnections
  • acceptCount

TCP连接的建立过程

客户端向服务端发送SYN包,服务端回复SYN+ACK,同时将这个处于SYN_RECV状态的连接保存到半连接队列。

客户端返回ACK包完成三次握手,服务端将ESTABLISHED状态的连接移入accept队列,等待应用程序(Tomcat)调用accept方法将连接取走。
这里涉及两个队列:

  • 半连接队列:保存SYN_RECV状态的连接
    队列长度由net.ipv4.tcp_max_syn_backlog设置
  • accept队列:保存ESTABLISHED状态的连接
    队列长度为min(net.core.somaxconn,backlog)。其中backlog是我们创建ServerSocket时指定的参数,最终会传递给listen方法:
int listen(int sockfd, int backlog);

若设置的backlog大于net.core.somaxconn,accept队列的长度将被设置为net.core.somaxconn,而这个backlog参数就是Tomcat中的acceptCount参数,默认值100,但请注意net.core.somaxconn默认值128。

在高并发情况下当Tomcat来不及处理新连接时,这些连接都被堆积在accept队列,而acceptCount参数可以控制accept队列长度。超过该长度,内核会向客户端发送RST,这样客户端会触发“Connection reset”异常。

Tomcat#maxConnections 指Tomcat在任意时刻接收和处理的最大连接数。
当Tomcat接收的连接数达到maxConnections时,Acceptor线程不会再从accept队列取走连接,这时accept队列中的连接会越积越多。

maxConnections的默认值与连接器类型有关:

  • NIO的默认值是10000
  • APR默认是8192

所以Tomcat

最大并发连接数 = maxConnections + acceptCount

若acceptCount

  • 设置过大
    请求等待时间会比较长
  • 设置过小
    高并发情况下,客户端会立即触发Connection reset异常
TIME_WAIT

通过netstat命令发现有大量的TCP连接处在TIME_WAIT状态,请问这是为什么?它可能会带来什么样的问题呢?

增大accept队列长度会使得Tomcat并发短连接数暴增,势必导致服务器处理完请求后,需主动断开连的连接数增加;
TCP 连接处在 TIME_WAIT 状态,是TCP协议规定的,四次挥手时主动关闭方所处的一个状态。断开连接时四次挥手的最后一个阶段,客户端要等待2MSL,保证服务端收到客户端ack(如果服务端没有收到最后一次挥手ack会重试,这时客户端需重新发送ack),所以在这个时间段内不会释放端口,若并发量过大会导致大量time_wait;一旦达到上限将导致服务器拒绝服务。

Tomcat网络调优实战

复现流量高峰时accept队列堆积场景,即会导致客户端触发“Connection reset”异常,然后调参解决。

步骤

  1. JMeter 创建一个测试计划、一个线程组、一个请求。
    测试计划:
    Tomcat各种网络异常场景解决方案及优化_java_02
    线程组(线程数这里设置为1000,模拟大流量):
    Tomcat各种网络异常场景解决方案及优化_java_03

请求(请求的路径是Tomcat自带的例子程序):
Tomcat各种网络异常场景解决方案及优化_客户端_04

2.启动Tomcat。
3.开启JMeter测试,在View Results Tree中会看到大量失败的请求,请求的响应里有“Connection reset”异常,也就是前面提到的,当accept队列溢出时,服务端的内核发送了RST给客户端,使得客户端抛出了这个异常。
Tomcat各种网络异常场景解决方案及优化_tomcat_05

  1. 修改内核参数,在/etc/sysctl.conf中增加一行net.core.somaxconn=2048,然后执行命令sysctl -p

  2. 修改Tomcat参数acceptCount为2048,重启Tomcat
    Tomcat各种网络异常场景解决方案及优化_java_06

  3. 再次启动JMeter,这次所有请求会成功,看不到异常了。我们可以通过下面的命令看到系统中ESTABLISHED的连接数增大了,这是因为我们加大了accept队列的长度。
    Tomcat各种网络异常场景解决方案及优化_客户端_07