vlambda博客
学习文章列表

Redis笔记---概述高性能网络模型

一段长时间没更新了,最近在学习Redis网络部分的源码,把一些学习心得总结一下

决定Redis高性能有三个因素:

  1. 基于内存的操作
  2. 合适的数据结构
  3. 高性能的网络模型

今天想聊聊的是Redis高性能的网络模型

一些基础知识

谈到高性能的网络模型,绕不开著名的Reactor模型,中文叫反应堆模型。

要实现高性能网络应用程序(可以百度C10K问题),可以从横向和纵向两个方向扩展,纵向除了升级硬件之外,另一个方式就是使用Reactor模式来构建网络应用程序。

反应堆这个名字比较抽象,但它有个更容易理解的名字,叫事件驱动模型,或者Event Loop模型。这个模型的核心有两点:

  • 它存在一个无限循环的事件分发线程,事件分发的背后,就是select、poll、epoll等I/O多路复用技术的使用
  • 所有的I/O操作都可以抽象成事件,每个事件必须有回调函数来处理

上面提到了I/O多路复用,这里简单地介绍一些select、poll和epoll的区别。

多路复用技术中,select是比较早出现的,它有一个比较明显的缺点是能监听的文件描述符数量有限,默认只能监听1024个文件描述符,如果要超过1024需要修改并重新编译系统内核。

之后出现的poll跟select差别不大,主要差别是突破了文件描述符的限制。

再后来epoll出现了,epoll与select和poll相比,性能有很大的提升,特别是在监听大量的文件描述符的情况下,性能损耗很小,而select和poll就做不到这点了。

下图展示了监听不同数量文件描述符的情况下,select,poll和epoll所消耗的cpu时间。

可以看到,从监听10个到10000个文件描述符,select和poll性能下降比较严重,而epll只是下降了一点,因此在高并发的网络应用中,一般都是选择epoll作为底层实现的。

去除迷雾,弄清本质

我们知道Redis利用单线程来处理网络请求,那究竟是怎样实现的呢?

用大白话来说,Redis的单线程模型就是在主线程中利用一个while循环来不断处理从网络/文件系统中接收到的事件


Redis笔记---概述高性能网络模型


基本原理了解后,接下来就简单看看上面的模型是怎样实现。

关键代码在ae.c的aeMain函数里 

/* * 事件处理器的主循环 */void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它 if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop);
// 开始处理事件 aeProcessEvents(eventLoop, AE_ALL_EVENTS); }}

Redis通过while循环不停地处理接收到的事件,包括文件事件(fileEvent)和时间事件(timeEvent)。aeProcessEvents函数内部首先会处理文件事件,处理完成后再处理时间事件,这样一个循环结束后又会进入下一个循环,直到程序退出。

Linux中一切皆文件,socket也是一个文件,处理socket的文件事件也即是处理网络I/O事件,而时间事件就到达某个时间节点时需要触发某个行为的事件。

细说实现

与Redis网络相关的数据结构,可以参考下面的图。



 从最左边开始看,有redisServer和list这两个数据结构:

  • redisServer: 一个全局静态结构体,只有一个实例,代表Redis服务端
  • list: 一个链表,Redis会为每个客户端链接新建一个redisClient实例,并把实例加入到链表中

中间部分有aeEventLoop和aeApiState两个结构体:

  • aeEventLoop:一个结构体,保存了事件循环(aeMain)使用到的各种上下文信息
  • apiApiState: 一个结构体,这个结构体根据不同的目标平台会选用不同的实现,上图展示的是epoll的实现。epoll实现中,会有一个epfd字段,表示epoll的文件描述符,还有events字段,用来承载epoll_wait返回的已就绪事件

最右边是aeFileEvent和aeTimeEvent结构体:

  • aeFileEvent: 文件事件的抽象
  • aeTimeEvent: 时间事件的抽象(定时任务等,例如Redis的各种后台任务,就是被抽象成事件时间,然后到点执行)

特别需要指出,上面两个Event结构体,内部的字段保存了事件响应函数的引用,当Redis处理该事件时,会从字段中取得函数的指针,调用该函数来处理事件。

由于不同的操作系统使用使用不同的API,windows下一般使用select,linux下使用epoll,mac下使用kqueue。Redis抽象出一套数据结构和API来表示I/O复用中的事件和事件的操作,这样上层的代码就能用同一套处理逻辑,屏蔽掉了底层实现的差异。

体现了面向接口的编程思想


Redis启动时网络相关部分的核心时序



 initServer首先调用aeCreateEventLoop函数新建EventLoop结构体,把事件循环要使用的上下文数据赋值给EventLoop。

然后调用listenToPort函数对外监听服务。

接着调用aeCreateTimeEvent函数注册Redis需要响应的时间事件,处理函数是serverCron,serverCron里包含了各种需要定时处理的任务,例如数据统计,写AOF文件,渐进式rehash等等的操作。

aeCreateFileEvent把服务器的监听socket注册到epoll,响应的函数为acceptTcpHandler。acceptTcpHandler最主要的作用是调用createClient函数为每一个客户端链接建立一个redisClient结构体,并加入到链表中去(前文介绍了),然后把客户端的socket注册到epoll,响应的函数为readQueryFromClient,这个函数就是后续负责处理客户端的读写请求。

aeMain就是实现事件循环的关键,aeMain会不停地循环调用aeProcessEvents函数,顾名思义,它负责不停地处理事件。aeProcessEvents内部会调用aeApiPoll去拉取需要处理的文件事件,然后把事件保存到名为fired的数组里,接下来遍历fired数组里的文件事件,执行关联的的事件处理函数。在处理完文件事件后,再处理时间事件,典型的就是执行serverCron函数。

Redis网络部分整体情况已经介绍完了。一句话总结,Redis高性能网络模型,依赖的是单线程的Reactor模式,底层的核心就是I/O多路复用,记住这点,Redis的高性能就不再神秘


后记

这篇学习笔记,就像挤牙膏一样每天挤一点,除了要阅读Redis的代码,还要把之前对I/O多路复用模糊的地方搞清楚,希望对有兴趣阅读Redis关于网络部分源码的同学提供一些参考。


参考资料:

极客时间 -《网络编程实战》

极客时间 -《Redis核心技术与实战》

《Redis设计与实现》