前言

最近在开发中用到netty,发现netty自己提供了native socket transport.Netty的核心开发者 Norman Maurer的回答https://stackoverflow.com/questions/23465401/why-native-epoll-support-is-introduced-in-netty,Netty的 epoll transport使用 epoll edge-triggered 而 java的 nio 使用 level-triggered.另外netty epoll transport 暴露了更多的nio没有的配置参数, 如 TCP_CORK, SO_REUSEADDR等等

epoll的事件派发接口可以运行在两种模式下:边缘触发(edge-triggered 简称 ET)和水平触发(level-triggered 简称LT):LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,应用程序read一个fd的时候一定要把它的buffer读光。所以ET模式效率更高。

我对SO_RESUEADDR比较感兴趣:参照这篇文章socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean总结一下

背景

TCP/UDP是由以下五元组唯一地识别的:
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
这些数值组成的组合可以唯一地确一个连接。对于任意连接,这五个值都不能完全相同。否则的话操作系统就无法区别这些连接了。

BSD

现在的所有操作系统socket实现都参照了BSD的socket实现,然后各自布发展了一些新特性。

SO_REUSEADDR

如果在一个socket绑定到某一地址和端口之前设置了其SO_REUSEADDR的属性,那么除非本socket与产生了尝试与另一个socket绑定到完全相同的源地址和源端口组合的冲突,否则的话这个socket就可以成功的绑定这个地址端口对。SO_REUSEADDR主要改变了系统对待通配符IP地址冲突的方式。如果不用SO_REUSEADDR的话,如果我们将socketA绑定到0.0.0.0:8080,那么任何将本机其他socket绑定到端口8080的举动都不会成功。因为0.0.0.0是一个通配符IP地址,意味着任意一个IP地址,所以任何其他本机上的IP地址都被系统认为已被占用。果设置了SO_REUSEADDR选项,因为0.0.0.0:21和192.168.1.1:21并不是完全相同的地址端口对(其中一个是通配符IP地址,另一个是一个本机的具体IP地址),所以这样的绑定是可以成功的。需要注意的是,无论socketA和socketB初始化的顺序如何,只要设置了SO_REUSEADDR,绑定都会成功;而只要没有设置SO_REUSEADDR,绑定都不会成功。

SO_REUSEADDR socketA socketB Result
ON / OFF 192.168.1.1:21 192.168.1.1:21 ERROR
ON / OFF 192.168.1.1:21 10.0.1.1:21 OK
ON / OFF 10.0.1.1:21 192.168.1.1:21 OK
OFF 192.168.1.1:21 0.0.0.0:21 ERROR
OFF 0.0.0.0:21 192.168.1.1:21 ERROR
ON 192.168.1.1:21 0.0.0.0:21 OK
ON 0.0.0.0:21 192.168.1.1:21 OK
ON / OFF 0.0.0.0:21 0.0.0.0:21 ERROR

这个表格假定socketA已经成功地绑定了表格中对应的地址,然后socketB被初始化了,其SO_REUSEADDR设置的情况如表格第一列所示,然后socketB试图绑定表格中对应地址。Result列是其绑定的结果。如果第一列中的值是ON/OFF,那么SO_REUSEADDR设置与否都与结果无关。

上面讨论了SO_REUSEADDR对通配符IP地址的作用,但其并不只有这一作用。其另一作用也是为什么大家在进行服务器端编程的时候会采用SO_REUSEADDR选项的原因。为了理解其另一个作用及其重要应用,我们需要先更深入地讨论一下TCP协议的工作原理。

每一个socket都有其相应的发送缓冲区(buffer)。当成功调用其send()方法的时候,实际上我们所要求发送的数据并不一定被立即发送出去,而是被添加到了发送缓冲区中。对于UDP socket来说,即使不是马上被发送,这些数据一般也会被很快发送出去。但对于TCP socket来说,在将数据添加到发送缓冲区之后,可能需要等待相对较长的时间之后数据才会被真正发送出去。因此,当我们关闭了一个TCP socket之后,其发送缓冲区中可能实际上还仍然有等待发送的数据。但此时因为send()返回了成功,我们的代码认为数据已经实际上被成功发送了。如果TCP socket在我们调用close()之后直接关闭,那么所有这些数据都将会丢失,而我们的代码根本不会知道。但是,TCP是一个可靠的传输层协议,直接丢弃这些待传输的数据显然是不可取的。实际上,如果在socket的发送缓冲区中还有待发送数据的情况下调用了其close()方法,其将会进入一个所谓的TIME_WAIT状态。在这个状态下,socket将会持续尝试发送缓冲区的数据直到所有数据都被成功发送或者直到超时,超时被触发的情况下socket将会被强制关闭。

操作系统的kernel在强制关闭一个socket之前的最长等待时间被称为延迟时间(Linger Time)。在大部分系统中延迟时间都已经被全局设置好了,并且相对较长(大部分系统将其设置为2分钟)。我们也可以在初始化一个socket的时候使用SO_LINGER选项来特定地设置每一个socket的延迟时间。我们甚至可以完全关闭延迟等待。但是需要注意的是,将延迟时间设置为0(完全关闭延迟等待)并不是一个好的编程实践。因为优雅地关闭TCP socket是一个比较复杂的过程,过程中包括与远程主机交换数个数据包(包括在丢包的情况下的丢失重传),而这个数据包交换的过程所需要的时间也包括在延迟时间中。如果我们停用延迟等待,socket不止会在关闭的时候直接丢弃所有待发送的数据,而且总是会被强制关闭(由于TCP是面向连接的协议,不与远端端口交换关闭数据包将会导致远端端口处于长时间的等待状态)。所以通常我们并不推荐在实际编程中这样做。TCP断开连接的过程超出了本文讨论的范围,如果对此有兴趣,可以参考这个页面。并且实际上,如果我们禁用了延迟等待,而我们的程序没有显式地关闭socket就退出了,BSD(可能包括其他系统)会忽略我们的设置进行延迟等待。例如,如果我们的程序调用了exit()方法,或者其进程被使用某个信号终止了(包括进程因为非法内存访问之类的情况而崩溃)。所以我们无法百分之百保证一个socket在所有情况下忽略延迟等待时间而终止。

这里的问题在于操作系统如何对待处于TIME_WAIT阶段的socket。如果SO_REUSEADDR选项没有被设置,处于TIME_WAIT阶段的socket任然被认为是绑定在原来那个地址和端口上的。直到该socket被完全关闭之前(结束TIME_WAIT阶段),任何其他企图将一个新socket绑定该该地址端口对的操作都无法成功。这一等待的过程可能和延迟等待的时间一样长。所以我们并不能马上将一个新的socket绑定到一个刚刚被关闭的socket对应的地址端口对上。在大多数情况下这种操作都会失败。

然而,如果我们在新的socket上设置了SO_REUSEADDR选项,如果此时有另一个socket绑定在当前的地址端口对且处于TIME_WAIT阶段,那么这个已存在的绑定关系将会被忽略。事实上处于TIME_WAIT阶段的socket已经是半关闭的状态,将一个新的socket绑定在这个地址端口对上不会有任何问题。这样的话原来绑定在这个端口上的socket一般不会对新的socket产生影响。但需要注意的是,在某些时候,将一个新的socket绑定在一个处于TIME_WAIT阶段但仍在工作的socket所对应的地址端口对会产生一些我们并不想要的,无法预料的负面影响。但这个问题超过了本文的讨论范围。而且幸运的是这些负面影响在实践中很少见到。

最后,关于SO_REUSEADDR,我们还要注意的一件事是,以上所有内容只要我们对新的socket设置了SO_REUSEADDR就成立。至于原有的已经绑定在当前地址端口对上的,处于或不处于TIME_WAIT阶段的socket是否设置了SO_REUSEADDR并无影响。决定bind操作是否成功的代码仅仅会检查新的被传递到bind()方法的socket的SO_REUSEADDR选项。其他涉及到的socket的SO_REUSEADDR选项并不会被检查。

SO_REUSEPORT

SO_REUSEPORT并不等于SO_REUSEADDR。这么说的含义是如果一个已经绑定了地址的socket没有设置SO_REUSEPORT,而另一个新socket设置了SO_REUSEPORT且尝试绑定到与当前socket完全相同的端口地址对,这次绑定尝试将会失败。同时,如果当前socket已经处于TIME_WAIT阶段,而这个设置了SO_REUSEPORT选项的新socket尝试绑定到当前地址,这个绑定操作也会失败。为了能够将新的socket绑定到一个当前处于TIME_WAIT阶段的socket对应的地址端口对上,我们要么需要在绑定之前设置这个新socket的SO_REUSEADDR选项,要么需要在绑定之前给两个socket都设置SO_REUSEPORT选项。当然,同时给socket设置SO_REUSEADDR和SO_REUSEPORT选项是也是可以的。

SO_REUSEPORT是在SO_REUSEADDR之后被添加到BSD系统中的。这也是为什么现在有些系统的socket实现里没有SO_REUSEPORT选项。因为它们在这个选项被加入BSD系统之前参考了BSD的socket实现。而在这个选项被加入之前,BSD系统下没有任何办法能够将两个socket绑定在完全相同的地址端口对上。

FreeBSD/OpenBSD/NetBSD

所有这些系统都是参考了较新的原生BSD系统代码。所以这三个系统提供与BSD完全相同的socket选项,这些选项的含义与原生BSD完全相同。

MacOS X

MacOS X的核心代码实现是基于较新版本的原生BSD的BSD风格的UNIX,所以MacOS X提供与BSD完全相同的socket选项,并且它们的含义也与BSD系统相同。

iOS

iOS事实上是一个略微改造过的MacOS X,所以适用于MacOS X的也适用于iOS。

Linux

在Linux3.9之前,只有SO_REUSEADDR选项存在。这个选项的作用基本上同BSD系统下相同。但其仍有两个重要的区别。

第一个区别是如果一个处于监听(服务器)状态下的TCP socket已经被绑定到了一个通配符IP地址和一个特定端口下,那么不论这两个socket有没有设置SO_REUSEADDR选项,任何其他TCP socket都无法再被绑定到相同的端口下。即使另一个socket使用了一个具体IP地址(像在BSD系统中允许的那样)也不行。而非监听(客户)TCP socket则无此限制。

第二个区别是对于UDP socket来说,SO_REUSEADDR的作用和BSD中SO_REUSEPORT完全相同。所以两个UDP socket如果都设置了SO_REUSEADDR的话,它们就可以被绑定在一组完全相同的地址端口对上。

Linux3.9加入了SO_REUSEPORT选项。只要所有socket(包括第一个)在绑定地址前设置了这个选项,两个或多个,TCP或UDP,监听(服务器)或非监听(客户)socket就可以被绑定在完全相同的地址端口组合下。同时,为了防止端口劫持(port hijacking),还有一个特别的限制:所有试图绑定在相同的地址端口组合的socket必须属于拥有相同用户ID的进程。所以一个用户无法从另一个用户那里“偷窃”端口。

除此之外,对于设置了SO_REUSEPORT选项的socket,Linux kernel还会执行一些别的系统所没有的特别的操作:对于绑定于同一地址端口组合上的UDP socket,kernel尝试在它们之间平均分配收到的数据包;对于绑定于同一地址端口组合上的TCP监听socket,kernel尝试在它们之间平均分配收到的连接请求(调用accept()方法所得到的请求)。这意味着相比于其他允许地址复用但随机将收到的数据包或者连接请求分配给连接在同一地址端口组合上的socket的系统而言,Linux尝试了进行流量分配上的优化。比如一个简单的服务器进程的几个不同实例可以方便地使用SO_REUSEPORT来实现一个简单的负载均衡,而且这个负载均衡有kernel负责, 对程序来说完全免费!

也就是说可以在同一个机器上,针对统一端口和ip启动多个进程,操作系统负责在这些进程中负载均衡

Android

Android的核心部分是略微修改过的Linux kernel,所以所有适用于Linux的操作也适用于Android。

Windows

Windows仅有SO_REUSEADDR选项。在Windows中对一个socket设置SO_REUSEADDR的效果与在BSD下同时对一个socket设置SO_REUSEPORT和SO_REUSEADDR相同。但其区别在于:即使另一个已绑定地址的socket并没有设置SO_REUSEADDR,一个设置了SO_REUSEADDR的socket总是可以绑定到与另一个已绑定的socket完全相同的地址端口组合上。这个行为可以说是有些危险的。因为它允许了一个应用从另一个引用已连接的端口上偷取数据。微软意识到了这个问题,因此添加了另一个socket选项:SO_EXCLUSIVEADDRUSE。对一个socket设置SO_EXCLUSIVEADDRUSE可以确保一旦该socket绑定了一个地址端口组合,任何其他socket,不论设置SO_REUSEADDR与否,都无法再绑定当前的地址端口组合。

Solaris

Solaris是SunOS的继任者。SunOS从某种程度上来说也是一个较早版本的BSD的一个支路。因此Solaris只提供SO_REUSEADDR,且其表现和BSD系统中基本相同。据我所知,在Solaris系统中无法实现与SO_REUSEPORT相同的功能。这意味着在Solaris中无法将两个socket绑定到完全相同的地址端口组合下。

与Windows类似的是,Solaris也为socket提供独占绑定的选项——SO_EXCLBIND。如果一个socket在绑定地址前设置了这个选项,即使其他socket设置了SO_REUSEADDR也将无法绑定至相同地址。例如:如果socketA绑定在了通配符IP地址下,而socketB设置了SO_REUSEADDR且绑定在一个具体IP地址和与socketA相同的端口的组合下,这个操作在socketA没有设置SO_EXCLBIND的情况下会成功,否则会失败。

背景

前几天在客户的生产环境上发现一个问题,开发的一个java服务,cpu占用率特别高,严重影响机器性能。当时重启了这个服务,临时解决了这个问题,还好在重启之前生成了一下javaCore文件,这几天分析java core后发现这是一个bug。

分析 javaCore

通过分析间隔几秒生成的javaCore文件。占用cpu资源最多的几个线程都在执行如下操作:

由于现象是cpu占用过高,结合上面的堆栈,以及这个项目是用netty建立了一个tcp服务器,猜测是某个地方可能有死循环,导致cpu过高。根据堆栈提供的信息,直接查看netty源码,从栈顶往下找,第一个netty类,SelectorUtil:

/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package org.jboss.netty.channel.socket.nio;

import org.jboss.netty.logging.InternalLogger;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.jboss.netty.util.internal.SystemPropertyUtil;

import java.io.IOException;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.Selector;
import java.util.concurrent.TimeUnit;

final class SelectorUtil {
    private static final InternalLogger logger =
        InternalLoggerFactory.getInstance(SelectorUtil.class);

    static final int DEFAULT_IO_THREADS = Runtime.getRuntime().availableProcessors() * 2;
    static final long DEFAULT_SELECT_TIMEOUT = 500;
    static final long SELECT_TIMEOUT =
            SystemPropertyUtil.getLong("org.jboss.netty.selectTimeout", DEFAULT_SELECT_TIMEOUT);
    static final long SELECT_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(SELECT_TIMEOUT);
    static final boolean EPOLL_BUG_WORKAROUND =
            SystemPropertyUtil.getBoolean("org.jboss.netty.epollBugWorkaround", false);

    // Workaround for JDK NIO bug.
    //
    // See:
    // - http://bugs.sun.com/view_bug.do?bug_id=6427854
    // - https://github.com/netty/netty/issues/203
    static {
        String key = "sun.nio.ch.bugLevel";
        try {
            String buglevel = System.getProperty(key);
            if (buglevel == null) {
                System.setProperty(key, "");
            }
        } catch (SecurityException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("Unable to get/set System Property '" + key + '\'', e);
            }
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Using select timeout of " + SELECT_TIMEOUT);
            logger.debug("Epoll-bug workaround enabled = " + EPOLL_BUG_WORKAROUND);
        }
    }

    static Selector open() throws IOException {
        return Selector.open();
    }

    static int select(Selector selector) throws IOException {
        try {
            return selector.select(SELECT_TIMEOUT);
        } catch (CancelledKeyException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(
                        CancelledKeyException.class.getSimpleName() +
                        " raised by a Selector - JDK bug?", e);
            }
            // Harmless exception - log anyway
        }
        return -1;
    }

    private SelectorUtil() {
        // Unused
    }
}

查看select()方法,在catch代码块一个一个日志输出,提示jdk bug但是有没有详细说明。
往上看有一个静态语句块,看注释,提到了一个jdk的bugNullPointerException in Selector.open()看了一下跟我遇到的不一样。在往上看有一个系统属性org.jboss.netty.epollBugWorkaround,看名字好像是为了绕开epollBug用的。好像跟这个有关系,直接查看这个变量被谁引用了。发现在AbstractNioSelector.run()方法中有引用,二在上面的线程栈中,正好也有这个方法,直接进入这个方法查看。

public void run() {
        thread = Thread.currentThread();
        startupLatch.countDown();

        int selectReturnsImmediately = 0;
        Selector selector = this.selector;

        if (selector == null) {
            return;
        }
        // use 80% of the timeout for measure
        final long minSelectTimeout = SelectorUtil.SELECT_TIMEOUT_NANOS * 80 / 100;
        boolean wakenupFromLoop = false;
        for (;;) {
            wakenUp.set(false);

            try {
                long beforeSelect = System.nanoTime();
                int selected = select(selector);
                if (selected == 0 && !wakenupFromLoop && !wakenUp.get()) {
                    long timeBlocked = System.nanoTime() - beforeSelect;
                    if (timeBlocked < minSelectTimeout) {
                        boolean notConnected = false;
                        // loop over all keys as the selector may was unblocked because of a closed channel
                        for (SelectionKey key: selector.keys()) {
                            SelectableChannel ch = key.channel();
                            try {
                                if (ch instanceof DatagramChannel && !ch.isOpen() ||
                                        ch instanceof SocketChannel && !((SocketChannel) ch).isConnected() &&
                                                // Only cancel if the connection is not pending
                                                // See https://github.com/netty/netty/issues/2931
                                                !((SocketChannel) ch).isConnectionPending()) {
                                    notConnected = true;
                                    // cancel the key just to be on the safe side
                                    key.cancel();
                                }
                            } catch (CancelledKeyException e) {
                                // ignore
                            }
                        }
                        if (notConnected) {
                            selectReturnsImmediately = 0;
                        } else {
                            if (Thread.interrupted() && !shutdown) {
                                // Thread was interrupted but NioSelector was not shutdown.
                                // As this is most likely a bug in the handler of the user or it's client
                                // library we will log it.
                                //
                                // See https://github.com/netty/netty/issues/2426
                                if (logger.isDebugEnabled()) {
                                    logger.debug("Selector.select() returned prematurely because the I/O thread " +
                                            "has been interrupted. Use shutdown() to shut the NioSelector down.");
                                }
                                selectReturnsImmediately = 0;
                            } else {
                                // Returned before the minSelectTimeout elapsed with nothing selected.
                                // This may be because of a bug in JDK NIO Selector provider, so increment the counter
                                // which we will use later to see if it's really the bug in JDK.
                                selectReturnsImmediately ++;
                            }
                        }
                    } else {
                        selectReturnsImmediately = 0;
                    }
                } else {
                    selectReturnsImmediately = 0;
                }

                if (SelectorUtil.EPOLL_BUG_WORKAROUND) {
                    if (selectReturnsImmediately == 1024) {
                        // The selector returned immediately for 10 times in a row,
                        // so recreate one selector as it seems like we hit the
                        // famous epoll(..) jdk bug.
                        rebuildSelector();
                        selector = this.selector;
                        selectReturnsImmediately = 0;
                        wakenupFromLoop = false;
                        // try to select again
                        continue;
                    }
                } else {
                    // reset counter
                    selectReturnsImmediately = 0;
                }

                // 'wakenUp.compareAndSet(false, true)' is always evaluated
                // before calling 'selector.wakeup()' to reduce the wake-up
                // overhead. (Selector.wakeup() is an expensive operation.)
                //
                // However, there is a race condition in this approach.
                // The race condition is triggered when 'wakenUp' is set to
                // true too early.
                //
                // 'wakenUp' is set to true too early if:
                // 1) Selector is waken up between 'wakenUp.set(false)' and
                //    'selector.select(...)'. (BAD)
                // 2) Selector is waken up between 'selector.select(...)' and
                //    'if (wakenUp.get()) { ... }'. (OK)
                //
                // In the first case, 'wakenUp' is set to true and the
                // following 'selector.select(...)' will wake up immediately.
                // Until 'wakenUp' is set to false again in the next round,
                // 'wakenUp.compareAndSet(false, true)' will fail, and therefore
                // any attempt to wake up the Selector will fail, too, causing
                // the following 'selector.select(...)' call to block
                // unnecessarily.
                //
                // To fix this problem, we wake up the selector again if wakenUp
                // is true immediately after selector.select(...).
                // It is inefficient in that it wakes up the selector for both
                // the first case (BAD - wake-up required) and the second case
                // (OK - no wake-up required).

                if (wakenUp.get()) {
                    wakenupFromLoop = true;
                    selector.wakeup();
                } else {
                    wakenupFromLoop = false;
                }

                cancelledKeys = 0;
                processTaskQueue();
                selector = this.selector; // processTaskQueue() can call rebuildSelector()

                if (shutdown) {
                    this.selector = null;

                    // process one time again
                    processTaskQueue();

                    for (SelectionKey k: selector.keys()) {
                        close(k);
                    }

                    try {
                        selector.close();
                    } catch (IOException e) {
                        logger.warn(
                                "Failed to close a selector.", e);
                    }
                    shutdownLatch.countDown();
                    break;
                } else {
                    process(selector);
                }
            } catch (Throwable t) {
                logger.warn(
                        "Unexpected exception in the selector loop.", t);

                // Prevent possible consecutive immediate failures that lead to
                // excessive CPU consumption.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // Ignore.
                }
            }
        }
    }

看这个方法,第一个映入眼帘的就是一个大的for (;;){...}死循环,难道是这里的问题?看着这里最符合我们遇到的现象,cpu占用过高。但是又感觉不太可能,因为这个是tcpserver,一般都是这么实现的,在一个死循环里,处理发生的事件。详细看循环里面的代码,里面有个select(selector)方法,这个方式会最终调用jdk的Selector.select(long timeout)方法,查看jdk源码,看方法注释,这个方法会阻塞的,所以理论上除非有事件就绪或者超时,否则该方法会阻塞,整个循环是不会占用太多CPU资源的。继续往下看,看到引用org.jboss.netty.epollBugWorkaround的地方,发现这个地方,做了一个判断,看注释可以发现这是jdk的一个bug,selector在某些情况下会不会阻塞,会立即返回,而且也没有就绪的时间,相当于在空转,所以会导致cpu过高,这段代码已经处理了这个问题,解决方法就是发生bug的时候新建一个selector,废弃旧的selector,但是这个属性如果不设置的话默认是false,这也解释了为什么项目上会遇到问题,要想解决这个问题,在启动的时候要加参数 -Dorg.jboss.netty.epollBugWorkaround=true。此参数是netty 3系列中的解决方案,在4中已经默认开启了此参数,所以4系列不需要做任何操作。这里有一个逻辑就是说程序怎么判断自身已经陷入了这个bug中呢,在netty3中是这样的,首先设置一个阈值,final long minSelectTimeout = SelectorUtil.SELECT_TIMEOUT_NANOS * 80 / 100;其中SelectorUtil.SELECT_TIMEOUT_NANOS是每次调用Selector.select(long timeout)的参数,也就是超时时间,minSelectTimeout用来衡量是否触发了这个bug,程序在每次select(long timeout)前后做了计时,只要这个时间小于minSelectTimeout,且返回就绪事件数是0,就认为触发了一次bug,selectReturnsImmediately用来记录触发bug的次数,如果这个次数达到了1024次,就进行重建selector,绕开这个bug。

结论

经过查询,发现两个先关的jdkbug