让人秒懂的 Redis 的事件处理机制
redis 是单进程,单线程模型,与 nginx 的多进程不同,与 golang 的多协程也不同,“工作的工人”那么少,可那么为什么 redis 能这么快呢?
epoll 多路复用
这里重点要说的就是 redis 的 IO 编程模型,首先了解下
为什么要有多路复用呢?
如果没有多路复用,一个线程只能监听一个端口的一个连接,这样这个效率比较低。当然我们有几种办法可以破除这个,一个是使用多线程模型,我们还是监听一个端口,但是一个请求进来,我们为其创建一个线程。但是这种消耗是比较大的。所以我们一直想办法,有没有办法一个线程监听多个端口,或者多个一个端口的多个连接(fd)。
这里再说说 fd, 文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行 I/O 操作(包括网络 socket 操作)的系统调用都通过文件描述符。每个连接请求上来,都会创建一个连接套接字,一个连接使用一个连接套接字。
对于监听端口,我们会有一个监听套接字,对应监听 fd。我们所有的监听业务都是从监听这个套接字开始的。
那么如果我一个程序能同时监听多个连接套接字,是不是就很赞了。是的,这就是 linux 的 io 多路复用逻辑。但是这么多连接套接字,传递数据等是断断续续的,A 连接接收一个包,B 连接再接收一个包,A 连接再接收一个包,B 连接再接收一个包....如果我等着 A 连接把包都接收完再处理 B,那效率是非常慢的。所以,这里我们就需要有一个通知机制,让有收到包的时候通知下处理线程。
linux 的 IO 多路复用逻辑主要有三种:select, poll, epoll。
select
select 模型监听的三个事件:读数据事件,写数据事件,异常事件。
使用 select 模型的步骤如下:
我们确定要监听的监听 fd 列表
调用 select 监听所有监听 fd,阻塞线程。
select 只有当有事件出现并且有事件的 fd 已经等待完毕
如果是创建一个连接事件:
创建一个连接套接字,连接 fd
将连接 fd 和监听 fd 集合放在一起
如果是一个读写事件:
遍历所有 fd,判断是否是准备好的 fd
如果是准备好的 fd,进行业务读写逻辑
循环进入 select。
select 一次可以监听 1024 个文件描述符。
poll 模型
poll 传递给内核的是:
监听的 fd 集合
需要监听的事件类型
实际发生的事件类型
poll 的模型逻辑是:
我们确定要监听的监听 fd 列表
调用 poll 监听所有监听 fd,阻塞线程。
poll 只有当有事件出现才解除阻塞
如果是创建一个连接事件:
创建一个连接套接字,连接 fd
将连接 fd 和监听 fd 集合放在一起
如果是一个读写事件:
遍历所有 fd,判断是否是有读写事件的 fd
如果 fd 有读写事件,进行业务读写逻辑
循环进入 poll。
poll 比 select 优秀在它没有了 1024 的限制了。但是还是有一些缺陷,就是必须要遍历所有 fd。
现在 C++程序员面临的竞争压力越来越大。那么,作为一名 C++程序员,怎样努力才能快速成长为一名高级的程序员或者架构师,或者说一名优秀的高级工程师或架构师应该有怎样的技术知识体系,这不仅是一个刚刚踏入职场的初级程序员,也是工作三五年之后开始迷茫的老程序员,都必须要面对和想明白的问题。为了帮助大家少走弯路,技术要做到知其然还要知其所以然。以下视频获取点击:C++架构师学习资料
如果想学习 C++工程化、高性能及分布式、深入浅出。性能调优、TCP,协程,Nginx 源码分析 Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,Linux 内核,P2P,K8S,Docker,TCP/IP,协程,DPDK 的朋友可以看一下这个学习地址:C/C++Linux服务器开发高级架构师/C++后台架构师
epoll
epoll 的数据结构类似 poll,但是在调用 epoll 的时候,它不是返回发生了事件的 fd 个数,而是返回了所有发生的事件,这个事件中可以查出发生事件的 fd。
所以 epoll 的逻辑模型是:
我们确定要监听的监听 fd 列表
调用 epoll 监听所有监听 fd,阻塞线程。
epoll 只有当有事件出现才解除阻塞,并且返回事件列表
遍历事件列表:
如果是创建一个连接事件:
创建一个连接套接字,连接 fd
将连接 fd 和监听 fd 集合放在一起,继续 epoll
如果是一个读写事件:
处理这个事件
循环进入 epoll。
说白了,epoll 就是我们逻辑上能想到的最优的通知机制。一群人去排队,有多个事件发生,警察来了,那么就告诉警察有哪几个列发生了什么事件,警察一个个处理就行了。
Reactor 模型
有了 IO 多路复用的机制,我们就可以实现一种模型,叫做 Reactor 模型了。Reactor 模型不懂的可以看看这个视频讲解:epoll原理剖析以及reactor模型
最经典的一张图就是这个
reactor 的五大角色:
Handle(句柄或者是描述符)
Synchronous Event Demultiplexer(同步事件分离器)
Event Handler(事件处理器)
Concrete Event Handler(具体事件处理器)
Initiation Dispatcher(初始分发器)
简要来说,Reactor 就是我们现在最正常理解的“事件驱动”,对,就是字面理解的那种。比如订阅一个 kafka,我们会创建一个监听程序,监听 kafka 的某个 topic,然后在监听程序中挂载几个处理不同消息的处理程序,每当有一个事件从 topic 进入的时候,我们就会有通过这个监听程序,通知我们的处理程序。处理程序来处理不同的消息。
这种所谓的通知机制,就叫做 reactor。
这个 kafka 的例子,里面有一个监听事件的程序,它一定是一个同步的,一条消息来了,投递一个消息,就叫做 Synchronous Event Demultiplexer(同步事件分离器)。而这个消息,就是 Handle(句柄或者是描述符)。我们需要将某个具体的事件处理函数,也就是上图的 Concrete Event Handler(具体事件处理器) 挂载到监听的处理程序中。当然这里的每个 Concrete Event Handler(具体事件处理器) 都必须遵照某种格式,比如定义了 handle_event 和 get_handle 接口。这种格式我们统称为 Event Handler(事件处理器)。再回到监听事件,监听事件一定有一个挂载的具体 map 之类的结构,即哪个事件对应哪个处理程序,这个挂载的核心我们叫它 Initiation Dispatcher(初始分发器)。
标准的处理流程描述如下:
当应用向 Initiation Dispatcher 注册 Concrete Event Handler 时,应用会标识出该事件处理器希望 Initiation Dispatcher 在某种类型的事件发生发生时向其通知,事件与 handle 关联
Initiation Dispatcher 要求注册在其上面的 Concrete Event Handler 传递内部关联的 handle,该 handle 会向操作系统标识
当所有的 Concrete Event Handler 都注册到 Initiation Dispatcher 上后,应用会调用 handle_events 方法来启动 Initiation Dispatcher 的事件循环,这时 Initiation Dispatcher 会将每个 Concrete Event Handler 关联的 handle 合并,并使用 Synchronous Event Demultiplexer 来等待这些 handle 上事件的发生
当与某个事件源对应的 handle 变为 ready 时,Synchronous Event Demultiplexer 便会通知 Initiation Dispatcher。比如 tcp 的 socket 变为 ready for reading
Initiation Dispatcher 会触发事件处理器的回调方法。当事件发生时, Initiation Dispatcher 会将被一个“key”(表示一个激活的 handle)定位和分发给特定的 Event Handler 的回调方法
Initiation Dispatcher 调用特定的 Concrete Event Handler 的回调方法来响应其关联的 handle 上发生的事件
在这五种角色中,
其中的 Initiation Dispatcher(初始分发器) 是最重要的,我们也称其为 Reactor。它本身定义了一些规范,同时提供了 Handler 的一些注册机制。
而 Synchronous Event Demultiplexer(同步事件分离器) 在 IO 场景下,一般是由操作系统底层实现的,就是说操作系统底层必须能有这个能力,才能基于这个能力实现 Reactor 模型。在我们这个场景下,就是前面提到的 linux 的多路复用机制。
Handle(句柄或者是描述符)在 IO 场景下就是 IO 网络连接的 fd。
而 Event Handler(事件处理器) 和 Concrete Event Handler(具体事件处理器) 在 IO 场景下分为三种处理事件:连接事件,写事件,读事件。对于连接事件的处理器,我们称之为 acceptor,读/写事件的处理器,我们统称为 handler。
所以在 IO 场景下,Reactor 我们需要实现的三个关键角色为:reactor、acceptor、handler。
Redis 的实现
在 redis 中,下面一张图就能说明其实现逻辑。
在 redis 中,有个 reactor(叫做 aeMain)接收客户端的 redis 请求。而在这个 reactor 中除了监听连接事件 acceptor 之外,还可以动态注册各种 handler (aeCreateFileEvent)。当一个客户端请求进入的时候,调用 aeProcessEvents 来分发事件。
这个逻辑就很清晰了吧。整个就是 redis 的事件处理机制。
评论