redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量
存在的问题 Redis 是跑在单线程中的, 所有的操作都是按照顺序线性执行的, 但是由于读写操作等待用户输入或输出都是阻塞的, 所以 I/O 操作在一般情况下往往不能直接返回, 这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务
redis的io模型主要是基于epoll实现的, 不过它也提供了 select和kqueue的实现, 默认采用epoll
有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻, 通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代, 服务器进程每次都把这100万个连接告诉操作系统(从用户态复制 句柄数据结构 到内核态), 让操作系统内核去查询这些套接字上是否有事件发生, 轮询完后, 再将句柄数据复制到用户态, 让服务器应用程序轮询处理已发生的网络事件, 这一过程资源消耗较大, 因此, select/poll一般只能处理几千的并发连接
1.每次调用select/poll, 都需要把fd集合从用户态拷贝到内核态, 这个开销在fd很多时会很大
2.同时每次调用select/poll都需要在内核遍历传递进来的所有fd, 这个开销在fd很多时也很大
3.针对select支持的文件描述符数量太小了, 默认是1024
4.select返回的是含有整个句柄的数组, 应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
5.select的触发方式是水平触发, 应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作, 那么之后每次select调用还是会将这些文件描述符通知进程
相比select模型, poll使用链表保存文件描述符, 因此没有了监视文件数量的限制, 但其他三个缺点依然存在。
epoll是poll的一种优化,返回后不需要对所有的fd进行遍历,在 内核中 维持了fd的列表。select和poll是将这个 内核列表维持在用户态,然后传递到内核中
epoll不再是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成
epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树), 将调用分成了3部分:
(1) 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
(2) 调用epoll_ctl向epoll对象中添加这100万个连接的套接字
(3) 调用epoll_wait收集发生的事件的连接
只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时, epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接
epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048
cat /proc/sys/fs/file-max
察看
效率提升, epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境 中, epoll 的效率就会远远高于 select 和 poll
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中
在epoll中,对于每一个事件,都会建立一个epitem结构体
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户
1.不用重复传递。调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。
2.在内核里, 一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象
3.由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
这个准备就绪list链表是怎么维护的呢?
当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里;当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了
一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可
LT, ET这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回这个句柄.