《Netty 权威指南》—— 伪异步IO编程

声明:本文是《Netty 权威指南》的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文。

为了解决同步阻塞IO面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。 下面,我们结合连接模型图和源码,对伪异步IO进行分析,看它是否能够解决同步阻塞IO面临的问题。

2.1.1.伪异步IO模型图

采用线程池和任务队列可以实现一种叫做伪异步的IO通信框架,它的模型图如下:



伪异步IO服务端通信模型(M:N)

当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。 下面的小节,我们依然采用时间服务器程序,将其改造成伪异步IO时间服务器,然后通过对代码进行分析,找出其弊端。

2.1.1.伪异步式IO创建的TimeServer源码分析

我们对服务端代码进行一些改造,源码如下:

01 public class TimeServer {
02  
03     /**
04      * @param args
05      * @throws IOException
06      */
07     public static void main(String[] args) throws IOException {
08     int port = 8080;
09     if (args != null && args.length > 0) {
10         try {
11         port = Integer.valueOf(args[0]);
12         } catch (NumberFormatException e) {
13         // 采用默认值
14         }
15     }
16     ServerSocket server = null;
17     try {
18         server = new ServerSocket(port);
19         System.out.println("The time server is start in port : " + port);
20         Socket socket = null;
21         TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(
22             50, 10000);// 创建IO任务线程池
23         while (true) {
24         socket = server.accept();
25         singleExecutor.execute(new TimeServerHandler(socket));
26         }
27     } finally {
28         if (server != null) {
29         System.out.println("The time server close");
30         server.close();
31         server = null;
32         }
33     }
34     }
35 }

伪异步IO的主函数代码发生了变化,我们首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接的时候,将请求Socket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。 伪异步IO的TimeServerHandlerExecutePool:

01 public class TimeServerHandlerExecutePool {
02  
03     private ExecutorService executor;
04  
05     public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) {
06     executor = new ThreadPoolExecutor(Runtime.getRuntime()
07         .availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS,
08         new ArrayBlockingQueue(queueSize));
09     }
10     public void execute(java.lang.Runnable task) {
11     executor.execute(task);
12     }
13 }

由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。 由于客户端代码并没有改变,因此,我们直接运行服务端和客户端,看执行结果: 服务端运行结果:



伪异步IO时间服务器服务端运行结果

客户端运行结果:



伪异步IO时间服务器客户端运行结果

伪异步IO通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。下个小节我们对伪异步IO进行深入分析,找到它的弊端,然后看看NIO是如何从根本上解决这个问题的。

2.1.1.伪异步IO弊端分析

要对伪异步IO的弊端进行深入分析,首先我们看两个JAVA同步IO的API说明,随后我们结合代码进行详细分析。

01 /**
02      * Reads some number of bytes from the input stream and stores them into
03      * the buffer array <code>b</code>. The number of bytes actually read is
04      * returned as an integer.  This method blocks until input data is
05      * available, end of file is detected, or an exception is thrown.
06      *
07      *
08  If the length of <code>b</code> is zero, then no bytes are read and
09      * <code>0</code> is returned; otherwise, there is an attempt to read at
10      * least one byte. If no byte is available because the stream is at the
11      * end of the file, the value <code>-1</code> is returned; otherwise, at
12      * least one byte is read and stored into <code>b</code>.
13      *
14      *
15  The first byte read is stored into element <code>b[0]</code>, the
16      * next one into <code>b[1]</code>, and so on. The number of bytes read is,
17      * at most, equal to the length of <code>b</code>. Let <i>k</i> be the
18      * number of bytes actually read; these bytes will be stored in elements
19      * <code>b[0]</code> through <code>b[</code><i>k</i><code>-1]</code>,
20      * leaving elements <code>b[</code><i>k</i><code>]</code> through
21      * <code>b[b.length-1]</code> unaffected.
22      *
23      * @param      b   the buffer into which the data is read.
24      * @return     the total number of bytes read into the buffer, or
25      *             <code>-1</code> if there is no more data because the end of
26      *             the stream has been reached.
27      * @exception  IOException  If the first byte cannot be read for any reason
28      * other than the end of the file, if the input stream has been closed, or
29      * if some other I/O error occurs.
30      * @exception  NullPointerException  if <code>b</code> is <code>null</code>.
31      */
32     public int read(byte b[]) throws IOException {
33         return read(b, 0, b.length);
34 }

请注意加粗斜体字部分的API说明,当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件: 1)   有数据可读 2)   可用数据已经读取完毕 3)   发生空指针或者IO异常 这意味着当对方发送请求或者应答消息比较缓慢、或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方60S才能够将数据发送完成,读取一方的IO线程也将会被同步阻塞60S,在此期间,其它接入消息只能在消息队列中排队。 下面我们接着对输出流进行分析,还是看JDK IO类库输出流的API文档,然后结合文档说明进行故障分析。 Java 输入流OutputStream:

1 public void write(byte b[]) throws IOException
2 *Writes an array of bytes. This method will block until the bytes are *actually written.
3 Parameters:
4 b - the data to be written
5 Throws: IOException
6 If an I/O error has occurred.

当调用OutputStream的write方法写输出流的时候,它将会被阻塞直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的都知道,当消息的接收方处理缓慢的时候,将不能及时的从TCP缓冲区读取数据,这将会导致发送方的TCP window size不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞IO,write操作将会被无限期阻塞,直到TCP window size大于0或者发生IO异常。
通过对输入和输出流的API文档进行分析,我们了解到读和写操作都是同步阻塞的,阻塞的时间取决于对方IO线程的处理速度和网络IO的传输速度。本质上来讲,我们无法保证生产环境的网络状况和对端的应用程序能够足够快,如果我们的应用程序依赖对方的处理速度,它的可靠性就非常差。也许在实验室进行的性能测试结果令大家满意,但是一旦上线运行,面对恶劣的网络环境和良莠不齐的第三方系统,问题就会如火山一样喷发。
伪异步IO实际上仅仅只是对之前IO线程模型的一个简单优化,它无法从根本上解决同步IO导致的通信线程阻塞问题。下面我们就简单分析下如果通信对方返回应答时间过长引起的级联故障:

  1. 服务端处理缓慢,返回应答消息耗费60S,平时只需要10MS;
  2. 采用伪异步IO的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60S;
  3. 假如所有的可用线程都被故障服务器阻塞,那后续所有的IO消息都将在队列中排队;
  4. 由于线程池采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞;
  5. 由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时;
  6. 由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。

如何破解这个难题?下个章节的NIO将给出答案。 

时间: 2024-11-08 23:22:18

《Netty 权威指南》—— 伪异步IO编程的相关文章

《Netty 权威指南》—— 4种IO的对比

声明:本文是<Netty 权威指南>的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. 2.5.1.概念澄清 为了防止由于对一些技术概念和术语的理解或者叫法不一致引起歧义,本小节特意对本书中的专业术语或者技术用语做下声明,如果它们与其它的一些技术书籍术语不一致,请以本小节的解释为准. 2.5.1.1. 异步非阻塞IO 很多人喜欢将JDK1.4提供的NIO框架称为异步非阻塞IO,但是,如果严格按照Unix网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞IO,不能叫

《Netty权威指南》目录

<Netty权威指南>是全球第二本.中国第一本Netty教材,它由华为平台中间件资深架构设计师李林锋撰写,作者有6年多的NIO设计和开发实战经验,多次受邀进行Netty和 NIO编程培训. 本书基于最新的Netty5.0 版本撰写,从Netty开发环境的搭建,到第一个基于Netty的NIO服务端和客户端程序的开发,一步步的让读者从入门到精通,熟练的掌握基于Netty 的NIO开发,理解Netty的架构设计原理,可以对Netty进行深度的定制设计和开发. 本书共分为五部分:第一部分介绍 JAVA

《Netty 权威指南》样章

声明:本文是<Netty 权威指南>的样章目录,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. 第 2 章  NIO入门 在本章节,我们分别对JDK的BIO.NIO和JDK1.7最新提供的NIO2.0的使用进行详细说明,通过流程图和代码讲解,让大家体会到随着Java IO类库的不断发展和改进,基于Java的网络编程会变得越来越简单,随着异步IO功能的增强,基于Java NIO开发的网络服务器甚至不逊色于采用C++开发的网络程序. 本章主要内容包括:  传统的同步阻塞式IO编程

《Netty 权威指南》—— 传统的BIO编程

声明:本文是<Netty 权威指南>的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信. 在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口,Socket负责发起连接操

《Netty 权威指南》—— 选择Netty的理由

声明:本文是<Netty 权威指南>的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. 在开始本节之前,我先讲一个亲身经历的故事:曾经有两个项目组同时用到了NIO编程技术,一个项目组选择自己开发NIO服务端,直接使用JDK原生的API,结果2个多月过去了,他们的NIO服务端始终无法稳定,问题频出.由于NIO通信是它们的核心组件之一,因此,项目的进度受到了严重的影响,领导对此非常恼火.另一个项目组直接使用Netty作为NIO服务端,业务的定制开发工作量非常小,测试表明,功能和

《Netty 权威指南》—— NIO创建的TimeClient源码分析

声明:本文是<Netty 权威指南>的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. 我们首先还是看下如何对TimeClient进行改造: public class TimeClient { /** * @param args */ public static void main(String[] args) { int port = 8080; if (args != null && args.length > 0) { try { port =

《Netty 权威指南》—— AIO 创建的TimeServer源码分析

声明:本文是<Netty 权威指南>的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现.异步通道提供两种方式获取获取操作结果: 通过java.util.concurrent.Future类来表示异步操作的结果: 在执行异步操作的时候传入一个java.nio.channels. CompletionHandler接口的实现类作为操作完成的回调. NIO2.0的异步套接字通道是真正的异步非阻塞IO

《Netty 权威指南》—— AIO版本时间服务器运行结果

声明:本文是<Netty 权威指南>的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. 执行TimeServer,运行结果如下: AIO时间服务器服务端运行结果 执行TimeClient,运行结果如下: AIO时间服务器客户端运行结果 下面我们继续看下JDK异步回调CompletionHandler的线程执行堆栈: AIO时间服务器异步回调线程堆栈 通过"Thread-2"线程堆栈我们可以发现,JDK底层通过线程池ThreadPoolExecutor来执

《Netty 权威指南》—— AIO创建的TimeClient源码分析

声明:本文是<Netty 权威指南>的样章,感谢博文视点授权并发编程网站发布样章,禁止以任何形式转载此文. 异步非阻塞IO版本的时间服务器服务端已经介绍完毕,下面我们继续看客户端的实现. 首先看下客户端主函数的实现,AIO时间服务器客户端  TimeClient: 01 public class TimeClient { 02   03     /** 04      * @param args 05      */ 06     public static void main(String[