0%

高性能网络应用框架Netty

Netty框架功能丰富,主要分析Netty框架中的线程模型,线程模型直接影响着网络程序的性能。

网络编程性能的瓶颈

Thread-Per-Message中的网络程序echo,采用的是阻塞式I/O(BIO)。BIO模型里,所有read()操作和write()操作都会阻塞当前线程,如果客户端已经和服务端建立了一个连接,而迟迟不发送数据,那么服务端的read()操作会一直阻塞,所以使用BIO模型,一般都会为每个socket分配一个独立的线程,这样就不会因为线程阻塞在一个socket上而影响对其它socket的读写。

BIO的线程模型,每一个socket都对应一个独立的线程;为了避免频繁创建、消耗线程,可以采用线程池,但是socket和线程之间的关系并不会变化。

BIO这种线程模型适用于socket链接不是很多的场景;但是现在的互联网场景,往往需要服务器能够支撑十万甚至百万链接,而创建十万甚至上百万个线程显然并不现实,所以BIO线程模型无法解决百万连接的问题。
互联网场景中,虽然连接多,但是每个连接上的请求并不频繁,所以线程大部分时间都在等待I/O就绪。即线程大部分时间都阻塞在那里,这完全是浪费,能够解决这个问题,就不需要这么多线程了。

将线程模型优化为用一个线程来处理多个连接,这样线程的利用率就上来了,同时所需要的线程数量也跟着降下来了。

使用BIO相关的API无法实现,BIO相关的socket读写操作都是阻塞式的,而一旦调用了阻塞式API,在I/O就绪前,调用线程会一直阻塞,也就无法处理其它的socket连接了。

Java里提供了非阻塞式(NIO)API,利用非阻塞式API就能够实现一个线程处理多个连接了。具体实现方式普遍采用Reactor模式。

Reactor模式

Handle指的是I/O句柄,在Java网络编程里,它本质上就是一个网络连接。 Event Handler就是一个事件处理器,其中handle_event()方法处理I/O时间,也就是每个Event Handler处理一个I/O Handle;get_handle()方法可以返回这个I/O的Handle。
Synchronous Event Demultiplexer可以理解为操作系统提供的I/O多路复用API。(如Linux里的spoll())

Reactor模式的核心是Reactor类,其中register_handler()和remove_handler()这两个方法可以注册和删除一个事件处理器;handle_events()方法是核心,也是Reactor模式的发动机,这个方法的核心逻辑是:首先通过同步事件多路选择器提供的select()方法监听网络事件,当有网络事件就绪后,就遍历事件处理器来处理该网络事件。由于网络事件时源源不断的,所以在主程序中启动Reactor模式,需要以while(true){}的方式调用handle_events()方法。

1
2
3
4
5
6
7
8
9
10
11
12
void Reactor::handle_events() {
//通过同步事件多路选择器提供的select()方法监听网络事件
select(handlers);
//处理网络事件
for(h in handlers) {
h.handle_event();
}
}
//在主程序中启动时间循环
while(true) {
handle_events();
}

Netty中的线程模型

Netty的实现虽然参考了Reactor模式,但是并没有完全照搬,Netty中最核心的概念是事件循环(EventLoop),负责监听网络事件并调用事件处理器进行处理(也就是Reactor模式中的Reactor)。
在4.x版本的Netty中,网络连接和EventLoop是稳定的多对一关系,而EventLoop和Java线程是1对1关系,这里的稳定指的是关系一旦确定就不再发生变化。也就是说一个网络连接只会对应唯一的一个EventLoop,而一个EventLoop也只会对应到一个Java线程,所以一个网络连接只会对应到一个Java线程(多对一)。
一个网络连接对应到一个Java线程上,最大的好处就是对于一个网络连接的事件处理是单线程的,这样就避免了各种并发问题

Netty中还有一个核心概念是EventLoopGroup,一个EventLoopGroup由一组EventLoop组成。实际使用中一般都会创建两个EventLoopGroup,一个称为bossGroup,一个称为workerGroup。

创建两个EventLoopGroup的原因和socket处理网络请求的机制有关。socket处理TCP网络连接请求,是在一个独立的socket中,每当有一个TCP连接成功建立,都会创建一个新的socket,之后对TCP连接的读写都是由新创建的socket完成的。也就是说处理TCP连接请求和读写请求时通过两个不同的socket完成的。(上面讨论网络请求时,简化模型只讨论了读写请求,没有讨论连接请求)

在Netty中,bossGroup是用来处理连接请求的,workerGroup是用来处理读写请求的。bossGroup处理完连接请求后,会将这个连接提交给workerGroup来处理,workerGroup里面有多个EventLoop,新的连接具体交给哪个EventLoop来处理,是由负载均衡算法决定的(Netty中使用的是轮询算法)。

用Netty实现Echo程序服务端

首先创建一个事件处理器(等同于Reactor模式中的事件处理器),然后创建了bossGroup和workerGroup,再之后创建并初始化了ServerBootstrap。注意事项:

  • 如果NettybossGoup只监听一个端口,那bossGroup只需要1个EventLoop。
  • 默认情况下,Netty会创建2*CPU核数个EventLoop,由于网络连接与EventLoop有稳定的关系,所以事件处理器在处理网络事件的时候是不能有阻塞操作的,否则很容易导致请求大面积超时。如果实在无法避免使用阻塞操作,那可以通过线程池来异步处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//事件处理器
final EchoServerHandler serverHandler = new EchoServerHandler();
//boss线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//worker线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();

try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSockerChannel.class)
.childHandler(new ChannelInitializer<SockerChannel>() {
@Override
public void initChannel(SockerChannel ch) {
ch.pipeline().addLast(serverHandler);
}
});
//bind服务端端口
ChannelFuture f = b.bind(9090).sync();
f.channel().closeFuture().sync();
} finally {
//终止个线程组
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}

//socket连接处理器
class EchoServerHandler extends ChannelInboundHandlerAdapter {
//处理读事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg);
}
//处理读完成事件
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
//处理异常事件
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

总结

Netty是一款优秀的网络编程框架,性能非常好,为了实现高性能的目标,Netty做了很多优化:ByteBuffer、支持零拷贝等,和并发编程相关的就是它的线程模型。Netty的线程模型设计的很精巧,每个网络连接都关联到了一个线程上,这样做的好处是:对于一个网络连接,读写操作都是单线程执行的,避免了并发程序的各种问题。