jdk1.6空轮询Bug的原因及解决方法

news/2024/7/9 17:27:18 标签: java, epoll, netty

简述

本文主要介绍一下jdk1.6版本中的NIO Selector空轮询BUG,描述一下BUG的现象及原因,以及Netty中如何巧妙的规避了这个bug。

为什么要写这篇文章,说来惭愧,很久以前面试官问我,知道jdk空轮询问题吗,为什么会有这个问题,如何解决这个问题?我没答上来。。

Selector空轮询BUG

重现场景步骤

  1. 服务端等待连接
  2. 客户端发起连接,发送消息
  3. 服务端接受连接,并注册监听通道的OP_READ
  4. 服务端读取消息,从感兴趣事件集合中移除OP_READ
  5. 客户端关闭连接
  6. 服务端给客户端发送消息
  7. 服务端select方法不再阻塞,无限被唤醒并且返回值为0.

实验结果

在window上,此步骤下,是正常的。但是在linux机器上,selector陷入了死循环(cpu100%)。

上面是官方JDK-6670302 : (se) NIO selector wakes up with 0 selected keys infinitely [lnx 2.4]给出的重现实验步骤。

bug根源

官方在6670302-BUG页面上好像并不认为是jdk的bug。也没给出具体原因。而把原因归结为Linux Kernel 2.4版本的bug(JDK-6481709)。官方认为linux 内核2.6版本解决这个bug并且也发行了4年了,更建议大家使用linux kernel2.6。

笔者愚钝,看了JDK-6481709这个BUG后,并没发现产生的原因。

后来终于在JDK-6403933 : (se) Selector doesn't block on Selector.select(timeout) (lnx)这个bug里找到了貌似是答案的答案。

问题产生于linux的epoll(显然是被甩锅了)。如果一个socket文件描述符,注册的事件集合码为0,然后连接突然被对端中断,那么epoll会被POLLHUP或者有可能是POLLERR事件给唤醒,并返回到事件集中去。这意味着,Selector会被唤醒,即使对应的channel兴趣事件集是0,并且返回的events事件集合也是0。

简而言之就是,jdk认为linux的epoll告诉我事件来了,但是jdk没有拿到任何事件(READ、WRITE、CONNECT、ACCPET)。但此时select()方法不再选择阻塞了,而是选择返回了0。

BUG现状

官方页面中显示jdk6u4版本和jdk7b12版本都已解决。实际上在1.6,1.7,1.8都没有解决。
也就是说linux内核为2.4的,使用jdk6u4以下的开发者,仍可能遭遇此bug。

其实官方也提供了解决的思路。

解决方案

JDK-6403933里面提到了几种方案,我总结一下:

  1. 取消对应的key,马上刷新Selector。就是在重现步骤中的第4步,立马调用selector.selectNow刷新一次selector。

     

  2. 如果注册到selector兴趣事件集为0,则直接取消注册。 如果注册到selector兴趣事件集不为0,则需要将linux epoll事件POLLHUP/POLLERR转化为OP_READ 或者OP_WRITE。由谁决定转化呢,笔者认为应该由jdk。这样程序就有机会探测到IO异常。

  3. 丢弃旧的selector,重新构造一个。

三种方法,笔者认为1、2都可能没有彻底解决问题。第一种,selectNow的调用,只是select的非阻塞版本,非常有可能在多线程中和selectionKey.cancel同时调用的。第二种方案,即使读写channel数据时抛出了IO异常,不是所有人都会记得关闭此Channel并deregister这个channel。

至于第三种方案,应该是可行的,因为重新构造了selector,需要重新注册channnel到其上,并注册感兴趣事件,重新注册的过程中有机会检测channel的可用性。但是什么时候需要重新创建一个呢?这可能就需要一些检测空轮询的机制了

Netty3中如何解决

netty3采用的是第三种方案,检测重点是select函数是否返回了0。代码在AbstractNioSelector类中

java">java">if (timeBlocked < minSelectTimeout) { boolean notConnected = false; //循环遍历所有selectionKey,剔除可能导致selector唤醒的被关闭的channel for (SelectionKey key : selector.keys()) { SelectableChannel ch = key.channel(); try { if (ch instanceof DatagramChannel && !ch.isOpen() || ch instanceof SocketChannel && !((SocketChannel) ch).isConnected()) { notConnected = true; //发现了关闭的通道赶紧取消以防万一,不会再下次select的key集合中 key.cancel(); } } catch (CancelledKeyException e) { // ignore } } if (notConnected) { selectReturnsImmediately = 0; } else { //到这里,发生了一次selector在关闭的通道上被唤醒,所以记数+1 //防止引起jdk epoll的bug selectReturnsImmediately++; } } else { selectReturnsImmediately = 0; } if (selectReturnsImmediately == 1024) { //发生了1024次了,应该碰到著名的epollbug了, //重新构造一个selector rebuildSelector(); selector = this.selector; selectReturnsImmediately = 0; wakenupFromLoop = false; continue; } 

这里,netty通过线程不断循环检测select是否返回0,若发生了1024次(次数不重要,若发生了epoll bug,肯定次数飙升),则开始重建selector。

看看重建的seletor代码,rebuildSelector方法:

java">java">public void rebuildSelector() { final Selector oldSelector = selector; final Selector newSelector; if (oldSelector == null) { return; } try { newSelector = SelectorUtil.open(); } catch (Exception e) { logger.warn("Failed to create a new Selector.", e); return; } // 将老的channel重新注册到新selector上 int nChannels = 0; for (; ; ) { try { for (SelectionKey key : oldSelector.keys()) { try { if (key.channel().keyFor(newSelector) != null) { continue; } int interestOps = key.interestOps(); key.cancel(); key.channel().register(newSelector, interestOps, key.attachment()); nChannels++; } catch (Exception e) { logger.warn("Failed to re-register a Channel to the new Selector,", e); close(key); } } } catch (ConcurrentModificationException e) { continue; } break; } selector = newSelector; try { //关闭老的selector oldSelector.close(); } catch (Throwable t) { if (logger.isWarnEnabled()) { logger.warn("Failed to close the old Selector.", t); } } } 
  1. AbstractNioSelector会启动一个线程,在当前selector会循环调用selector.select(timeout)方法,如果在timeout时间之内,selector返回了,则需要检测唤醒它的SelectionKey里面,有没有未关闭的连接channel存在。有则取消这个key。这能防止引起epoll bug。
  2. 什么时候可以认为发生了epoll bug呢,就是阻塞的select方法提前被唤醒了并且返回了0。有就增加计数器,计数器的值很快会到1024,然后就可以重建一个selector,抛弃那个已经在无限轮回的oldSelector。
  3. 将oldselector上的key都取消掉,重新注册到新的selector上。关闭oldSelector。

总结

本文讲述了jdk epoll bug的原因,及解决方法。原因是给关闭的通道发消息。解决的最好方法,是重建一个selector。

 

转载于:https://www.cnblogs.com/qiumingcheng/p/9481528.html


http://www.niftyadmin.cn/n/658615.html

相关文章

vb中利用xmlhttp来下载远程文件

建立一个vb工程&#xff0c;Project1添加引用&#xff1a;Microsoft scripting runtime,Microsoft Active Data Object,Microsoft MsXml Form1代码&#xff1a;Public a As MSXML2.XMLHTTP Private Sub Command1_Click() Dim d As Class1 Set a New MSXML2.XMLHTTP a.…

列表文

列表是我们最以后最常用的数据类型之一&#xff0c;通过列表可以对数据实现最方便的存储、修改等操作 定义列表 1names [Alex,"Tenglan",Eric]通过下标访问列表中的元素&#xff0c;下标从0开始计数 12345678>>> names[0]Alex>>> names[2]Eric>…

spring-cloud: eureka之:ribbon负载均衡自定义配置(二)

spring-cloud: eureka之&#xff1a;ribbon负载均衡自定义配置&#xff08;二&#xff09; 有默认配置的话基本上就是轮询接口&#xff0c;现在我们改用自定义配置&#xff0c;同时支持&#xff1a;轮询&#xff0c;随机接口读取 准备工作&#xff1a; 1.eureka服务 2.两个user…

Flask Web框架

Flask依赖两个外部库&#xff1a;Werkzeug和Jinja2。Werkzeug是一个WSGI&#xff08;在Web应用和多种服务器之间的标准Python接口&#xff09;工具集&#xff1b;Jinja2负责渲染模板。所以在安装Flask之前&#xff0c;需要安装这两个外部库。 安装虚拟环境 1、安装Virtualenv …

002-常用的Linux命令

用户相关&#xff1a; 增加用户 jeff adduser jeff passwd jeff 切换用户 到jeff su - jeff     显示当前用户所在的目录 pwd     在Linux系统下&#xff0c;默认情况下&#xff0c;创建一个用户的同时会给用户在系统的目录下创建一个属于自己的用户目录&#xff0c;该…

Java父类调用被子类重写的方法

https://blog.csdn.net/zhuoaiyiran/article/details/194897451.如果父类构造器调用了被子类重写的方法&#xff0c;且通过子类构造函数创建子类对象&#xff0c;调用了这个父类构造器&#xff08;无论显示还是隐式&#xff09;&#xff0c;就会导致父类在构造时实际上调用的是…

openstack swift 安装(单独对象存储服务)

参考&#xff1a;https://docs.openstack.org/mitaka/zh_CN/install-guide-rdo/swift.html 安装YUM包 yum install centos-release-openstack-rocky -y 注意&#xff1a;先决条件不需要执行&#xff0c;从安装并配置组件开始 与authtoken keystoneauth不需要执行 在[pipeline:m…

利用XMLHTTP无刷新添加数据[转]

我们讲讲如何添加数据.我们传统的提交数据的方法都是用<Form>来实现的.<Form>标记中的Method属性确定了表单元素的数据在发送到服务器时,如何对HTTP请求信息进行打包. Method 属性可以使用的方法Method属性 发送表单元素的方式 读…