首先,在讲述高性能IO编程设计的时候,我们先思考一下何为“高性能”呢,如果自己来设计一个web体系服务,选择BIO还是NIO的编程方式呢?其次,我们可以了解下构建一个web体系服务中,为了能够支撑更多的并发连接数,一般会有两种web架构设计方案,即线程架构以及事件驱动设计,在Java的IO设计演进文章已对线程架构设计方案进行详细的阐述,本文主要以事件驱动设计具体实现技术展开讨论.
web体系设计
在epoll技术原理分析文章中讲到C10K的优化问题,需要从文件描述符限制,线程资源,内存资源,网络数据包大小传输等方面进行优化,目的是提升web服务的连接调度处理能力,支撑更多的客户端并发连接响应,因此高性能的IO设计意味着(实现IO高性能的目标)需要考虑以下几个方面,即:
为了达到web服务的高性能设计目标,我们需要考虑技术落地方案的选择,现有方案有基于'one thread one connection'的BIO以及'one thread one server'的NIO技术实现方式,其次,在这里需要声明一点就是BIO视为单线程的同步操作,NIO视为单线程的异步操作,同时我们也需要关注两种不同IO的实现在性能测试中的结果是如何的,才能有效地帮助我们实现高性能的目标,以下是摘录《 Thousands of Threads and Blocking I/O》的性能测试结果数据,现分析如下:
异步web与同步web的吞吐量
通过上述可知,在相同的操作系统环境下,同步web的IO吞吐量更高,主要包含以下方面:
编程方式
while(true){
// 调用select()
int rs = select();
// 如果rs没有对应的就绪事件个数,继续select()
if (rs <= 0){
continue;
}
// 获取可用的key
Set<Keys> keySet = selectKeys();
for(Key key: keySet){
if(key.isAcceptable()){
client = accept();
// register and save the key
client.register(...);
}else if(key.isReadable()){
// read();
// decode();
// process();
// encode();
// write();
}
}
}
while(true){
client = accept();
client.read();
// decode();
// process();
// encode();
// write();
}
通过上述可以看出,BIO是面向单连接处理的编程方式,调用accept以及read方法都需要进行等待就绪状态才能进行下一步操作,而NIO则是面向单线程处理多连接的编程方式(严格意义上是基于事件编程),通过轮询以及就绪事件的遍历来处理就绪事件,相比BIO在实现上会更为复杂些,然而对于实现高性能的IO设计,我们还需要借助多线程技术来实现,下面针对多线程的同步与异步方式进行对比与分析
多线程环境下同步与异步性能对比
通过上述可知,多线程环境下使用同一个类库进行测试的性能,1000个与1个线程执行的性能效率上相差不大,因此线程上下文切换的成本其实不高
通过上述可知,syncHashMap
与HashTable
随着增加的线程数,其执行的性能耗时更高,因为同步操作的hashtable
和syncHashMap
是在线程级别加锁实现顺序的写操作,因此需要等待其他线程执行完成才能被唤醒执行,对于具备“异步”特性的类库则是通过多线程并发方式对容器实现写操作,即同一个时刻可以有多个线程对容器实现写操作.
多核环境下的同步与异步性能对比
通过上述可知,具备‘异步‘的并发类库不论是在单核还是多核环境下性能基本差不多,但是对于实现同步hashtable
的性能在多核环境充分利用cpu核数提升性能,但是在上述我们注意到SyncHashMap执行的性能会更差,为什么?个人理解上述的map类库都是放在相同环境并发执行,而并发环境必然存在资源的竞争,因此对于在激烈的并发竞争环境中,同步操作的成本会更高.
BIO与NIO分析小结
concurrenthashmap
与hashtable
执行内存IO的写操作,后者需要通过加锁实现线程同步,而前者同样是加锁,但却是分片加锁,使得线程可异步化执行,即同一个对象可以让不同线程进行写操作,这个时候性能上的提升并不依赖于线程资源.简而言之,高性能IO设计可以运用分散的思想并借助并发多线程技术以及充分利用计算机资源技术手段来达到目标,同时为了保证web服务可伸缩性,可以考虑引入中间层的思想来解决现有无法扩展的问题,接下来,我们开始进入web服务设计,为了能够支撑更多的并发连接数,一般会有两种web体系架构设计模式,一种是基于线程的架构,另一种是基于事件驱动架构设计.现针对上述两种架构展开分析.
线程连接架构是基于"每个连接对应每个线程"的设计思想,这样设计主要有以下几个方面考虑:
线程与连接1:1模式
线程与连接N:M模式
基于上述线程架构的问题,引入事件驱动设计方案,接下来我们来看下事件驱动设计是什么,如何解决上述的问题.
在讲述事件驱动设计之前,可以先通过一个简单的示例展开.当我们在前端页面触发点击事件的时候,就会调用对应的一个触发函数来响应对应的点击事件,也就是说开发人员需要通过以下方式来完成一个点击事件的注册与绑定操作:
// 获取button组件
var btn = document.getElementById("login");
// 绑定点击事件
login.click(function(){
// login process
});
对此,我们先梳理下什么是事件,事件系统有哪些组成,最后再根据上述的点击事件将整个事件处理流程以时序图的方式展开.
事件定义与结构组成
事件流层
至此,我们对事件的定义有了基本认知之后,那么对于上述的一个完整的点击事件流程是如何进行运作的呢,现如下图所示:
对于EDA的NIO而言,相比上述事件设计是运用相同的思路,但是具体实现的技术方案略有不同,EDA的NIO技术实现是基于Reactor模式,现展开NIO编程的Reactor模式进行分析.
Reactor模式
在一个通用的web服务中,一般具备以下的几方面的特征:
一般地,对于经典的TBA架构的web服务如下图:
在上述图中看到每个线程处理每个handler,且不讨论先前TBA存在的问题,就可扩展性而言就存在局限性,尤其是针对部分线程执行decode-compute-encode过程中出现耗时缓慢情况时,很难对其进行优化操作,甚至无法通过服务进行配置调优,没有达到高性能的可伸缩性要求.
对于一个高性能的IO事件驱动设计,主要包含有以下三个内容:
对于IO事件驱动架构实现的技术主要是使用Reactor模式,现开始进入Reactor模式的分析
Reactor定义
反应器设计模式是一个事件处理模式,用于处理一个或多个输入并发地传递给服务处理程序的服务请求。然后,服务处理程序对传入请求进行多路复用,并将它们同步分派给相关的请求处理程序.
Reactor组成结构
Reactor设计示意图如下
通过上述示意图可知,Reactor模式在应用程序级别代码交由handler进行处理,而对于整个网络的复用操作交由多路复用器进行处理,实现反应堆的复用与应用程序业务逻辑的解耦,同时可以针对handler处理器进行调优处理以达到handler能够更快速地响应真正的IO事件并返回给客户端程序响应结果.
Reactor的事件轮询
通过上述可知,在事件轮询中包含以下三个步骤:
两个核心参与者
Reactor处理流程
select()
不断监听socket事件的变化,通过NIO的SelectionKey保存当前socket事件变化状态.socket
端口监听到事件变化,此时将客户端的socket
注册并保存到SelectionKey中,即Acceptor操作socket
监听到可读事件,将会在Reactor中添加对应的事件响应处理器Handler并由内部的转发器分发到对应的Handler进行处理下游事件反应器为可选,主要用于处理返回的结果呈现,可以理解为前端结果展示的组件.
在文章开头部分讲述到实现高性能的目标,通过对比NIO与BIO的编程设计分析,我们基本上都会基于NIO模式来设计一个高性能的web服务,而一般地,对于NIO服务设计具备高性能的目标,需要借助以下的技术手辅助段来达到目标.
实现高性能手段
accept
以及read
操作为非阻塞byteBuffer
缓存或发送数据一个单线程NIO服务通用设计
select
的轮询调用在上述过程,业务核心数据逻辑具备多样性,需要针对不同的场景来进行分析,因此影响性能的处理步骤往往在于最后一步,由此可通过Reactor与多线程技术来进一步提升web服务的处理性能
Reactor技术演进
接下来以图解的方式来查看Reactor与多线程技术的演进过程.以下图解均来自《Scale IO in Java》以及github上的gnet库来演示.
最后,我们单从Reactor技术变化来看,其设计的目的无非包含以下几个方面:
最后,感谢花时间阅读,如果有用欢迎转发或者点个好看,谢谢!