NIO中Epoll Bug产生的原因以及Netty中是如何解决的

https://blog.csdn.net/speakguoer/article/details/120132750

https://blog.csdn.net/qq_40751552/article/details/111715056

文章目录

产生原因

正常情况下,select()方法在没有事件时,程序一直阻塞在该方法,但是在没有任何事件的情况下,该方法被唤醒,但是在程序对seclectionKey进行遍历时,却没有任何key,导致程序一直在循环中不能执行下去,从而导致CPU100%。 具体解释为Linux kernel 2.6中的一部分,poll和epoll会对已经连接好后却突然中断连接的socket给eventSet返回一个集合,集合为POLLHUP,也可能为POLLERR,因为eventSet的事件集合发生了变化,导致Selector被唤醒。

Java官方解释

Bug ID: JDK-6670302 (se) NIO selector wakes up with 0 selected keys infinitely [lnx 2.4]

问题复现

环境:Centos7 (其实只要是Linux系统就可以) JDK版本:JDK6u3 (一定要是update3,后边版本对该问题修复,不太容易复现) 下载链接:Java Archive Downloads - Java SE 6 客户端代码

import java.io.*;
import java.net.*;
​
public class TestClient {
    private static final long SLEEP_PERIOD = 5000L; // 5 seconds
    private String host;
    private int port;
​
    public TestClient(String host, int port) {
        this.host = host;
        this.port = port;
    }
​
    public static void main(String[] args) throws Throwable {
        if (args.length < 2 || args[0].equals("127.0.0.1") || args[0].equals("localhost")) {
            System.err.println("Usage : java TestClient <host name> <port> (host name should not be localhost)");
            System.exit(0);
        }
​
        new TestClient(args[0], Integer.parseInt(args[1])).start();
    }
​
    public void start() throws Throwable {
        Socket socket = new Socket(host, port);
​
        BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(
                new OutputStreamWriter(socket.getOutputStream()),
                true /* auto flush */);
​
        out.println("abcdef");
​
        System.out.println("1. CLIENT CONNECTED AND WROTE MESSAGE");
​
        Thread.sleep(SLEEP_PERIOD);
​
//         socket.shutdownOutput();
        socket.close();
​
        System.out.println("4. CLIENT SHUTDOWN OUTPUT");
​
        Thread.sleep(SLEEP_PERIOD * 3);
    }
}

服务端代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
​
public class TestServer{
    private static final long SLEEP_PERIOD = 5000L; // 5 seconds
    private static final int BUFFER_SIZE = 8192;
    private int port;
​
    public TestServer(int port) {
        this.port = port;
    }
​
    public static void main(String[] args) throws Throwable {
        if (args.length < 1) {
            System.err.println("Usage : java TestServer <port>");
            System.exit(0);
        }
​
        new TestServer(Integer.parseInt(args[0])).start();
    }
​
    public void start() throws Throwable {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
​
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        ServerSocket server = serverChannel.socket();
        server.bind(new InetSocketAddress(port));
​
        Selector selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        SocketChannel clientChannel = null;
​
        System.out.println("0. SERVER STARTED TO LISTEN");
        boolean writeNow = false;
​
        while (true) {
            try {
                // wait for selection
                int numKeys = selector.select();
​
                if (numKeys == 0) {
                    System.err.println("select wakes up with zero!!!");
                }
​
                Iterator it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey selected = (SelectionKey) it.next();
                    int ops = selected.interestOps();
​
                    try {
                        // process new connection
                        if ((ops & SelectionKey.OP_ACCEPT) != 0) {
                            clientChannel = serverChannel.accept();
                            clientChannel.configureBlocking(false);
​
                            // register channel to selector
                            clientChannel.register(selector, SelectionKey.OP_READ, null);
                            System.out.println("2. SERVER ACCEPTED AND REGISTER READ OP : client - " + clientChannel.socket().getInetAddress());
                        }
​
                        if ((ops & SelectionKey.OP_READ) != 0) {
                            // read client message
                            System.out.println("3. SERVER READ DATA FROM client - " + clientChannel.socket().getInetAddress());
                            readClient((SocketChannel) selected.channel(), buffer);
​
                            // deregister OP_READ
                            System.out.println("PREV SET : " + selected.interestOps());
                            selected.interestOps(selected.interestOps() & ~SelectionKey.OP_READ);
                            System.out.println("NEW SET : " + selected.interestOps());
​
                            Thread.sleep(SLEEP_PERIOD * 2);
                            new WriterThread(clientChannel).start();
                        }
​
                    } finally {
                        // remove from selected key set
                        it.remove();
                    }
                }
            } catch (IOException e) {
                System.err.println("IO Error : " + e.getMessage());
            }
        }
    }
​
​
    public void readClient(SocketChannel channel, ByteBuffer buffer) throws IOException {
        try {
            buffer.clear();
​
            int nRead = channel.read(buffer);
​
            if (nRead < 0) {
                channel.close();
                return;
            }
​
            if (buffer.position() != 0) {
                int size = buffer.position();
                buffer.flip();
                byte[] bytes = new byte[size];
                buffer.get(bytes);
                System.out.println("RECVED : " + new String(bytes));
            }
        } catch (IOException e) {
            System.err.println("IO Error : " + e.getMessage());
            channel.close();
        }
    }
​
    static class WriterThread extends Thread {
        private SocketChannel clientChannel;
        public WriterThread(SocketChannel clientChannel) {
            this.clientChannel = clientChannel;
        }
​
        public void run() {
            try {
                writeClient(clientChannel);
                System.out.println("5. SERVER WRITE DATA TO client - " + clientChannel.socket().getInetAddress());
            } catch (IOException e) {
                System.err.println("5. SERVER WRITE DATA FAILED : " + e);
            }
        }
​
        public void writeClient(SocketChannel channel) throws IOException {
            try {
                ByteBuffer buffer = ByteBuffer.wrap("zwxydfdssdfsd".getBytes());
                int total = buffer.limit();
​
                int totalWrote = 0;
                int nWrote = 0;
​
                while ((nWrote = channel.write(buffer)) >= 0) {
                    totalWrote += nWrote;
                    if (totalWrote == total) {
                        break;
                    }
                }
            } catch (IOException e) {
                System.err.println("IO Error : " + e.getMessage());
                channel.close();
            }
        }
​
​
    }
}

操作步骤: 1、将上述代码使用前边下载好的jdk6u3打成jar包,可以用IDE工具也可以用命令行 命令行输入:jar cvf xxx.jar xxx,其中xxx表示项目目录名称 2、打好jar包后,在Linux系统中执行jar包 终端一启动服务端:java -classpath 包名.TestServer 77777 终端二启动客户端:java -classpath 包名.TestClient XXXXX 77777,其中XXXXX表示当前机器的IP 等待客户端退出,在客户端屏幕显示内容

在这里插入图片描述

在服务器终端显示以上内容,表示bug复现成功。

产生原因

正常情况下,selector.select()操作是阻塞的,只有被监听的fd有读写操作时,才被唤醒。但是,在这个bug中,没有任何fd有读写请求,但是select()操作依旧被唤醒很显然,这种情况下,selectedKeys()返回的是个空数组,然后按照逻辑执行到while(true)处,循环执行,导致死循环。

Netty的解决方法


对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数。
若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。
重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值