Redis Server跑在单进程单线程中,接收到的命令操作都是按照顺序线性执行的,即便如此,它的读写性能依然能达到10W+的QPS,不得不说:Redis的设计十分优秀。
为什么Redis的读写性能这么高呢?原因有许多,我们列举主要的三个:
1、Redis基于内存操作:
绝大部分的请求为纯粹的内存操作,而且使用hash结构存储数据,查找和操作的时间复杂度均为O(1)。
2、Redis数据结构简单:
redis对数据的操作还是比较简单的,而且redis的数据结构是专门设计的。
3、单线程-IO多路复用模型:
单线程的设计省去了很多的麻烦:比如上下文切换、资源竞争、CPU切换消耗以及各种锁操作等等问题,而IO多路复用模型的使用更让Redis提升了效率。
今天,我们就来聊一聊:IO多路复用模型。
IO即为网络I/O,多路即为多个TCP连接,复用即为共用一个线程或者进程,模型最大的优势是系统开销小,不必创建也不必维护过多的线程或进程。
IO多路复用是经典的Reactor设计模式,有时也称为异步阻塞IO(异步指socket为non-blocking,堵塞指select堵塞),为常见的四种IO模型之一,其他三种分别是:同步堵塞IO、同步非堵塞IO、异步(非堵塞)IO。
IO多路复用的核心是可以同时处理多个连接请求,为此使用了两个系统调用,分别是:
-
select/poll/epoll--模型机制:可以监视多个描述符(fd),一旦某个描述符就绪(读/写/异常)就能通知程序进行相应的读写操作。读写操作都是自己负责的,也即是阻塞的,所以本质上都是同步(堵塞)IO。Redis支持这三种机制,默认使用epoll机制。
-
recvfrom--接收数据。
而blocking IO只调用了recvfrom,所以在连接数不高的情况下,blocking IO的性能不一定比IO多路复用差。
补充:异步IO无需自己负责读写操作。
我们来看一下IO多路复用模型的三种实现机制:
1、select机制:
select原理:
当client连接到server后,server会创建一个该连接的描述符fd,fd有三种状态,分别是读、写、异常,存放在三个fd_set(其实就是三个long型数组)中。
#include <sys/select.h>
//select接口,调用时会堵塞
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
而select()的本质就是通过设置和检查fd的三个fd_set来进行下一步处理。
select大体执行步骤:
1) 使用copy_from_user从用户空间拷贝fd_set到内核空间
2) 内核遍历[0,nfds)范围内的每个fd,调用fd所对应的设备的驱动poll函数,poll函数可以检测fd的可用流(读流、写流、异常流)。
3) 检查是否有流发生,如果有发生,把流设置对应的类别,并执行4,如果没有流发生,执行5。或者timeout=0,执行4。
4) select返回。
5)select阻塞进程,等待被流对应的设备唤醒时执行2,或timeout到期,执行4。
select的局限性:
-
维护一个存放大量描述符的数组:每次调用select()都需要将fd_set从用户态拷贝到内核态,然后由内核遍历fd_set,如果fd_set很大,那么拷贝和遍历的开销会很大,为了减少性能损坏,内核对fd_set的大小做了限制,并通过宏定义控制,无法改变(1024)。
-
单进程监听的描述符数量有限制:单进程能打开的最大连接数。
-
轮询遍历数组,效率低下:select机制只知道有IO发生,但是不知道是哪几个描述符,每次唤醒,都需要遍历一次,复杂度为O(n)。
2、poll机制:
poll原理:
poll的实现和select非常相似,轮询+遍历+根据描述符的状态处理,只是fd_set换成了pollfd,而且去掉了最大描述符数量限制,其他的局限性同样存在。
#include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
3、epoll机制:epoll对select和poll进行了改进,避免了上述三个局限性。
epoll原理:
与select和poll只提供一个接口函数不同的是,epoll提供了三个接口函数及一些结构体:
/*建一个epoll句柄*/
int epoll_create(int size);
/*向epoll句柄中添加需要监听的fd和时间event*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*返回发生事件的队列*/
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
struct eventpoll
{
...
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll_ctl监控的事件*/
struct rb_root rbr;
/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
struct list_head rdllist;
...
};
-
调用epoll_create:linux内核会在epoll文件系统创建一个file节点,同时创建一个eventpoll结构体,结构体中有两个重要的成员:rbr是一棵红黑树,用于存放epoll_ctl注册的socket和事件;rdllist是一条双向链表,用于存放准备就绪的事件供epoll_wait调用。
-
调用epoll_ctl:会检测rbr中是否已经存在节点,有就返回,没有则新增,同时会向内核注册回调函数ep_poll_callback,当有事件中断来临时,调用回调函数向rdllist中插入数据,epoll_ctl也可以增删改事件。
-
调用epoll_wait:返回或者判断rdllist中的数据即可。
epoll两种工作模式:LT--水平触发 ET--边缘触发
-
LT:只要文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作。
-
ET:检测到有IO事件时,通过epoll_wait调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,必须将该文件描述符一直读到空,让errno返回EAGAIN为止,否则下次的epoll_wait不会返回余下的数据,会丢掉事件。
ET比LT更加高效,因为ET只通知一次,而LT会通知多次,LT可能会充斥大量不关心的就绪文件描述符。
epoll总结:
-
使用红黑树而不是数组存放描述符和事件,增删改查非常高效,轻易可处理大量并发连接。
-
红黑树及双向链表都在内核cache中,避免拷贝开销。
-
采用回调机制,事件的发生只需关注rdllist双向链表即可。
epoll编程框架:一个大体的编程应用框架
for( ; ; )
{
nfds = epoll_wait(epfd, events, 20, 500);
for(i=0; i<nfds; ++i)
{
if(events[i].data.fd == listenfd) //如果是主socket的事件,则表示有新的连接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket
{
if ( (sockfd = events[i].data.fd) < 0) continue;
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他情况的处理
}
}
}
PS:如有任何问题或疑问,请留言告诉我。
喜欢这篇文章的朋友,欢迎关注公众号,第一时间收到更新内容。