Skip to content

Redis 网络模型

字数: 0 字 时长: 0 分钟

网络编程模型

在 《UNIX网络编程》 一书中,总结归纳了 5 种 IO 模型:

  • 阻塞 IO (Blocking IO)
  • 非阻塞 IO (Non-Blocking IO)
  • IO 多路复用 (IO Multiplexing)
  • 信号驱动 IO (Signal-Driven IO)
  • 异步 IO (Asynchronous IO)

Unix网络模型.webp

阻塞 IO (BIO)

阻塞 IO 模型.webp

  1. 用户应用请求内核是否有新的网络数据
  2. 如果没有数据,就阻塞直到有数据到来
  3. 等待内核将数据拷贝到用户空间
  4. 用户应用处理数据

阻塞 IO (BIO) 在请求内核数据的时候,没有数据就会一直阻塞到获取数据

非阻塞 IO (NIO)

非阻塞IO模型.webp

  1. 用户应用请求内核是否有新的网络数据
  2. 如果没有数据,内核直接返回 error ,用户就知道数据没有准备好,隔一段时间再来请求
  3. 直到有一次请求,内核数据已经准备好
  4. 数据从内核态拷贝到用户态进行处理

非阻塞 IO (NIO) 在等待内核数据的时候,采用轮询的方式替代阻塞

在 NIO 模式中,一切都是非阻塞的

  • accept() 方法是非阻塞的:如果没有客户端连接,就返回无连接标识
  • read() 方法也是非阻塞的:如果没有数据就返回无数据标识;如果有数据只阻塞 read() 读取数据的时间

在 NIO 模式中,当一个客户端和服务端连接时,会把 socket 加入到一个数组中,隔一段时间遍历一次,检查这个 socket 是否能读到数据,这样一个线程就能处理多个客户端的连接和读取了

异步 IO (AIO)

异步 IO .webp

异步 IO(AIO) 首先是非阻塞 IO ,区别在于成功标志的时机。

异步 IO 连内核到用户态的数据拷贝都是异步的,直到数据拷贝完成,才会回调一个信号,通知一切已经准备完成。用户应用就可以直接处理结果了。

IO 多路复用

NIO 解决了 BIO 多个连接需要耗费多个线程来处理的问题, NIO 中一个线程就可以处理多个连接,但还是存在性能问题:

  1. 当客户端很多时,比如有 1 万个客户端,那么每次循环就需要遍历 1 万个 socket,即使也许其中只有 10socket 有数据
  2. 遍历过程是在用户态,需要调用内核态的 read() 方法判断是否有数据,涉及到用户态和内核态的切换

IO 多路复用可以解决上述问题:将一批文件描述符 FD 一次传给内核,由内核去遍历处理,不再两态转换,而是直接从内核获得结果。

IO多路复用.webp

1. select

select 是最早的 IO 多路复用实现方案

java
// 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.webp

select 函数执行流程

  1. 用户空间创建 fd_set 集合,把需要监听的位置置为 1
  2. 用户空间拷贝 fd_set 集合到内核空间
  3. 内核空间遍历 fd_set 集合,对每个 fd 检查是否处于就绪状态,如果没有 fd 就绪,且 timeout 未到期,则线程进入睡眠状态
    • fd 就绪,内核唤醒进程,进程处理(有事件位置保留1,没有事件位置置为 0)
    • 超时时间到达,select() 函数返回结果 -1
  4. 内核处理结束后,将修改后的 fd_set 集合(仅保留就绪的 fd)拷贝回用户空间
  5. 用户空间检查 fd_set 集合,根据就绪的 fd 进行 read() 等IO操作

select 函数缺点

  • FD 上限为 FD_SETSIZE,默认为 1024 ,修改需重新编译内核
  • 每次调用 select() 都需要两次数据拷贝和线性遍历所有 FD

2. poll

poll 使用 pollfd 数组突破了 select 模式 1024 个 FD 的限制,但本质性能问题还是没有解决

java
int poll(
    struct pollfd *fds, // pollfd 数组,可自定义大小
    nfds_t nfds, //数组元素个数
    int timeout //超时时间
)

3. epoll

epoll 是当前最先进的多路复用实现方案,但只能在 Linux 系统中使用,不同于 selectpoll 的轮询机制,epoll采用的是事件驱动机制,每个 fd 上注册有回调函数,当网卡接收到数据时,会调用回调函数,同时将该 fd 放入到就绪列表 ,调用 epoll_wait 检查是否有事件发生时,不用遍历所有 FD,而只用遍历 rdlist 就绪列表

java
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 的执行流程

  1. 调用 epoll_create 创建一个 eventpoll 结构体,包含一个监听事件红黑树和一个就绪链表

epoll1.webp

  1. 调用 epoll_ctleventpoll 中注册一个监听的 fd ,并且注册 fd 对应的回调函数

epoll2.webp

  1. 调用 epoll_wait 开始阻塞等待事件到来
  2. 内核将监听到的事件添加一份到就绪链表 list_head

epoll3.webp

  1. 内核唤醒用户线程,并将就绪链表拷贝到用户空间

epoll4.webp

  1. 用户应用只需要关心就绪的 fd 事件,取出结构体里关联的回调函数进行回调处理即可

Redis 网络模型

为什么在 Windows 上部署 Redis 无法完全发挥性能?

因为只有 Linux 系统才支持 epoll 模型(时间复杂度O(1)) ,Redis 在 Windows 上使用的是 select 模型 (时间复杂度O(n)

IO 多路复用与 Reactor 模式的关系

  • Reactor 是一种设计模式,它基于事件驱动
  • IO 多路复用是操作系统提供的机制

Reactor 模式通常依赖于操作系统提供的 IO 多路复用机制,实现对多个事件源的监听,从而构建高效的事件驱动型应用程序。

Redis 线程模型演进

Redis版本迭代.webp

最开始学习 Redis 时,那时候 Redis 版本为 3.0 ,经常听说 Redis 为单线程模型,但现在这个说法已不再准确

Redis单线程.webp

Redis 4.0之前为什么一直采用单线程

  • CPU 并不是您使用 Redis 的瓶颈,Redis 的瓶颈通常受内存限制或网络限制。
  • 单线程模型使 Redis 的开发和维护更加简单,不易出错
  • 单线程模型也能并发地处理多客户端的请求,因为使用的是 IO 多路复用 (单 Reactor 单线程模型)

Redis 4.0 开始,对键的读写的工作线程仍然是单 Reactor 单线程模式,但持久化和异步删除任务已经开始引入后台线程实现,防止阻塞工作线程

真正多线程登场

随着网络硬件的提升,Redis 的性能瓶颈有时出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。于是 Redis 6.0 ,引入了 Reactor 多线程模型,对客户端的读取请求、解析命令、响应数据都交给了 IO 线程。而真正执行命令还是和之前的单线程模型一致,在主线程上完成的。

Redis多线程.webp