前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >再聊服务化基石:也有IO的事儿

再聊服务化基石:也有IO的事儿

作者头像
用户1682855
发布2018-06-08 11:12:51
4280
发布2018-06-08 11:12:51
举报
文章被收录于专栏:前沿技墅前沿技墅

IO的阻塞与同步

IO即输入/输出(Input/Output)。每个应用系统都少不了交互,或多或少都会产生数据,而它们的核心:IO,其性能的发展明显落后于 CPU 。对于高性能、高并发的应用系统来说,回避IO瓶颈进而提升性能是至关重要的。

阻塞与非阻塞

一般来说,IO模型可以分为阻塞/非阻塞及同步/异步。先从简单的阻塞/非阻塞模型说起。

  • 阻塞IO:用户进程发起IO操作后,必须等待IO操作完成才能继续运行。通信协议中的 Socket 编程,为了简单起见,也使用的这种方式。但这种方式会造成CPU大量闲置,系统吞吐量很低。虽然可以优化为一个 Socket 使用一个独立的线程,但仍会造成线程膨胀。
  • 非阻塞IO:用户进程发起IO操作后,可以不必等待IO操作完成,即可继续做其他事情。但用户进程需要定期询问IO操作是否就绪,可以使用一个线程监听所有的 Socket ,从而极大减少线程数量。对于IO与CPU密集程度的适度操作将会极大提升系统吞吐量,但用户进程不停轮询会略微增加额外的CPU资源浪费。

因此,阻塞IO与非阻塞IO的本质是,程序是否在等待调用结果。

同步IO与异步IO

操作系统的IO远比上文所述复杂。

Linux 内核会将所有的外部设备当作一个文件来操作,与外部设备的交互均可等同于对文件进行操作,Linux对文件的读写全是通过内核提供的系统调用。Linux内核使用 file descriptor 处理对本地文件的读写,同理,Linux内核使用socket file descriptor处理与Socket相关的网络读写,应用程序对文件的读写则通过对描述符的读写完成。

IO涉及两个系统对象,一个是调用它的用户进程,另一个是kernel,即系统内核。一次读取操作会分为以下几个步骤进行。

  1. 用户进程调用read方法向内核发起读请求并等待就绪。
  2. 内核将要读取的数据复制到文件描述符所指向的内核缓存区中。
  3. 内核将数据从内核缓存区复制至用户进程空间。

同步/异步与阻塞/非阻塞是不同的。同步 IO 和异步IO关注的是内核,而阻塞IO与非阻塞IO对应的则是调用它的函数。

阻塞/非阻塞的关注点在于——如果,系统内核中的数据还未准备完成时,用户进程是继续等待至准备完成,还是直接返回并先处理其他事情。

当系统内核将处理数据操作准备完毕后,需要等待内核将数据复制到用户进程后再进行处理,称为同步IO;而用户进程无须关心实际IO的操作过程,只需由内核在IO完成后通知既可,由内核进程来执行最终的IO操作,这种方式称为异步IO。

select、poll、epoll是Linux系统中使用最多的IO多路复用机制。IO多路复用可以监视多个描述符,一旦某个描述符读写操作就绪,便可以通知程序进行相应的读写操作。尽管实现方式不同,但 select、poll、epoll 都属于同步IO,它们都需要在读写事件就绪后自己进行读写操作,内核向用户进程复制数据的过程仍然是阻塞的,而异步IO则无须自己负责读写,它的实现将负责把数据从内核复制到用户空间。

总结一下,同步IO与异步IO的本质区别是,内核数据复制到用户空间时是否进行等待。

Java中的IO

Java对于IO的封装分为BIO、NIO和AIO,除异步阻塞IO之外,其他三种IO模型的组合与之一一对应。由于Java的IO接口相对面向底层,上手的难度不低,因此衍生出不少第三方IO处理框架,如Netty、mina等。

先看一下Java的IO原生处理框架。

BIO

Java中的BIO对应的是同步阻塞IO。它是JDK1.4版本以前的唯一选择,程序直观、简单、易理解,但对服务器资源要求比较高,而且性能较差。在当前具备很多替代方案的前提下,已不建议大规模使用,仅适用于连接数少且并发不高的场景。

BIO的服务器实现模式为一个连接分配一个线程。客户端有连接请求时,服务器端就启动一个线程进行处理。它缺乏弹性伸缩能力,服务端的线程个数和客户端并发访问数呈正比,随着访问量的增加会迅速导致线程数量膨胀,最终导致系统性能的急剧下降。可以通过合理使用线程池的方案改进“一连接一线程”模型,实现一个线程处理多个客户端的模型,如下图所示。

NIO

JDK 1.4中的 java.nio.*包中引入了全新的Java IO类库,与之相对应的是同步非阻塞IO。相比于 BIO,NIO 在性能上实现了质的提升,适用于连接数目多且连接较短的轻量级操作架构,非常适用于后端应用系统间的调用。对于目前互联网高负载和高并发的场景,NIO有极大的用武之地。它的美中不足是编程模型比较复杂,实现一个健壮的框架并非易事。

在使用NIO之前,需要理解以下的基础知识。

  • Buffer:Buffer是包含需要读取或写入的数据的缓冲区。NIO中所有数据读写均通过缓冲区来操作。常用的Buffer实现类有ByteBuffe、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer 、CharBuffer等。
  • Channel:Channel是数据读写的通道。与Stream不同,它是双向的,可以用于读和写的同时操作。其中对应文件操作的管道是 FileChannel,对应网络操作的管道则是 SelectableChannel。与 BIO模型中ServerSocket和Socket相对应的是NIO模型中的ServerSocketChannel和SocketChannel。它们都是 SelectableChannel的实现类,这两种通道同时支持阻塞和非阻塞模式,在NIO中更加推荐使用非阻塞模式。
  • Selector:Selector通过不断地轮询注册在其上的Channel来达到选择已经就绪的任务的目的。它可以同时轮询多个Channel,一个Selector即使接入成千上万个客户端,也不会出现明显的性能瓶颈。Selector是整个NIO的核心,理解 Selector 机制是理解NIO的关键。当 Selector 发现某个Channel有数据状态变化时,会通过 SelectorKey 触发相关事件,并由对此事件感兴趣的应用实现相关的事件处理器。

理解和学会使用 Selector,是 NIO的关键。NIO 通过非阻塞IO的编程模型,虽然在代码的编写难度方面有所增加,但对应用的性能提升却有质的帮助。因此,直到现在,使用Java原生接口编写网络通信程序时,NIO 仍然是使用最多的。

AIO

随着Java7的推出,NIO.2也进入了人们的视野。NIO.2虽然在2003年的 JSR 203就已被提出,但直到2011年才于 JDK 7中实现并一同发布。NIO.2提供了更多的文件系统操作API以及文件的异步IO操作,即AIO。

由于每个操作系统 AIO 对应的实现方式都不同,因此Java做了封装。Linux系统中2.6内核及其以上版本对应的是epoll,低版本仍然对应poll。Windows系统也有相应的 IOCP 的系统级支持。由于Java的服务端程序很少将Windows系统作为生产服务器,因此Linux系统的IO模型更加受到关注。

我们看到,在Linux系统上,Java实际并未真正使用异步IO,而是非阻塞IO。AIO虽然封装效果更好,模拟成为了异步IO的样子,但其本质仍然是poll或epoll这样的同步IO。

使用AIO有两种方式,一种是较为简单的将来式,另一种是使用较为复杂的回调式。

将来式即使用java.util.concurrent.Future对结果进行访问。在提交一个IO请求之后即返回一个Future对象,通过检查Future的状态可以得知操作完成还是失败,或者在进行中,然后调用 Future 的 get 阻塞当前进程或获取消息。但由于 Future的 get方法是同步并阻塞的,与完全同步的编程模式无异,导致异步操作仅作为摆设,因此并不推荐使用这种方法。

回调式是 AIO 推荐使用的首选方式。对此,NIO.2 提供 java.nio.channels.CompletionHandler 作为回调接口,该接口定义了 completed 和 failed 方法,用于让应用开发者自行覆盖并实现业务逻辑。当 I/O 操作结束后,系统将会调用CompletionHandler的completed或failed方法来结束一次调用。

AIO虽然在编程接口上比 NIO 更加简单,但是由于其使用的系统级别的IO模型与NIO是一样的,因此两者在性能方面并没有明显差距。由于AIO出现的时间较晚,而且并无性能提升,因此没有想象中那样普及。

Netty

虽然AIO的出现简化了NIO的开发,但是使用AIO的应用其实并不多。主要是由于Java语言本身的发展远远落后其丰富的第三方开源产品。

AIO 不但没有成为主流的网络通信应用开发利器,而恰相反。在 AIO 没有出现时,由于NIO的API过于底层,导致编写一个健壮的网络通信程序十分复杂,因此出现了一系列第三方通信框架,Mina和Netty就是其中的佼佼者。发展至今,Netty 由于优雅的编程模型以及健壮的异常处理方式,渐渐成为网络通信应用开发的首选框架。

Netty是由JBOSS提供的一个Java开源框架,是一个由事件驱动的异步高性能网络应用开发框架。Netty 基于Java NIO,在 AIO 出现后,Netty也进行了尝试,但由于AIO的性能并未有本质提升,因此Netty在其4.0的其中一个版本中将AIO移除。

如上图所示,Netty分为核心、传输和协议三个模块。

核心模块提供了性能极高的零拷贝能力。前文提到IO是需要将数据从系统内核复制到用户进程中,再进行下一步操作的。所谓的零拷贝是指无须为数据在内存之间的复制消耗资源,即不需要将数据内容复制到用户空间,而直接在内核空间中传输至网络,从而提升系统的整体性能。Linux的sendfile函数实现了零拷贝的能力,而使用 Linux 函数的Java NIO同样也通过其 FileChannel 的transfer方法实现了该功能。Netty同样通过封装NIO实现了零拷贝功能。而且 Netty 还提供了各种便利的缓冲区对象,在操作系统层面之外的Java应用层面进行数据操作优化已达到更优的效果。

Netty的核心模块还提供了统一的通信API和可高度扩展的事件驱动模型。

传输模块和协议模块是Netty的有力补充。传输模块支持TCP和UDP等Socket通信,以及HTTP和同一JVM内的通信通道。

Netty分离了业务处理,序列化/反序列化与服务端主进程的耦合,使得代码更加清晰易懂,并且以非常简单优雅的方式提供了异步处理的框架。Netty 的出现极大简化了 NIO 的开发,对于非遗留代码,建议使用 Netty 构建网络程序。

本文摘自博文视点2018年开年大戏——张亮作品:《云原生分布式中间件架构》,深度揭秘开源产品Elastic-Job&Sharding-JDBC! 张亮 当当架构部负责人。主要负责分布式中间件及私有云平台的搭建。乐于分享,拥抱开源,主导两个自研项目Elastic-Job和Sharding-JDBC都已正式开源。擅长以Java为主的分布式架构以及以Mesos为主的云平台方向,推崇代码优雅化,对如何编写强表现力代码有深入研究。搜索并关注公众号“点亮架构”可追踪作者最新架构实践与感悟。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-11-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前沿技墅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • IO的阻塞与同步
    • Java中的IO
    相关产品与服务
    消息队列 TDMQ
    消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档