Redis 网络模型
字数: 0 字 时长: 0 分钟
网络编程模型
在 《UNIX网络编程》 一书中,总结归纳了 5 种 IO 模型:
- 阻塞 IO (Blocking IO)
- 非阻塞 IO (Non-Blocking IO)
- IO 多路复用 (IO Multiplexing)
- 信号驱动 IO (Signal-Driven IO)
- 异步 IO (Asynchronous IO)
阻塞 IO (BIO)
- 用户应用请求内核是否有新的网络数据
- 如果没有数据,就阻塞直到有数据到来
- 等待内核将数据拷贝到用户空间
- 用户应用处理数据
阻塞 IO (BIO) 在请求内核数据的时候,没有数据就会一直阻塞到获取数据。
非阻塞 IO (NIO)
- 用户应用请求内核是否有新的网络数据
- 如果没有数据,内核直接返回 error ,用户就知道数据没有准备好,隔一段时间再来请求
- 直到有一次请求,内核数据已经准备好
- 数据从内核态拷贝到用户态进行处理
非阻塞 IO (NIO) 在等待内核数据的时候,采用轮询的方式替代阻塞
在 NIO 模式中,一切都是非阻塞的
accept()
方法是非阻塞的:如果没有客户端连接,就返回无连接标识read()
方法也是非阻塞的:如果没有数据就返回无数据标识;如果有数据只阻塞read()
读取数据的时间
在 NIO 模式中,当一个客户端和服务端连接时,会把 socket
加入到一个数组中,隔一段时间遍历一次,检查这个 socket
是否能读到数据,这样一个线程就能处理多个客户端的连接和读取了
异步 IO (AIO)
异步 IO(AIO) 首先是非阻塞 IO ,区别在于成功标志的时机。
异步 IO 连内核到用户态的数据拷贝都是异步的,直到数据拷贝完成,才会回调一个信号,通知一切已经准备完成。用户应用就可以直接处理结果了。
IO 多路复用
NIO 解决了 BIO 多个连接需要耗费多个线程来处理的问题, NIO 中一个线程就可以处理多个连接,但还是存在性能问题:
- 当客户端很多时,比如有 1 万个客户端,那么每次循环就需要遍历 1 万个
socket
,即使也许其中只有10
个socket
有数据 - 遍历过程是在用户态,需要调用内核态的
read()
方法判断是否有数据,涉及到用户态和内核态的切换
IO 多路复用可以解决上述问题:将一批文件描述符 FD 一次传给内核,由内核去遍历处理,不再两态转换,而是直接从内核获得结果。
1. select
select
是最早的 IO 多路复用实现方案
// select 函数,用于监听多个 fd 的集合
int select(
int nfds, //要监听的 fd_set 的最大值 + 1
fd_set *readfds, //要监听读事件的 fd 集合
fd_set *writefds, //要监听写事件的 fd 集合
fd_set *exceptfds, //要监听异常事件的 fd 集合
struct timeval *timeout //超时时间
)
select 函数执行流程
- 用户空间创建
fd_set
集合,把需要监听的位置置为 1 - 用户空间拷贝
fd_set
集合到内核空间 - 内核空间遍历
fd_set
集合,对每个fd
检查是否处于就绪状态,如果没有fd
就绪,且timeout
未到期,则线程进入睡眠状态- 有
fd
就绪,内核唤醒进程,进程处理(有事件位置保留1,没有事件位置置为 0) - 超时时间到达,
select()
函数返回结果 -1
- 有
- 内核处理结束后,将修改后的
fd_set
集合(仅保留就绪的fd
)拷贝回用户空间 - 用户空间检查
fd_set
集合,根据就绪的fd
进行read()
等IO操作
select 函数缺点
- FD 上限为
FD_SETSIZE
,默认为1024
,修改需重新编译内核 - 每次调用
select()
都需要两次数据拷贝和线性遍历所有 FD
2. poll
poll
使用 pollfd
数组突破了 select 模式 1024
个 FD 的限制,但本质性能问题还是没有解决
int poll(
struct pollfd *fds, // pollfd 数组,可自定义大小
nfds_t nfds, //数组元素个数
int timeout //超时时间
)
3. epoll
epoll 是当前最先进的多路复用实现方案,但只能在 Linux 系统中使用,不同于 select
、poll
的轮询机制,epoll
采用的是事件驱动机制,每个 fd 上注册有回调函数,当网卡接收到数据时,会调用回调函数,同时将该 fd 放入到就绪列表 ,调用 epoll_wait
检查是否有事件发生时,不用遍历所有 FD,而只用遍历 rdlist
就绪列表
struct eventpoll {
struct rb_root rbr; //一颗红黑树,记录要监听的 FD
struct list_head rdlist; //一个链表,记录就绪的 FD
}
// 1. 在内核创建 eventpoll 结构体,返回对应的句柄
// 注意这个 size 只是给内核的一个建议
int epoll_create(int size);
// 2. 将一个 FD 添加到 epoll 红黑树中,并设置 ep_poll_callback
// callback 触发时,就把对应的 FD 加入到 rdlist 这个就绪队列中
int epoll_ctl(int epfd, // eventpoll 实例的句柄
int op, //操作标识 包含 添加、删除、修改
int fd, //需要监听的 fd
struct epoll_event *event //要监听的事件类型:读、写、异常
);
// 3. 监听 rdlist 这个就绪队列是否为空,不为空则返回就绪的 FD 数量
int epoll_wait(int epfd, // eventpoll 实例的句柄
struct epoll_event *events, //返回结果,内核返回的就绪的 FD
int maxevents, //events 数组的最大长度
int timeout //超时时间
);
epoll 的执行流程
- 调用
epoll_create
创建一个eventpoll
结构体,包含一个监听事件红黑树和一个就绪链表
- 调用
epoll_ctl
向eventpoll
中注册一个监听的 fd ,并且注册 fd 对应的回调函数
- 调用
epoll_wait
开始阻塞等待事件到来 - 内核将监听到的事件添加一份到就绪链表
list_head
- 内核唤醒用户线程,并将就绪链表拷贝到用户空间
- 用户应用只需要关心就绪的 fd 事件,取出结构体里关联的回调函数进行回调处理即可
Redis 网络模型
为什么在 Windows 上部署 Redis 无法完全发挥性能?
因为只有 Linux 系统才支持 epoll
模型(时间复杂度O(1)
) ,Redis 在 Windows 上使用的是 select
模型 (时间复杂度O(n)
)
IO 多路复用与 Reactor 模式的关系
- Reactor 是一种设计模式,它基于事件驱动
- IO 多路复用是操作系统提供的机制
Reactor 模式通常依赖于操作系统提供的 IO 多路复用机制,实现对多个事件源的监听,从而构建高效的事件驱动型应用程序。
Redis 线程模型演进
最开始学习 Redis 时,那时候 Redis 版本为 3.0 ,经常听说 Redis 为单线程模型,但现在这个说法已不再准确
Redis 4.0之前为什么一直采用单线程?
- CPU 并不是您使用 Redis 的瓶颈,Redis 的瓶颈通常受内存限制或网络限制。
- 单线程模型使 Redis 的开发和维护更加简单,不易出错
- 单线程模型也能并发地处理多客户端的请求,因为使用的是 IO 多路复用 (单
Reactor
单线程模型)
Redis 4.0 开始,对键的读写的工作线程仍然是单 Reactor
单线程模式,但持久化和异步删除任务已经开始引入后台线程实现,防止阻塞工作线程
真正多线程登场
随着网络硬件的提升,Redis 的性能瓶颈有时出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。于是 Redis 6.0 ,引入了 Reactor
多线程模型,对客户端的读取请求、解析命令、响应数据都交给了 IO 线程。而真正执行命令还是和之前的单线程模型一致,在主线程上完成的。