运维视角分析Java网络异常

近日我大NOS一台极其重要的MySQL数据库服务器触发内核bug(背啊~),导致数据库异常,大量连接卡住,直到切换备库前端tomcat才恢复(还有部分没有恢复,这个稍后再表)。从线上db异常到切换这一段时间,tomcat报了各种错误,可谓百花齐放,其中大多是网络相关的,我们就从这些错误入手,以运维的视角审视java程序网络异常。 超时是解决网络异常很重要的方法,文章的后半部分,我们会详细的介绍JDBC的几种超时。

一、几种常见的java.net.SocketException异常

1)
####################### ##############################################
Caused by: com.netease.backend.db.common.exceptions.DDBConnFullException: Get resource from pool 'ddb_nos_new:nos_common' time out
        at com.netease.backend.db.exec.conn.ConnResManager.getConnRes(ConnResManager.java:133)
#####################################################################
注解:这个错是从连接池获取数据库连接超时,严格来讲这不是个网络异常。但是  time out 却是我们经常遇到的一类异常,其中很大一部分是跨网络的服务导致的。它表示我们请求的模块或者服务长时间不给响应,超过了等待时间。看到这个异常你基本可以确定两点,一是自己依赖的服务不能正常处理请求了;二是自己(打这条日志的程序)目前还是正常的,线程还可以正常退出,没有因为依赖的服务而无限的等待。 

2)
#####################################################################
java.net.ConnectException: Connection refused
        at java.net.PlainSocketImpl.socketConnect(Native Method)
        at java.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:351)
        at java.net.PlainSocketImpl.connectToAddress(PlainSocketImpl.java:213)
        at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:200)
        at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:366)
        at java.net.Socket.connect(Socket.java:529)
#####################################################################
注解:错误是服务端拒绝请求。该异常发生在客户端进行 new Socket(ip, port)操作时,该异常发生的原因是找不到ip地址或者找不到指定的端口进行监听。如果不是配置有问题,则代表服务端已经不再监听端口。

3)
#####################################################################
java.net.SocketException: Connection reset
        at java.net.SocketInputStream.read(SocketInputStream.java:168)
        at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:113)
        at com.mysql.jdbc.util.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:160)
        at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:188)
#####################################################################
注解:Connection reset产生的原因是如果服务端的Socket端退出,但退出时并未关闭该连接(或主动关闭或者因为异常退出而引起的关闭),客户端如果再从连接中读数据则抛出该异常。该异常在客户端和服务器端均有可能发生。
与该异常相近的一个错误是 (Connect reset by peer),它发生在写数据的情况:如果一端的Socket被关闭,另一端仍发送数据,发送的第一个数据包引发该异常(Connect reset by peer),如果再继续写数据则抛出(java.net.SocketException: Broken pipe)。

下面再介绍几种这次事故没有遇到,但是也很常见的 SocketException

4)
#####################################################################
java.net.BindException:Address already in use: JVM_Bind。
#####################################################################
注解:该错误发生在服务监听端口的时候,它表示端口已经被占用,应该使用netstat检查下。

5)
#####################################################################
java.net.SocketException: Socket is closed
#####################################################################
注解:该异常在客户端和服务器均可能发生。异常的原因是己方主动关闭了连接后(调用了Socket的close方法)再对网络连接进行读写操作。

二、jdbc超时详解

前文说到有部分的tomcat在数据库切换后没有自动回复,我们的数据库一般是有镜像的,但是有时数据库切换到了镜像,应用服务器并没有自动恢复,需要重启才能正常服务。究其原因,是因为应用没有设置好超时,很多idle的tcp连接不能自动释放,需要等待操作系统释放(默认2小时),从而导致大量线程hung住。下面我们详细介绍下应用与数据库之间的超时设置:



这张图可以比较好的说明JDBC的超时层级,一般而言,数据库超时分为三个层级:(1)Transaction Timeout;(2)Statement Timeout;(3)socket timeout。这三个层次由高到低,从业务层深入到网络层。低级别的超时有效力要大于高层级的,例如 socket timeout设置的很短的话,Transaction Timeout设置的再大也不会起作用。

1) Transaction Timeout:
    transaction timeout一般存在于框架(Spring, EJB)或应用级。简单地说,transaction timeout就是“statement Timeout * N(需要执行的statement数量) + @(垃圾回收等其他时间)”。transaction timeout用来限制执行statement的总时长。

2)  Statement Timeout
    statement timeout用来限制statement的执行时长,timeout的值通过调用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API进行设置。不过现在开发者已经很少直接在代码中设置,而多是通过框架来进行设置。


MySQL JDBC timeout执行流程
       1. 通过调用Connection的createStatement()方法创建statement
  2. 调用Statement的executeQuery()方法
  3. statement通过自身connection将query发送给MySQL数据库
  4. statement创建一个新的timeout-execution线程用于超时处理
  5. 5.1版本后改为每个connection分配一个timeout-execution线程
  6. 向timeout-execution线程进行注册
  7. 达到超时时间
  8. TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法
  9. timeout-execution线程创建一个和statement配置相同的connection
  10. 使用新创建的connection向超时query发送cancel query(KILL QUERY “connectionId”)




Oracle JDBC Statement的QueryTimeout处理过程
  1. 通过调用Connection的createStatement()方法创建statement
  2. 调用Statement的executeQuery()方法
  3. statement通过自身connection将query发送给Oracle数据库
  4. statement在OracleTimeoutPollingThread(每个classloader一个)上进行注册
  5. 达到超时时间
  6. OracleTimeoutPollingThread调用OracleStatement的cancel()方法
  7. 通过connection向正在执行的query发送cancel消息

(3) socket timeout
    JDBC的socket timeout在数据库被突然停掉或是发生网络错误(由于设备故障等原因)时十分重要。由于TCP/IP的结构原因,socket没有办法探测到网络错误,因此应用也无法主动发现数据库连接断开。如果没有设置socket timeout的话,应用在数据库返回结果前会无期限地等下去(linux 默认的超时是2小时),这种连接被称为dead connection。  为了避免dead connections,socket必须要有超时配置。socket timeout可以通过JDBC设置,socket timeout能够避免应用在发生网络错误时产生无休止等待的情况,缩短服务失效的时间。
    下面是不同驱动的socket timeout配置方式


本文来自网易实践者社区,经作者潘威授权发布。