编辑推荐:
本文来自于infoq,文章深入分析了grpc线程模型以及源码,结合netty分析了grpc的源码结构,最后给出了减少竞争的锁优化方案。
1. RPC线程模型
1.1. BIO线程模型
在JDK 1.4推出Java NIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。因此,在很长一段时间里,大型的应用服务器都采用C或者C++语言开发,因为它们可以直接使用操作系统提供的异步I/O或者AIO能力。当并发访问量增大、响应时间延迟增大之后,采用Java
BIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低时延,它极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环,传统采用BIO的Java
Web服务器如下所示(典型的如Tomcat的BIO模式):
图1-1 基于BIO线程模型的Java Web服务器
采用该线程模型的服务器调度特点如下:
1.服务端监听线程Acceptor负责客户端连接的接入,每当有新的客户端接入,就会创建一个新的I/O线程负责处理Socket
2.客户端请求消息的读取和应答的发送,都有I/O线程负责
3.除了I/O读写操作,默认情况下业务的逻辑处理,例如DB操作等,也都在I/O线程处理
4.I/O操作采用同步阻塞操作,读写没有完成,I/O线程会同步阻塞
BIO线程模型主要存在如下三个问题:
1.性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制
2.可靠性问题:由于I/O操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致I/O线程被挂住,阻塞时间无法预测
3.可维护性问题:I/O线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,通常会对它的线程模型进行优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽,它的工作原理如下所示:
图1-2 改进版BIO线程模型
优化之后的BIO模型采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。本质上来讲,无法保证生产环境的网络状况和对端的应用程序能足够快,如果应用程序依赖对方的处理速度,它的可靠性就非常差,优化之后的BIO线程模型仍然无法从根本上解决性能线性扩展问题。
1.2. 异步非阻塞线程模型
从JDK1.0到JDK1.3,Java的I/O类库都非常原始,很多UNIX网络编程中的概念或者接口在I/O类库中都没有体现,例如Pipe、Channel、Buffer和Selector等。2002年发布JDK1.4时,NIO以JSR-51的身份正式随JDK发布。它新增了个java.nio包,提供了很多进行异步I/O开发的API和类库,主要的类和接口如下:
1.进行异步I/O操作的缓冲区ByteBuffer等
2.进行异步I/O操作的管道Pipe
3.进行各种I/O操作(异步或者同步)的Channel,包括ServerSocketChannel和SocketChannel
4.多种字符集的编码能力和解码能力
5.实现非阻塞I/O操作的多路复用器selector
6.基于流行的Perl实现的正则表达式类库
7.文件通道FileChannel
新的NIO类库的提供,极大地促进了基于Java的异步非阻塞编程的发展和应用,也诞生了很多优秀的Java
NIO框架,例如Apache的Mina、以及当前非常流行的Netty。
Java NIO类库的工作原理如下所示:
图1-3 Java NIO类库工作原理
在Java NIO类库中,最重要的就是多路复用器Selector,它是Java NIO编程的基础,熟练地掌握Selector对于掌握NIO编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。通常一个I/O线程会聚合一个Selector,一个Selector可以同时注册N个Channel,这样单个I/O线程就可以同时并发处理多个客户端连接。另外,由于I/O操作是非阻塞的,因此也不会受限于网络速度和对方端点的处理时延,可靠性和效率都得到了很大提升。
典型的NIO线程模型(Reactor模式)如下所示:
图1-4 Java NIO线程模型示例(Reactor模式)
1.3. RPC性能三原则
影响RPC框架性能的三个核心要素如下:
1.I/O模型:用什么样的通道将数据发送给对方,BIO、NIO或者AIO,IO模型在很大程度上决定了框架的性能
2.协议:采用什么样的通信协议,Rest+ JSON或者基于TCP的私有二进制协议。协议的选择不同,性能模型也不同。相比于公有协议,内部私有二进制协议的性能通常可以被设计的更优
3.线程:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,通信线