双十一剁手节,在享受剁手的快感后,阿里公布了双十一交易的峰值又破纪录了,每秒25.6万,这卓越的成绩在全球恐怕也是没有对手了。马云说办双十一不赚钱,目的有四个,一是给消费者带来快乐,二是给商家带来快乐,三是给阿里带来技术的提升,四是给阿里带来组织人才的提升。虽说语出惊人,但是第三个目的确实是实实在在的,25.6万每秒的成绩绝非能在一般的场景中锻炼出来,而实现这样超高水准成绩涉及到硬件、软件等方方面面的数百项技术,绝对是超级工程。而在这个超级工程中,最重要的基础之一就是高并发的网络I/O操作,不论是客户端到服务器,还是服务器集群间通信、服务器内部数据交互,都少不了网络I/O操作,要做到高并发的业务,稳定可靠的高性能网络I/O并发是基石。
高并发是个相对的概念,作为人类,跟一个人讲话会很轻松,同时跟两个人讲话勉勉强强,能同时跟5个人讲话就很牛了,若能同时跟50个人讲话,那就可以说这个人是高并发了。高并发是可以形容各种业务的,前面提到阿里的支付业务,每秒25.6万笔是举世瞩目的成绩,但是数据库简单的查询业务其实可以轻轻松松达到百万甚至千万次每秒,故高并发是必须要和某一项具体业务绑在一起的,为了更加明确和清晰,剥离障眼的外衣,只看精华,只看不考虑任何应用业务的网络I/O并发。
网络I/O并发发展了几十年,加上各种新兴高级编程语言测出不穷,在网络I/O上做了各种优化或封装,捣鼓来捣鼓去,发明了reactor模型、proactor模型等等,万变不离其宗,无非都是四种网络I/O模型。这四种网络I/O模型都是TCP下的模型,适用于所有基于TCP的网络协议,基本覆盖了互联网绝大多数的网络通信协议,而UDP等非流式网络协议不讨论。
说了半天,先来认识一下四种模式的大名:同步阻塞、同步非阻塞、异步阻塞、同步非阻塞。为了更好紧贴主旨,简化逻辑,以下没有说明的情况下只讨论I/O的读写操作,异常和连接建立等操作略过。
同步阻塞
相信这是所有人刚接触网络编程时最先学习就是同步阻塞模式,这种模式非常的简单容易理解,在实际多应用于业务简单的客户端。
这种模式下的socket被设置为阻塞模式,所谓的阻塞就是socket在建立连接、读、写操作上都可能会被阻塞,直到socket被激活唤醒才执行I/O操作;而所谓的同步指的是应用线程必须要等I/O操作结束,即使被阻塞了也要等。打个比方去ATM机取钱,只有输入了密码才能进行下一步,即使ATM反应很慢,输了密码还加载半天,这种交互模式虽然效率低,不过好在简单和保险。
同步非阻塞
同步非阻塞,顾名思义,socket被设置为非阻塞模式,但依旧工作在同步模式下。这种模式在实际中很少见,应用的地方不是很多,主要针对有大量I/O读写的业务,比如FTP服务,而大多数业务场景下同步非阻塞模式的性价比不高,较大的浪费机器资源。
在该模式下的应用线程并不知道操作系统内核是否准备好了socket上的操作,需要不断的调用内核的系统调用api判断返回值,遇到socket可操作时便执行操作。由此看见,应用线程的所有系统调用都必须得到反馈才能执行接下来的流程,同步操作的特产非常明显。
异步阻塞
终于到异步了,异步可谓是让网络I/O并发性能得到巨大的提升,异步模型的出现直接改变了网络I/O编程的历史,得益于异步模式可以写出并发非常高的模型,现在的高性能网络I/O并发基本都是用异步模式,并且分为阻塞和非阻塞两种。
这一节专门讲异步阻塞模式,先啰嗦下何为异步,所谓异步指的调用者不需要等被调用者立即返回调用结果,区别于同步下调用者必须等到被调用者返回结果。然后再说阻塞,异步阻塞的阻塞不好理解,同步模式下的阻塞指的是socket阻塞,而异步阻塞的socket也是非阻塞的,那为什还叫异步阻塞呢?
在解答这个问题前,先来了解下异步阻塞用到的系统调用,异步阻塞的实现依赖I/O多路复用(multiplexing)或者称为“事件驱动”,主要用到的API有Select、Epoll、Kqueque等。上一小节说过socket在非阻塞模式下,通过系统调用操作状态为不可操作的socket时会立即得到返回,然而这时的返回状态是失败的,I/O多路复用的出现很好的解决了这种多余的调用,应用线程把感兴趣的socket交给系统内核监测,当所有的socket都没有可操作的状态时被阻塞,当有任意一个socket状态为可操作时唤醒线程,线程就可以去处理状态发生变化的socket了。
I/O多路复用不仅让socket可以异步操作了,还可以让一个线程同时处理多个socket,为单进程单线程编写高并发的网络I/O提供了可能。但I/O多路复用的特征也就是让应用层的线程阻塞在I/O多路复用的系统调用上了,故称为异步阻塞模式。
异步非阻塞
有很多人说异步阻塞模式不是真的异步,异步非阻塞模式才是真正的异步,说的大概是异步阻塞模式下I/O读写异步调用并没有真的调用读写API,而是把socket加入到监测列表里,等到socket状态变为可读写时才真正调用读写API。从调用形式看确实像个“伪异步”,不过啊,谁教异步阻塞是哥哥,出来的早,占先机,加上性能足够强劲,现在大部分的高并发I/O仍旧用的异步阻塞模式,比如时下热门的网络组件,C编写的Libevent、Libev和JAVA编写的Netty等等都是实现了事件驱动的Reactor模型,即异步阻塞模式。而“真异步”异步非阻塞模式的API出现的稍晚(Linux下2.6才出现——AIO,windows下到vista出现——IOCP),以至于现成的网络组件比较少,C下比较出名的就是ACE,而微软直接在语言层面C#支持了异步非阻塞。
通过下图可以看到,应用线程直接通过系统的异步API操作socket,不需要等待返回操作结果也不会阻塞,直接去干别的事就好了,操作系统内核会负责socket上的一切工作,等待socket上的操作结束,根据应用线程给的参数操作系统会通过调用异步线程告知应用层socket操作完毕以及处理接下来的逻辑,或者更简单粗暴的直接通过信号打断应用线程,告知socket操作完毕及处理后续逻辑。
看过异步非阻塞模式是不是觉得直接秒杀前面三种模式,从理论上说,异步非阻塞模式的确是性能最好的一种模式。
Linux的异步API
AIO介绍
现在量大常见的操作系统都提供了成熟稳定的异步API,windows下的IOCP,以及Linux下的AIO,当然异步API既可以实现异步阻塞模型又可以实现异步非阻塞模型,比如windows下的很多语言实现的Reactor模型就是通过IOCP,但是要写出性能极致的I/O高并发还是实现“真异步”的异步非阻塞。
Windows的IOCP就不用赘述了,来简单认识不常见的Linux下的异步API:AIO。AIO的API可以用短小精悍来形容,总共才7个接口,分别如下:
API函数
原型
说明
aio_read
intaio_read(structaiocb*aiocbp);
请求异步读操作
aio_error
intaio_error(structaiocb*aiocbp);
检查异步请求的状态
aio_return
ssize_taio_return(structaiocb*aiocbp);
获得完成的异步请求的返回状态
aio_write
intaio_write(structaiocb*aiocbp);
请求异步写操作
aio_suspend
intaio_suspend(conststructaiocb*constcblist[],intn,conststructtimespec*timeout);
挂起调用进程,直到一个或多个异步请求已经完成(或失败);这个API可以用来实现异步阻塞
aio_cancel
intaio_cancel(intfd,structaiocb*aiocbp);
取消异步I/O请求
lio_listio
intlio_listio(intmode,structaiocb*list[],intnent,structsigevent*sig);
发起一系列I/O操作
可以看到接口设计得相当巧妙,接口参数非常少,最多也才4个参数,数据类型也很简单,除了structaiocb都是常规数据类型。其中最复杂的参数类型structaiocb亦是非常的精巧,整个结构体总共才7个成员,使用起来很方便,结构体成员如下:
数据类型
名称
描述
Int
aio_fildes
文件描述符
off_t
aio_offset
文件偏移
volatilevoid*
aio_buf
用户数据缓冲区指针
size_t
aio_nbytes
待传输数据长度
Int
aio_reqprio
请求优先级
structsigevent
aio_sigevent
异步通知结构
Int
aio_lio_opcode
要执行的操作
我们已经熟悉了AIO的所有接口,对,你没看错,总共需要我们记住的才7个函数,仅仅就这7个函数,就可以完成不可思议的异步I/O操作。而这仅有的7个函数,又可以划分成3种类型:
I/O操作控制
aio_read、aio_write、lio_listio、aio_cancel
获取I/O操作状态
aio_error、aio_suspend
获取I/O操作结果
aio_return
I/O操作控制类型中有三个接口aio_read、aio_write、lio_listio是发起I/O操的,一个接口aio_cancel用来取消操作。aio_read、aio_write不用解释了,看字面就知道分别是发起读写操作,而lio_listio是发起批量操作,既可以是读也可以是写。
获取I/O操作状态类型中只有两个接口,aio_error是立即获取状态,aio_suspend则会阻塞调用进程,直到I/O操作结束。
获取I/O操作结果的接口就一个aio_return,只有在I/O操作状态为结束的时候才能调用。
AIO的使用流程就更简单了,AIO提供的API少而精悍,可以根据需要灵活的多种方式调用,为了深入浅出,先介绍两种方式调用,轮询的方式和通知的方式,如下图所示,从图上可以看出,轮询的方式更为直接简单。
AIO的轮询方式示例
轮询方式非常简单,发起一个I/O操作,然后系统内核就去异步处理这个操作了,这时候主线程也不干别的,循环获取I/O操作的状态,直到I/O操作的状态为结束,最后获取I/O操作的结果即可。没错,内核层的I/O操作确实是异步的,只不过应用层线程在I/O操作期间自我等待同步,所以白搭,对于客户端来说就是非异步的。直接上代码:
代码清单1
intfd,ret;
structaiocbmy_aiocb;
//打开一个文件
fd=open(file.txt,O_RDONLY);
bzero((char*)my_aiocb,sizeof(structaiocb));
//给缓冲区分配空间
my_aiocb.aio_buf=malloc(BUFSIZE+1);
//参数赋值
my_aiocb.aio_fildes=fd;
my_aiocb.aio_nbytes=BUFSIZE;
my_aiocb.aio_offset=0;
//发起异步读取操作
ret=aio_read(my_aiocb);
//此处循环获取操作状态,相当于同步非阻塞了
while(aio_error(my_aiocb)==EINPROGRESS);
//I/O操作结束,获取结果
if((ret=aio_return(my_iocb))0){
//成功,获取了读取的字节数
}else{
//操作失败
}
AIO的通知方式示例
通知方式使用也非常简单,发起一个I/O操作,这与轮询方式的完全一致,区别在于状态获取,主线程发起操作后就去干别的了,不再傻傻的等着获取I/O操作状态,这才是真的异步嘛,你干你的,我干我的。那调用者怎么知道,I/O操作结束了没有呢,答案就是异步通知,其实AIO的异步通知并不是什么新科技,乃是POSIX-sigevent,信号事件,经典的异步事件通知机制,熟悉的读者看到前文structsigevent这个结构体时估计就已经猜到用来干啥的。内核在I/O操作结束的时候,通过信号中断主线程或者创建一个新的线程告知用户I/O操作结束了,至于是用信号中断主线程还是创建新线程,这个由使用者控制。无论哪种方式通知,用户在收到通知后,只需检查I/O操作的状态,当状态值为已结束,获取结果即可。
接下来给出一段使用信号中断作为一步通知代码样例:
代码清单2
//发起I/O操作
voidsetup_io(...){
intfd;
structsigactionsig_act;//安装信号使用的结构
structaiocbmy_aiocb;
...
sigemptyset(sig_act.sa_mask);
sig_act.sa_flags=SA_SIGINFO;//设置信号可以传参
sig_act.sa_sigaction=aio_