redis高性能概述

背景

今天来讲一下redis高性能的原因,如何以单线程来支撑10W级别的并发。

Redis的事件循环

C10K problen

C10K就是单机一万并发的问题,最初服务器是来一个连接就创建一个进程/线程,这样在并发量多的情况下就会有性能问题。

问题描述

创建的进程/线程多了,数据拷贝频繁

缓存I/O需要数据拷贝,其拷贝流程是:磁盘 -> 内核缓冲区 -> 用户进程空间,并且拷贝过程是阻塞的。

上下文切换开销大

有限的硬件资源下,多线程通过微观上时间片的切换,实现了同时服务上百个用户的能力。64 位的 Linux 为每个线程的栈分配了 8MB 的内存,还预分配了 64MB 的内存作为堆内存池。多线程的开发成本虽然低,但内存消耗大,切换次数过多,无法实现高并发。

总结

在单机一万并发下,创建的进程线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、其拷贝过程是阻塞的)。

进程/线程上下文切换消耗大,主要是包括寄存器的切换和页表的切换。寄存器需要保存当前和即将进行的指令,页表需要保存当前进程使用到的数据。

切换多了导致操作系统崩溃,这就是C10K问题的本质。

redisxiancheng1

异步编程

怎么才能实现高并发呢?把上图中本来由内核实现的请求切换工作,交由用户态的代码来完成就可以了。

异步编程方式通过非阻塞系统调用和多路复用,把原本属于内核的请求切换能力,放在用户态的代码中执行,如下图是使用异步编程由用户态代码来切换线程。

由用户态的异步框架来切换用户请求,切换后是在同一个线程中运行,不需要进行内核上下文切换。
redisxiancheng2

这样,不仅减少了每个请求的内存消耗,也降低了切换请求的成本,最终实现了高并发。Redis、Nginx就是依赖异步化编程实现了百万量级的并发。

具体地说,它们是通过同步非阻塞IO实现宏观层面的异步操作

具体到linux执行层面,epoll技术的编程模型是同步非阻塞回调,也可以叫做Reactor,事件驱动,事件轮循(EventLoop)。Nginx,libevent,node.js这些就是Epoll时代的产物。对应的另外一种模式是Proactor。

这两种模式的主要区别是真正的读取/写入数据操作是由谁来完成的。Reactor需要由应用程序自己读取或者写入数据,而Proactor是已经帮用户读取/写入完成数据到缓存区,操作系统会读取/写入缓存区到真正的IO设备,应用程序只需要从缓存区读取/写入即可。

IO复用选择

select/poll/epoll都是都是IO多路复用。

select

select拷贝fd从用户态到内核态,然后遍历fd,调用其回调函数,主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列。

在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

回调方法返回时,会返回准备就绪的设备。如果没有准备就绪,则休眠直到下次被唤醒继续重复这个过程。

select实现需要自己不断轮询所有fd集合,直到设备就绪,所以select的运行复杂度是O(n)。而且每次调用select的时候,都需要把fd从用户态拷贝到内核态。select支持的文件描述符数量是1024。

poll

poll的实现跟select一致,唯一区别是poll基于链表保存文件描述符,所以没有select的1024个的数量限制。

epoll

epoll是对select/poll的改进,我们来看下它是怎么解决select/poll的两个缺点的。

epoll只在第一次调用的时候把fd拷贝进内核,不需要每次wait的时候都拷贝。它是在epoll_ctl的时候拷贝的,而不是在epoll_wait的时候拷贝的,所以不需要每次wait都拷贝一遍。

epoll在epoll_ctl的时候也是需要把current挂一遍,并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。

摘抄一下优点总结:

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

redis的选择

具体看redis的选择,当然是哪个高效来哪个。

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif

模式

Reactor模型

刚才我们讲到redis采用的是Reactor模型,这里就来详细讲一下什么是Reactor模型。

并发模型的选择,主要需要考虑以下两点:

  1. IO模型:服务器如何管理连接
  2. 进程模型:服务器如何处理请求

如下图所示,Reactor监控连接事件,收到事件后通过dispatch分发

  • 如果是连接建立事件,则由acceptor处理。Acceptor接收请求并创建handler处理。
  • 如果不是连接事件,则调用连接对应的handler。Handler主要作用是读取数据,然后做业务处理,最后返回数据。

redisxiancheng3

Proactor模型

Proactor和Reactor的主要区别是,Proactor读取数据后再调用回调函数处理请求。

如下图所示,Proactor处理请求的流程是:

  • Proactor Initiator 负责创建 Proactor 和 Handler,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核。
  • Asynchronous Operation Processor(异步IO操作) 负责处理注册请求,并完成 I/O 操作,完成IO操作后通知Proactor。
  • Proactor根据事件的不同类型调用不同handler。
  • Handler完成业务处理。

简单来讲,Reactor和Proactor的区别就是,一个是同步非阻塞,一个是异步非阻塞。

Redis的主流程

Reactor模型

具体到redis上面,redis使用的是单Reactor单进程模型。

如下图所示,redis中使用的模式是Reactor模式,主要是基于以下两个原因:

  1. Linux下AIO不成熟
  2. redis基于内存,数据处理快,不会对IO读写进行过长时间的阻塞(当然这也只是相对而言,我们可以看到redis6.0上已经支持了多线程的IO读写,后面会找写一篇文章来讲)

redisxiancheng5

Redis事件循环

我们来看下redis的事件循环:参考下图

  • Server初始化并启动下面的循环。
  • aeEventLoop接收请求并分发
    • 轮询ae_poll获取已经发生的文件事件。aeApiPoll的最大阻塞时间取决于最近到达的时间事件的事件
  • 接下来,如果是可读事件则调用acceptTcpHandler:
    • 创建client对象,绑定后续客户端命令请求
    • 创建文件事件,绑定处理函数processCommand,处理客户端命令请求
    • 写入回复缓冲区
  • 如果是可写事件,则调用sendReplyToClient:
    • 取回复缓冲区内容写入套接字
  • 如果有时间事件到期了,则调用serverCron:
    • 比如清除过期键值、渐进式rehash、bgsave、aof等等。

redisxiancheng6

refer

https://www.cnblogs.com/anker/p/3265058.html
http://redisbook.com/preview/sds/implementation.html

comments powered by Disqus