解析单线程 Redis如何扛起每秒百万并发(一)

一、理解多路复用原理

最近,项目工作中用到了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 的编程中,和传统的函数调用思路不同的是,我们并不能主动调用某个 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 这个函数内,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,&server.ipfd_count);
     
        // 2.1.3 注册 accept 事件处理器
        for (j = 0; j < server.ipfd_count; j++) {
            aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
                acceptTcpHandler,NULL);
        }
        ...
    }
    

    2.1 创建 epoll 对象

    本小节的逻辑看起来貌似不短,但其实只是创建了一个 epoll 对象出来而已。
    创建 epoll 对象的逻辑在 aeCreateEventLoop 中,在创建完后,Redis 将其保存在 redisServer 的 aeEventLoop 成员中,以备后续使用。
    struct redisServer {
        ...
        aeEventLoop *el;
    }
    
    aeCreateEventLoop 详细逻辑,Redis 在操作系统提供的 epoll 对象基础上又封装了一个 eventLoop 出来,所以创建的时候是先申请和创建 eventLoop。
//file:src/ae.c
aeEventLoop *aeCreateEventLoop(int setsize) {
    aeEventLoop *eventLoop;
    eventLoop = zmalloc(sizeof(*eventLoop);
 
    //将来的各种回调事件就都会存在这里
    eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
    ......
 
    aeApiCreate(eventLoop);
    return eventLoop;
}

在 eventLoop 里,注意一下 eventLoop->events,将来在各种事件注册的时候都会保存到这个数组里。

//file:src/ae.h
typedef struct aeEventLoop {
    ......
    aeFileEvent *events; /* Registered events */
}

具体创建 epoll 的过程在 ae_epoll.c 文件下的 aeApiCreate 中。在这里,真正调用了 epoll_create :

//file:src/ae_epoll.c
static int aeApiCreate(aeEventLoop *eventLoop) {
    aeApiState *state = zmalloc(sizeof(aeApiState));
    state->epfd = epoll_create(1024); 
    eventLoop->apidata = state;
    return 0;
}

2.2 绑定监听服务端口

再来看 Redis 中的 listen 过程,它在 listenToPort 函数中。虽然调用链条很长,但其实主要就是执行了个简单 listen 而已。

//file: src/redis.c
int listenToPort(int port, int *fds, int *count) {
    for (j = 0; j < server.bindaddr_count || j == 0; j++) {
        fds[*count] = anetTcpServer(server.neterr,port,NULL,
                server.tcp_backlog);
    }
}

Redis 是支持开启多个端口的,所以在 listenToPort 中我们看到是启用一个循环来调用 anetTcpServer。在 anetTcpServer 中,逐步会展开调用,直到执行到 bind 和 listen 系统调用。

//file:src/anet.c
int anetTcpServer(char *err, int port, char *bindaddr, int backlog)
{
    return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}
static int _anetTcpServer(......)
{
    // 设置端口重用
    anetSetReuseAddr(err,s)
    // 监听
    anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog)
}
static int anetListen(......) {
    bind(s,sa,len);
    listen(s, backlog);
    ......
}

2.3 注册事件回调函数

回头再看一下 initServer,它调用 aeCreateEventLoop 创建了 epoll,调用 listenToPort 进行了服务端口的 bind 和 listen。接着就开始调用 aeCreateFileEvent 来注册一个 accept 事件处理器。

//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,&server.ipfd_count);
 
    // 2.1.3 注册 accept 事件处理器
    for (j = 0; j < server.ipfd_count; j++) {
        aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL);
    }
    ...
}

注意看调用 aeCreateFileEvent 时传的重要参数是 acceptTcpHandler,它表示将来在 listen socket 上有新用户连接到达的时候,该函数将被调用执行。我们来看 aeCreateFileEvent 具体代码。

//file: src/ae.c
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    // 取出一个文件事件结构
    aeFileEvent *fe = &eventLoop->events[fd];
 
    // 监听指定 fd 的指定事件
    aeApiAddEvent(eventLoop, fd, mask);
 
    // 设置文件事件类型,以及事件的处理器
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
 
    // 私有数据
    fe->clientData = clientData;
}

函数 aeCreateFileEvent 一开始,从 eventLoop->events 获取了一个 aeFileEvent 对象。在 2.1 中我们介绍过 eventLoop->events 数组,注册的各种事件处理器会保存在这个地方。
接下来调用 aeApiAddEvent。这个函数其实就是对 epoll_ctl 的一个封装。主要就是实际执行 epoll_ctl EPOLL_CTL_ADD。

//file:src/ae_epoll.c
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    // add or mod
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;
    ......
 
    // epoll_ctl 添加事件
    epoll_ctl(state->epfd,op,fd,&ee);
    return 0;
}

每一个 eventLoop->events 元素都指向一个 aeFileEvent 对象。在这个对象上,设置了三个关键东西:

  • rfileProc:读事件回调
  • wfileProc:写事件回调
  • clientData:一些额外的扩展数据
    将来 当 epoll_wait 发现某个 fd 上有事件发生的时候,这样 redis 首先根据 fd 到 eventLoop->events 中查找 aeFileEvent 对象,然后再看 rfileProc、wfileProc 就可以找到读、写回调处理函数。
    回头看 initServer 调用 aeCreateFileEvent 时传参来看。
//file: src/server.c
void initServer() {
    ......
    // 2.1.3 注册 accept 事件处理器
    for (j = 0; j < server.ipfd_count; j++) {
        aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL);
    }
}

listen fd 对应的读回调函数 rfileProc 事实上就被设置成了 acceptTcpHandler,写回调没有设置,私有数据 client_data 也为 null。

三、高性能Redis服务启动网络原理总结

在 initServer 这个函数内,Redis 做了这么三件重要的事情。

  • 创建一个 epoll 对象
  • 对配置的监听端口进行 listen
  • 把 listen socket 让 epoll 给管理起来


参考链接:
推荐一个零声学院后台服务器免费公开课,个人觉得老师讲得不错,分享给大家:
https://course.0voice.com/v1/course/intro?courseId=5&agentId=0
后台服务器

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring4GWT GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet.Applet 简单实现!~ 网页表格组件 GWT Advanced Table GWT Advanced Table 是一个基于 GWT 框架的网页表格组件,可实现分页数据显示、数据排序和过滤等功能! Google Tag Library 该标记库和 Google 有关。使用该标记库,利用 Google 为你的网站提供网站查询,并且可以直接在你的网页里面显示搜查的结果。 github-java-api github-java-api 是 Github 网站 API 的 Java 语言版本。 java缓存工具 SimpleCache SimpleCache 是一个简单易用的java缓存工具,用来简化缓存代码的编写,让你摆脱单调乏味的重复工作!1. 完全透明的缓存支持,对业务代码零侵入 2. 支持使用Redis和Memcached作为后端缓存。3. 支持缓存数据分区规则的定义 4. 使用redis作缓存时,支持list类型的高级数据结构,更适合论坛帖子列表这种类型的数据 5. 支持混合使用redis缓存和memcached缓存。可以将列表数据缓存到redis中,其他kv结构数据继续缓存到memcached 6. 支持redis的主从集群,可以做读写分离。缓存读取自redis的slave节点,写入到redis的master节点。 Java对象的SQL接口 JoSQL JoSQL(SQLforJavaObjects)为Java开发者提供运用SQL语句来操作Java对象集的能力.利用JoSQL可以像操作数据库中的数据一样对任何Java对象集进行查询,排序,分组。 搜索自动提示 Autotips AutoTips是为解决应用系统对于【自动提示】的需要(如:Google搜索), 而开发的架构无关的公共控件, 以满足该类需求可以通过快速配置来开发。AutoTips基于搜索引擎Apache Lucene实现。AutoTips提供统一UI。 WAP浏览器 j2wap j2wap 是一个基于Java的WAP浏览器,目前处于BETA测试阶段。它支持WAP 1.2规范,除了WTLS 和WBMP。 Java注册表操作类 jared jared是一个用来操作Windows注册表的 Java 类库,你可以用来对注册表信息进行读写。 GIF动画制作工具 GiftedMotion GiftedMotion是一个很小的,免费而且易于使用图像互换格式动画是能够设计一个有趣的动画了一系列的数字图像。使用简便和直截了当,用户只需要加载的图片和调整帧您想要的,如位置,时间显示和处理方法前帧。 Java的PList类库 Blister Blister是一个用于操作苹果二进制PList文件格式的Java开源类库(可用于发送数据给iOS应用程序)。 重复文件检查工具 FindDup.tar FindDup 是一个简单易用的工具,用来检查计算机上重复的文件。 OpenID的Java客户端 JOpenID JOpenID是一个轻级的OpenID 2.0 Java客户端,仅50KB+(含源代码),允许任何Web网站通过OpenID支持用户直接登录而无需注册,例如Google Account或Yahoo Account。 JActor的文件持久化组件 JFile JFile 是 JActor 的文件持久化组件,以及一个高吞吐的可靠事务日志组件。 Google地图JSP标签库 利用Google:maps JSP标签库就能够在你的Web站点上实现GoogleMaps的所有功能而且不需要javascript或AJAX编程。它还能够与JSTL相结合生成数据库驱动的动态Maps。 OAuth 实现框架 Agorava Agorava 是一个实现了 OAuth 1.0a 和 OAuth 2.0 的框架,提供了简单的方式通过社交媒体进行身份认证的功能。 Eclipse的JavaScript插件 JSEditor JSEditor 是 Eclipse 下编辑 JavaScript 源码的插件,提供语法高亮以及一些通用的面向对象方法。 Java数据库连接池 BoneCP BoneCP 是一个高性能的开源java数据库连接池实现库。它的设计初衷就是为了提高数据库连接池的性能,根据某些测试数据发现,BoneCP是最快的连接池。BoneCP很小,只有四十几K

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值