深度解析单线程的 Redis 如何做到每秒数万 QPS 的超高处理能力!
大家好,我是飞哥!
今天开篇先给大家讲个飞哥自己的小故事。我在学校和刚毕业头一年主要从事的客户端开发,那时候对服务器端编程还不擅长。
有一次去面试服务器端岗位,面试官问我有一个连接过来,你该怎么编程处理它。我答道:“主线程收到请求后,创建一个子线程处理。”
面试官接着问,那如果有一千个连接同时来呢?我说“那就多创建一点线程,搞个线程池”。面试官继续追问如果一万个呢?我答道:“......不会...”。
事实上,服务器端只需要单线程可以达到非常高的处理能力,Redis 就是一个非常好的例子。仅仅靠单线程就可以支撑起每秒数万 QPS
的高处理能力。今天我们就来带大家看看 Redis 核心网络模块的内部实现,学习下 Redis 是如何做到如此的高性能的!
一、理解多路复用原理
在开始介绍 Redis 之前,我想有必要先来简单介绍下 epoll。
在传统的同步阻塞网络编程模型里(没有协程以前),性能上不来的根本原因在于进程线程都是笨重的家伙。让一个进(线)程只处理一个用户请求确确实实是有点浪费了。
先抛开高内存开销不说,在海量的网络请求到来的时候,光是频繁的进程线程上下文就让 CPU 疲于奔命了。
如果把进程比作牧羊人,一个进(线)程同时只能处理一个用户请求,相当于一个人只能看一只羊,放完这一只才能放下一只。如果同时来了 1000 只羊,那就得
1000 个人去放,这人力成本是非常高的。
性能提升思路很简单,就是让很多的用户连接来复用同一个进(线)程,这就是多路复用。多路指的是许许多多个用户的网络连接。复用指的是对进(线)程的复用。换到牧羊人的例子里,就是一群羊只要一个牧羊人来处理就行了。
不过复用实现起来是需要特殊的 socket 事件管理机制的,最典型和高效的方案就是 epoll。放到牧羊人的例子来,epoll
就相当于一只牧羊犬。
在 epoll 的系列函数里, epoll_create 用于创建一个 epoll 对象,epoll_ctl 用来给 epoll 对象添加或者删除一个
socket。epoll_wait 就是查看它当前管理的这些 socket 上有没有可读可写事件发生。
当网卡上收到数据包后,Linux 内核进行一系列的处理后把数据放到 socket 的接收队列。然后会检查是否有 epoll 在管理它,如果是则在
epoll 的就绪队列中插入一个元素。epoll_wait 的操作就非常的简单了,就是到 epoll 的就绪队列上来查询有没有事件发生就行了。关于 epoll
这只“牧羊犬”的工作原理参见深入揭秘 epoll 是如何实现 IO 多路复用的 (Javaer 习惯把基于 epoll 的网络开发模型叫做 NIO)
在基于 epoll 的编程中,和传统的函数调用思路不同的是,我们并不能主动调用某个 API
来处理。因为无法知道我们想要处理的事件啥时候发生。所以只好提前把想要处理的事件的处理函数注册到一个事件分发器上去。当事件发生的时候,由这个事件分发器调用回调函数进行处理。这类基于实现注册事件分发器的开发模式也叫
Reactor 模型。
二、Redis 服务启动初始化
理解了 epoll 原理后,我们再来实际看 Redis 具体是如何使用 epoll 的。直接在 Github 上就可以非常方便地获取 Redis
的源码。我们切到 5.0.0 版本来看单线程版本的实现(多线程我们改天再讲)。
# git clone https://github.com/redis/redis
# cd redis
# git checkout -b 5.0.0 5.0.0
其中整个 Redis 服务的代码总入口在 src/server.c 文件中,我把入口函数的核心部分摘了出来,如下。
//file: src/server.c
int main(int argc, char **argv) {
......
// 启动初始化
initServer();
// 运行事件处理循环,一直到服务器关闭为止
aeMain(server.el);
}
其实整个 Redis 的工作过程,就只需要理解清楚 main 函数中调用的 initServer 和 aeMain 这两个函数就足够了。
本节中我们重点介绍 initServer,在下一节介绍事件处理循环 aeMain。在 initServer 这个函数内,Redis
做了这么三件重要的事情。
创建一个 epoll 对象对配置的监听端口进行 listen把 listen socket 让 epoll 给管理起来//file: src/server.c
void initServer() {
// 2.1.1 创建 epoll
server.el = aeCreateEventLoop(server.maxclients CONFIG_FDSET_INCR);
// 2.1.2 绑定监听服务端口
listenToPort(server.port,server.ipfd,