七叶笔记 » golang编程 » 精通Redis!epoll?IO的同/异步、阻塞/非阻塞?都懂了吗?

精通Redis!epoll?IO的同/异步、阻塞/非阻塞?都懂了吗?

说到 缓存 数据库 Redis 无疑是现在互联网界的一哥了。许多大厂的缓存 中间件 都是在Redis的基础上进行了封装。Redis作为一个以单线程模型为基础的缓存在速度上如何能媲美 多线程 的MC缓存?除了三圈在上一篇文章《带你领略Redis数据结构之美》中介绍的Redis采用了大量的精妙的数据结构来提升其性能外还与Redis底层的 IO 模型息息相关。

Redis和Memecache

首选我们先讨论下redis的单线程和memecache的多线程。众所周知Redis使用的是单线程模型,在规避了cpu切换损耗的同时也能完全保证数据按顺序提交,不需要考虑数据一致性的问题。memcache 是多线程模型通过cas保证数据一致性。

Redis 之所以叫做单 线程 ,并非每个redis进程都只有一个线程(其持久化和集群同步就是开启了另外的线程)而是因为它采用的文件事件处理器 file event handler是单线程的。至于Redis和Memcache谁性能更好,这个在不同场景下有不同的判定,但是无论哪个通过正确的调优和集群部署都能应对绝大部分公司的业务需求,而Redis的有点在于其具备丰富的数据结构,同时提供了完善的持久化方案,能适应更为丰富的应用场景。

epoll 的优势

上面提到了Redis底层采用了Epoll这款 多路复用 IO模型,这里先拿它与其他几种多路复用IO模型做一个简单的对比,以便大家先对此有个简单的认识。相比于select/poll Epoll在处理大量 文件描述符 时具有非常明显的优势。缓存这类中间件的使用场景就是为了提升高并发场景下的系统的稳定性和吞吐量,而服务器之间的数据传输一般通过 socket 进行,在 Linux 环境下所有执行I/O操作(包括网络socket操作)的系统调用都通过文件描述符FD,在高并发场景下特别是IM场景下,必然会有大量活跃的socket存在,这样就会产生大量的FD。

由于select受到一个进程所打开的最大FD的限制(通过FD_SETSIZE设置,默认值是1024)所以对于现在大型互联网应用动辄数万的并发场景来说远远不能满足,虽然可以选择通过修改linux服务器源码的形式来提升这个限制,可是据验证这样会带来网络效率的下降。而 poll 通过链表实现了对fd的存储,所以没有长度限制,它所支持的FD上限理论是最系统大可以打开文件的数目。但他和select同时存在另一个弊端,进行网络数据传输时sokect产生的FD都会被保存在一个集合里。而select和poll该执行哪个用户线程的操作都是通过轮训集合里的这些FD来判断的。在select/poll模型下会将这个存储fd的集合不断的从线程空间拷贝到内核空间,然后轮训看是否存在可操作的IO,当fd数量庞大的时候拷贝和 轮询 的过程都是非常耗时且消耗CPU的。Epoll的设计就很好的解决了无差别轮训和FD集合的拷贝这两个问题。epoll和select/poll的区别可以先看下图,不明白的地方后面《Epoll介绍》中会有讲解。

Epoll介绍

在介绍Epoll之前,我们先明确一下概念,Epoll是Linux系统对多路复用IO的一种实现(查看JDK NIO源码你可以看到在linux nio采用epoll作为底层实现,但是在windows下还是用的select),同select/poll一样,他们都是非阻塞同步IO,而并非异步IO。那么什么是阻塞和非阻塞,什么又是同步和异步呢?

1、阻塞和非阻塞

我们先说下阻塞和非阻塞。为了提升IO效率,系统引入了缓冲区,减少频繁I/O操作而引起频繁的系统调用,对于用户线程来说操作一个流时,是以缓冲区为单位进行操作。对于内核来说也是如此。这个缓冲区存在四个状态:缓冲区满,缓冲区空,缓冲区非空,缓冲区非满。我们假设有个用户线程要进行数据处理,那么当线程写入数据的时候会先检查这个缓冲区是否为满,如果非满则进行数据写入,如果满则停止写入;同时内核进行数据处理时也会检查这个缓冲区是否为空,如果非空则将数据拷贝到内核空间进行处理,如果空则阻塞。于是我们知道当缓冲区为空或者非满时候用户线程可以进行操作,而当缓冲区为满或者非空时内核线程可以进行处理。其他情况则会发生阻塞。那么非阻塞就是指内核线程在发现当前处理线程的缓冲区为空时不是阻塞在那里等待缓冲区里有数据再处理,而是切换到处理其他线程的数据。这里就会出现前面提到的一个问题,为了判断当前哪个线程有数据可以处理,需要不断的轮训FD的集合。

2、异步和同步

那么什么是异步和同步呢。简单来说异步和同步区分就是用户线程发起的操作后无需等待结果立刻就去做其他的事情,内核线程会将数据从用户线程拷贝到内核空间,处理好之后再拷贝回用户线程,并通知用户线程操作完成。也就是说用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

3、Epoll的设计要点

(1)首选Epoll在Linux内核中构建了一个文件系统,该文件系统采用 红黑树 来构建,红黑树在查询、新增、删除的效率极高,保障了在存在大量活跃连接的情况下的性能。

(2)其次Epoll红黑树上采用事件异步唤醒,内核监听I/O,事件发生后内核搜索红黑树并将对应节点数据放入异步唤醒的事件队列中。这就避免了无差别的轮询,不会因为连接数增加而导致性能的快速下降。

(3)最后Epoll的数据从用户空间到内核空间采用 mmap 存储I/O映射来加速。该方法是目前Linux进程间通信中传递最快,消耗最小,传递数据过程不涉及系统调用的方法。这点大大提升了存在大量FD时数据拷贝的消耗。

Epoll具体的图示如下:

相关文章