从Kafka到NIO

在谈NIO之前,简单回顾下内核态和用户态

内核空间是Linux内核运行的空间,而用户空间是用户程序的运行空间,为了保证内核安全,它们之间是隔离的,即使用户的程序崩溃了,内核也不受影响。
内核空间可以执行任意命令,调用系统的一切资源,用户空间只能执行简单运算,不能直接调用系统资源(I/O,进程资源,内存分配,外设,计时器,网络通信等),必须通过系统接口(又称 system call),才能向内核发出指令。

linux-structure.png

用户进程通过系统调用访问系统资源的时候,需要切换到内核态,而这对应一些特殊的堆栈和内存环境,必须在系统调用前建立好。而在系统调用结束后,cpu会从内核态切回到用户态,而堆栈又必须恢复成用户进程的上下文。而这种切换就会有大量的耗时。

进程缓冲区

一般程序在读取文件的时候先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节长度的数据,写入buffer。(用较小的次数填满buffer)。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。这里的buffer我们称为用户缓冲区,它的目的是为了减少频繁I/O操作而引起频繁的系统调用,从而降低操作系统在用户态与核心态切换所耗费的时间。

内核缓冲区

除了在进程中设计缓冲区,内核也有自己的缓冲区。

当一个用户进程要从磁盘读取数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程缓冲区中。

但若是内核缓冲区中没有数据,内核会把对数据块的请求,加入到请求队列,然后把进程挂起,为其它进程提供服务。

等到数据已经读取到内核缓冲区时,把内核缓冲区中的数据读取到用户进程中,才会通知进程,当然不同的io模型,在调度和使用内核缓冲区的方式上有所不同。

你可以认为,read是把数据从内核缓冲区复制到进程缓冲区。write是把进程缓冲区复制到内核缓冲区。

当然,write并不一定导致内核的写动作,比如os可能会把内核缓冲区的数据积累到一定量后,再一次写入。这也就是为什么断电有时会导致数据丢失。

所以,我们进行IO操作的请求过程如下:用户进程发起请求(调用系统函数),内核接收到请求后(进程会从用户态切换到内核态),从I/O设备中获取数据到内核buffer中,再将内核buffer中的数据copy到用户进程的地址空间,该用户进程获取到数据后再响应客户端。

I/O复用模型

JavaNIO使用了I/O复用模型

IO_multi.png

从图中可以看出,我们阻塞在select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件的时候,我们调用recvfrom把所读数据从内核缓冲区复制到应用进程缓冲区。

那么内核态怎么判断I/O流可读可写?
内核针对读缓冲区和写缓冲区来判断是否可读可写

而java从1.5开始就使用epoll代替了之前的select,它对select有所增强,比较有特点的是epoll支持水平触发(epoll默认)和边缘触发两种方式。

epoll比select高效主要几种在两点,这个可以参考知乎的这个回答(https://www.zhihu.com/question/20122137/answer/146866418)

  1. 减少用户态和内核态之间的文件句柄拷贝
  2. 减少对可读可写文件句柄的遍历

epoll和NIO的操作方式对应图如下:


nio_compare_epoll.png
  1. epoll_ctl 注册事件
  2. epoll_wait 轮询所有的socket
  3. 处理对应的事件

epoll中比较有趣的是水平触发(LT)和边缘触发(ET)。

水平触发(条件触发):读缓冲区只要不为空,就一直会触发读事件;写缓冲区只要不满(发送得速度比写得速度快),就一直会触发写事件。这个比较符合编程习惯,也是epoll的缺省模式。

边缘触发(状态触发):读缓冲区的状态,从空转为非空的时候,触发1次;写缓冲区的状态,从满转为非满的时候,触发1次。比如你发送一个大文件,把写缓存区塞满了,之后缓存区可以写了,就会发生一次从满到不满的切换。

通过分析,我们可以看出:
对于LT模式,要避免"写的死循环"问题:写缓冲区为满的概率很小,也就是"写的条件"会一直满足,所以如果你注册了写事件,没有数据要写,但它会一直触发,所以在LT模式下,写完数据,一定要取消写事件。

对应ET模式,要避免"short read"问题:比如你收到100个字节,它触发1次,但你只读到了50个字节,剩下的50个字节不读,它也不会再次触发,此时这个socket就废了。因此在ET模式,一定要把"读缓冲区"的数据读完。

验证

代码太长了,我就只列出一段服务器端的主要代码,client端的比较简单,写法和server端也类似就不列出来了

Selector selector = Selector.open();

// 创建通道ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 将通道设置为非阻塞
serverSocketChannel.configureBlocking(false);

ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8989));

/**
* 将通道(Channel)注册到通道管理器(Selector),并为该通道注册selectionKey.OP_ACCEPT事件
* 注册该事件后,当事件到达的时候,selector.select()会返回,
* 如果事件没有到达selector.select()会一直阻塞。
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

// 循环处理
while (true) {
    // 当注册事件到达时,方法返回,否则该方法会一直阻塞
    selector.select();

    // 获取监听事件
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();

    // 迭代处理
    while (iterator.hasNext()) {
        // 获取事件
        SelectionKey key = iterator.next();

        // 移除事件,避免重复处理
        iterator.remove();

        // 客户端请求连接事件,接受客户端连接就绪
       if (key.isAcceptable()) {
           ServerSocketChannel server = (ServerSocketChannel) key.channel();
           SocketChannel socketChannel = server.accept();
           socketChannel.configureBlocking(false);
           // 给通道设置写事件,客户端监听到写事件后,进行读取操作
           socketChannel.register(selector, SelectionKey.OP_WRITE);
        } else if(key.isWritable()) {
            System.out.println("write");
            handleWrite(key);
        }
    }
}

当client连接上server的时候就会发现server一直收到写事件,write会一直打印。所以使用条件触发的API 时,如果应用程序不需要写就不要关注socket可写的事件,否则就会无限次的立即返回一个write ready通知。大家常用的select就是属于条件触发这一类,长期关注socket写事件会出现CPU 100%的毛病。所以在使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,在有数据往外写的时候再注册写事件。

取消写事件可以这样写 selectionKey.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);

Kafka中如何处理的

在上一篇对Kafka网络层的分析中,我们知道了它是通过NIO和服务端进行通信的。其中在KafkaChannel的send()方法里面有这样一段代码:

private boolean send(Send send) throws IOException {
  send.writeTo(transportLayer);
  if (send.completed())
    transportLayer.removeInterestOps(SelectionKey.OP_WRITE);

  return send.completed();
}

请注意这里的transportLayer.removeInterestOps(SelectionKey.OP_WRITE),它移除了注册的OP_WRITE事件。

既然取消了,肯定会添加。在发送数据之前KafkaChannel的setSend()方法里面又注册了OP_WRITE事件

public void setSend(Send send) {
  if (this.send != null)
      throw new IllegalStateException("Attempt to begin a send operation with prior send operation still in progress, connection id is " + id);
  this.send = send;
  this.transportLayer.addInterestOps(SelectionKey.OP_WRITE);
}

所以还是那句话: 在没有数据可以往外写的时候要取消写事件,在有数据往外写的时候再注册写事件。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • 参考 https://www.cnblogs.com/hapjin/p/5736188.html https://...
    jection阅读 4,068评论 3 32
  • 1. 硬链接和软连接区别 硬连接-------指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区...
    杰伦哎呦哎呦阅读 2,251评论 0 2
  • 作者: 一字马胡 转载标志 【2017-11-24】 更新日志 一、Java OIO Java OIO (Jav...
    一字马胡阅读 1,350评论 0 12
  • 必备的理论基础 1.操作系统作用: 隐藏丑陋复杂的硬件接口,提供良好的抽象接口。 管理调度进程,并将多个进程对硬件...
    drfung阅读 3,537评论 0 5
  • 去年今日, 刚经历高考的我, 第二天也是早早的醒来, 自知没发挥好,但也于事无补, 成绩出来虽有不甘,但也接受, ...
    遇见爱吃肉阅读 185评论 0 0