You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 KiB

11 | 如何实现高性能的异步网络传输?

你好我是李玥。上一节课我们学习了异步的线程模型异步与同步模型最大的区别是同步模型会阻塞线程等待资源而异步模型不会阻塞线程它是等资源准备好后再通知业务代码来完成后续的资源处理逻辑。这种异步设计的方法可以很好地解决IO等待的问题。

我们开发的绝大多数业务系统都是IO密集型系统。跟IO密集型系统相对的另一种系统叫计算密集型系统。通过这两种系统的名字估计你也能大概猜出来IO密集型系统是什么意思。

IO密集型系统大部分时间都在执行IO操作这个IO操作主要包括网络IO和磁盘IO以及与计算机连接的一些外围设备的访问。与之相对的计算密集型系统大部分时间都是在使用CPU执行计算操作。我们开发的业务系统很少有非常耗时的计算更多的是网络收发数据读写磁盘和数据库这些IO操作。这样的系统基本上都是IO密集型系统特别适合使用异步的设计来提升系统性能。

应用程序最常使用的IO资源主要包括磁盘IO和网络IO。由于现在的SSD的速度越来越快对于本地磁盘的读写异步的意义越来越小。所以使用异步设计的方法来提升IO性能我们更加需要关注的问题是如何来实现高性能的异步网络传输。

今天,咱们就来聊一聊这个话题。

理想的异步网络框架应该是什么样的?

在我们开发的程序中如果要实现通过网络来传输数据需要用到开发语言提供的网络通信类库。大部分语言提供的网络通信基础类库都是同步的。一个TCP连接建立后用户代码会获得一个用于收发数据的通道每个通道会在内存中开辟两片区域用于收发数据的缓存。

发送数据的过程比较简单,我们直接往这个通道里面来写入数据就可以了。用户代码在发送时写入的数据会暂存在缓存中,然后操作系统会通过网卡,把发送缓存中的数据传输到对端的服务器上。

只要这个缓存不满,或者说,我们发送数据的速度没有超过网卡传输速度的上限,那这个发送数据的操作耗时,只不过是一次内存写入的时间,这个时间是非常快的。所以,发送数据的时候同步发送就可以了,没有必要异步。

比较麻烦的是接收数据。对于数据的接收方来说,它并不知道什么时候会收到数据。那我们能直接想到的方法就是,用一个线程阻塞在那儿等着数据,当有数据到来的时候,操作系统会先把数据写入接收缓存,然后给接收数据的线程发一个通知,线程收到通知后结束等待,开始读取数据。处理完这一批数据后,继续阻塞等待下一批数据到来,这样周而复始地处理收到的数据。


这就是同步网络IO的模型。同步网络IO模型在处理少量连接的时候是没有问题的。但是如果要同时处理非常多的连接同步的网络IO模型就有点儿力不从心了。

因为每个连接都需要阻塞一个线程来等待数据大量的连接数就会需要相同数量的数据接收线程。当这些TCP连接都在进行数据收发的时候会导致什么情况呢会有大量的线程来抢占CPU时间造成频繁的CPU上下文切换导致CPU的负载升高整个系统的性能就会比较慢。

所以我们需要使用异步的模型来解决网络IO问题。怎么解决呢

你可以先抛开你知道的各种语言的异步类库和各种异步的网络IO框架想一想对于业务开发者来说一个好的异步网络框架它的API应该是什么样的呢

我们希望达到的效果,无非就是,只用少量的线程就能处理大量的连接,有数据到来的时候能第一时间处理就可以了。

对于开发者来说最简单的方式就是事先定义好收到数据后的处理逻辑把这个处理逻辑作为一个回调方法在连接建立前就通过框架提供的API设置好。当收到数据的时候由框架自动来执行这个回调方法就好了。

实际上,有没有这么简单的框架呢?

使用Netty来实现异步网络通信

在Java中大名鼎鼎的Netty框架的API设计就是这样的。接下来我们看一下如何使用Netty实现异步接收数据。

// 创建一组线性
EventLoopGroup group = new NioEventLoopGroup();

try{
    // 初始化Server
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    serverBootstrap.group(group);
    serverBootstrap.channel(NioServerSocketChannel.class);
    serverBootstrap.localAddress(new InetSocketAddress("localhost", 9999));

    // 设置收到数据后的处理的Handler
    serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            socketChannel.pipeline().addLast(new MyHandler());
        }
    });
    // 绑定端口,开始提供服务
    ChannelFuture channelFuture = serverBootstrap.bind().sync();
    channelFuture.channel().closeFuture().sync();
} catch(Exception e){
    e.printStackTrace();
} finally {
    group.shutdownGracefully().sync();
}

这段代码它的功能非常简单就是在本地9999端口启动了一个Socket Server来接收数据。我带你一起来看一下这段代码

  1. 首先我们创建了一个EventLoopGroup对象命名为group这个group对象你可以简单把它理解为一组线程。这组线程的作用就是来执行收发数据的业务逻辑。
  2. 然后使用Netty提供的ServerBootstrap来初始化一个Socket Server绑定到本地9999端口上。
  3. 在真正启动服务之前我们给serverBootstrap传入了一个MyHandler对象这个MyHandler是我们自己来实现的一个类它需要继承Netty提供的一个抽象类ChannelInboundHandlerAdapter在这个MyHandler里面我们可以定义收到数据后的处理逻辑。这个设置Handler的过程就是我刚刚讲的预先来定义回调方法的过程。
  4. 最后就可以真正绑定本地端口启动Socket服务了。

服务启动后如果有客户端来请求连接Netty会自动接受并创建一个Socket连接。你可以看到我们的代码中并没有像一些同步网络框架中那样需要用户调用Accept()方法来接受创建连接的情况在Netty中这个过程是自动的。

当收到来自客户端的数据后Netty就会在我们第一行提供的EventLoopGroup对象中获取一个IO线程在这个IO线程中调用接收数据的回调方法来执行接收数据的业务逻辑在这个例子中就是我们传入的MyHandler中的方法。

Netty本身它是一个全异步的设计我们上节课刚刚讲过异步设计会带来额外的复杂度所以这个例子的代码看起来会比较多比较复杂。但是你看其实它提供了一组非常友好API。

真正需要业务代码来实现的就两个部分一个是把服务初始化并启动起来还有就是实现收发消息的业务逻辑MyHandler。而像线程控制、缓存管理、连接管理这些异步网络IO中通用的、比较复杂的问题Netty已经自动帮你处理好了有没有感觉很贴心所以非常多的开源项目使用Netty作为其底层的网络IO框架并不是没有原因的。

在这种设计中Netty自己维护一组线程来执行数据收发的业务逻辑。如果说你的业务需要更灵活的实现自己来维护收发数据的线程可以选择更加底层的Java NIO。其实Netty也是基于NIO来实现的。

使用NIO来实现异步网络通信

在Java的NIO中它提供了一个Selector对象来解决一个线程在多个网络连接上的多路复用问题。什么意思呢在NIO中每个已经建立好的连接用一个Channel对象来表示。我们希望能实现在一个线程里接收来自多个Channel的数据。也就是说这些Channel中任何一个Channel收到数据后第一时间能在同一个线程里面来处理。

我们可以想一下一个线程对应多个Channel有可能会出现这两种情况

  1. 线程在忙着处理收到的数据这时候Channel中又收到了新数据
  2. 线程闲着没事儿干所有的Channel中都没收到数据也不能确定哪个Channel会在什么时候收到数据。

Selecor通过一种类似于事件的机制来解决这个问题。首先你需要把你的连接也就是Channel绑定到Selector上然后你可以在接收数据的线程来调用Selector.select()方法来等待数据到来。这个select方法是一个阻塞方法这个线程会一直卡在这儿直到这些Channel中的任意一个有数据到来就会结束等待返回数据。它的返回值是一个迭代器你可以从这个迭代器里面获取所有Channel收到的数据然后来执行你的数据接收的业务逻辑。

你可以选择直接在这个线程里面来执行接收数据的业务逻辑,也可以将任务分发给其他的线程来执行,如何选择完全可以由你的代码来控制。

小结

传统的同步网络IO一般采用的都是一个线程对应一个Channel接收数据很难支持高并发和高吞吐量。这个时候我们需要使用异步的网络IO框架来解决问题。

然后我讲了Netty和NIO这两种异步网络框架的API和它们的使用方法。这里面你需要体会一下这两种框架在API设计方面的差异。Netty自动地解决了线程控制、缓存管理、连接管理这些问题用户只需要实现对应的Handler来处理收到的数据即可。而NIO是更加底层的API它提供了Selector机制用单个线程同时管理多个连接解决了多路复用这个异步网络通信的核心问题。

思考题

刚刚我们提到过Netty本身就是基于NIO的API来实现的。课后你可以想一下针对接收数据这个流程Netty它是如何用NIO来实现的呢欢迎在留言区与我分享讨论。

感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。