1.4 NIO的问题和Netty是怎么解决的
本章,我们将看Java NIO的一些问题和限制,以及Netty是怎样解决这些问题的。JDK拥有NIO包是朝着正确的方向迈出的一大步,但是用户却受限于如何使用它们。这些问题经常是由于过去所做的设计选择的结果,并且不容易改变,其他的是由于缺陷。
1.4.1 跨平台和兼容性问题
NIO的级别比较低,并且依赖于操作系统如何处理IO。所以完全满足Java提供统一API的需求,在所有的OS中工作和行为都是相同的,这不是微不足道的。
例如,你经常发现用NIO在Linux上工作的很好,但在Windows上面有问题。作者建议,即使你不使用NIO,但在不同的系统上做测试是非常重要的。即使在你工作的linux环境通过了测试,需要确保在其他的操作系统上验证。如果你不验证的话,或许会发生一些有趣的事情...
NIO.2看起来或许很理想,它仅仅支持Java7。另外,在撰写本文时,没有用于数据报管道的NIO.2 API(用于UDP应用程序),因此其用法仅限于TCP应用程序。
Netty通过提供统一的API的方式解决了这个问题,它允许相同的语义在Java6或7中无缝地工作。你不用担心底层版本,你将从相似或一致的API中获益。
1.4.2 扩展ByteBuffer...或者不
如前所述,ByteBuffer被用来当作数据容器。不幸的是,JDK 不包含允许包装ByteBuffer数组的ByteBuffer实现。最对于实现最小化内存复制是很有用的。如果你想:我将自己实现,不浪费你的时间。ByteBuffer有一个私有的构造器,所以你是不能够扩展它的。
Netty提供了它自己的ByteBuffer实现,它绕过了这个限制,并通过提供其他几种构建、使用和操作的方法以更进一步使用更简单的API来处理ByteBuffer。
1.4.3 散射和聚集或许会导致泄露
许多管道的实现支持散射和聚集,这个功能允许同时读/写更多的ByteBuffer实例,并拥有更好的性能。往往是由OS内核处理如何读/写,它经常会有更高的性能。因为OS内核更接近硬件,知道怎么做是最高效的。
如果你想在不同的ByteBuffer实例中分割数据,并分别处理每个缓冲区,散射和聚集会经常被用到。例如,在一个ByteBuffer中含有Header,另一个含有body。
图1.4展示了一个散射读是如何执行的。你可以将一个ByteBuffer实例数组传递给ScatteringByteChannel,然后数据将会从管道中分散到缓冲数组中。

聚集写也是相似的方式,但是数据是写到管道中。将ByteBuffer实例数组传递到GatheringByteChannel.write()方法,数据从buffers聚集到管道。
图1.5说明了聚集写的执行过程

不幸的是,这个功能在Java中被破坏,直到Java6的更新和Java7才正常。在之前,它会导致内存泄露,最终造成OutOfMemoryError。当你使用散射和聚集的时候,你需要很小心,确保使用的是正确的版本。
你或许会问:“为什么不更新Java?“,我同意这可以解决问题,但实际上,更新通常是不可行的。因为你的公司可能会对所有系统上部署的版本有所限制。改变的代价很高,事实上,这是不可行的。
1.4.4 粉碎著名的Epoll的bug
在类似Linux的系统中,选择器使用的是IO事件通知设备-epoll。这是一项高性能技术,其中操作系统和网络堆栈异步工作。不幸的是,直到今天,著名的Epoll bug还是会在选择器中导致一个无效的状态,结果会导致100%的CPU占用和运转(spinning)。恢复的唯一方法是回收旧的选择器,并且将之前已经注册的管道实例转到新创建的选择器中。
这里发生的是,即使没有已经被选择的SelectionKey实例, Selector.select() 也会停止阻塞,并立即返回。这违反了定义,在Javadoc中,Selector.select()方法是这样描述的:Selector.select() 如果没有已选择的,则必须阻塞。
NOTE For more details on the problem, see https://github.com/netty/netty/issues/327.
解决这个Epoll问题的范围是有限的。但Netty试图去自动检测和预防它。下面是Epoll的例子。
这段代码的效果是 循环吃内存:
... while (true) { } ...
值一直不是false,代码将保持CPU的运转并且吃资源。这会造成一些不良的后果,它会消耗所有的CPU,并阻止其他CPU的工作。
这些只是你在非阻塞IO中看到的一部分问题。不幸的是,尽管这个领域已经发展了多年,问题仍然需要解决。幸运的是,Netty解决了它们!
最后更新于
这有帮助吗?