【Redis源码系列】Redis服务启动流程分析--超详细逐行分析

1,094 阅读3分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

经过多年的发展, Redis已经经历了6个大版本, 6.0系列也增加了很多新特性, 如果IO的多线程处理等。同时源码依旧保持了非常高的水准, 简洁的目录布局以及清晰地文档注释和代码结构, 让人可以非常愉快的解读开发者的意图以及解决方案, 相比php的源码则背负了沉重的历史包袱, 话不多说, 手撕代码。

  • 源码版本: 6.0.14

1. 测试用例

根目录下的 ./src/server.c 为服务的启动文件, 启动后首先判断是否为测试启动, 根据参数不同的测试方法,如图示可以非常方便找到 ziplist.c, quicklist.c 下面对应的方法 image.png

2. 环境和资源初始化

image.png

其中OOM的handler函数处理如下:

image.png 逻辑为打印日志 & 退出服务。

3. 初始化服务配置

调用 void initServerConfig(void) 方法 初始化 server 对象的默认字段值, 比较重要的如: image.png

4. 初始化ACL

  • ACL: 根据源码注释翻译: 必须尽快初始化ACL子系统,因为基本网络代码和客户端创建依赖于ACL,关于ACL网络库备注: www.oschina.net/p/acl
  • moduleInitModulesSystem: 初始化模块系统, 包括设置键位通知订户列表和静态客户端, 设置命令过滤列表,创建计时器基数树,初始化事件监听列表等

image.png

5. 运行参数内存转移

将可执行路径和参数按顺序存放在安全的地方,以便稍后能够重新启动服务器, 可见高可用的思想在细节处的体现:

image.png

6. 初始化哨兵配置

image.png

7. 命令行和配置解析

image.png

8. 初始化服务

  • 注册信号量相关函数
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
makeThreadKillable();
  • 初始化服务器配置
server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
server.hz = server.config_hz;
server.pid = getpid();
server.in_fork_child = CHILD_TYPE_NONE;
server.main_thread_id = pthread_self();
server.current_client = NULL;
server.fixed_time_expire = 0;
...
  • 创建全局共享内存对象, 并且尽可能大的提高主服务进程可用文件描述符数量, 应为除了 maxclient 参数确定的最大可操作文件描述符之外,服务还需要其他的文件描述符, 如: 持久化、监听套接字、日志文件等
// 创建全局对象
createSharedObjects();
    shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
    shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
    ...
// 调整文件打开数量限制
// 首先尝试读取独夫妻设定的文件数量
// 如果失败则使用: 1024 - 配置的最小服务需求fd数量(默认32)
// 否则根据如下方法计算并设定
adjustOpenFilesLimit();
    ...
    while(bestlimit > oldlimit) {
        rlim_t decr_step = 16;

        limit.rlim_cur = bestlimit;
        limit.rlim_max = bestlimit;
        if (setrlimit(RLIMIT_NOFILE,&limit) != -1) break;
        setrlimit_error = errno;

        /* We failed to set file limit to 'bestlimit'. Try with a
         * smaller limit decrementing by a few FDs per iteration. */
        if (bestlimit < decr_step) break;
        bestlimit -= decr_step;
    }
    ...
  • 创建事件监听轮训: aeCreateEventLoop, 事件相关的操作这里简单待过, 下篇文章详细解读Redis的事件机制
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
...
  • 重点: 创建socket, 并且监听。服务会监听两个配置的端口, 一个是服务端口, 一个是tls端口, 如果有配置或者初始化值, 则做监听操作, 调用同样的函数: listenToPort, listenToPort中会做一些IPv6的特殊操作处理, 其中比较重要的方法是: anetTcpServer, 此方法原型为:
int anetTcpServer(char *err, int port, char *bindaddr, int backlog)
{
    return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}

跟踪此函数调用, 最终进入: anetListen 进入我们熟悉的bind, listen系列函数, 完成socket的创建, 监听和绑定操作:

static int anetListen(char *err, int s, struct sockaddr *sa, socklen_t len, int backlog) {
    // 熟悉的绑定操作
    if (bind(s,sa,len) == -1) {
        anetSetError(err, "bind: %s", strerror(errno));
        close(s);
        return ANET_ERR;
    }
    
    // 熟悉的监听操作
    if (listen(s, backlog) == -1) {
        anetSetError(err, "listen: %s", strerror(errno));
        close(s);
        return ANET_ERR;
    }
    return ANET_OK;
}

监听完成后, 会通过循环初始化每一个DB库对象, struct redisServer 中的*db字段是一个redisDb类型的指针数组, 然后依旧是server对象的参数赋值,在此不展示代码, 有兴趣的同学可以自己调试查看, 完成后会创建aE时间时间和aE文件时间, 分别调用: aeCreateFileEvent, aeCreateFileEvent,同属于时间相关Api, 下一篇重点介绍, 接下来就是熟悉的Accept

# 先调用:
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) 

# 步进: 
int anetTcpAccept(char *err, int s, char *ip, size_t ip_len, int *port) {
    ...
    static int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len)
    ...
}

# 步进:
int fd;
    while(1) {
        fd = accept(s,sa,len);
        if (fd == -1) {
            if (errno == EINTR)
                continue;
            else {
                anetSetError(err, "accept: %s", strerror(errno));
                return ANET_ERR;
            }
        }
        break;
    }
    return fd;

至此完成服务的socket 创建, 绑定, 监听, accept操作。

  • 最后一步是初始化其他操作,解释如下:
# 初始化脚本缓存
replicationScriptCacheInit();
# 初始化lua 脚本运行环境
scriptingInit(1);
# 初始化慢日志
slowlogInit();
# 初始化延时监控
latencyMonitorInit();

至此, initServer 处理完成。

9. 其他初始化或者检查操作

// 熟悉的redis ACSII 字符画, 字符画变量定义: ascii_logo
void redisAsciiArt(void)
// 检查tcp-backlog参数设置, 也是一个比较重要的参数设置, 不同的场景可以有优化设置
void checkTcpBacklogSettings(void)

// 接下来是针对各个平台的不同设置或者初始化操作, 在此只解读linux下的操作:
moduleLoadFromQueue();
ACLLoadUsersAtStartup();
InitServerLast();
loadDataFromDisk();

// 设置cpu亲和性
redisSetCpuAffinity();
// 设置OOM score
setOOMScoreAdj();

10. 启用事件处理器, 轮训事件并处理

事件机制比较重要, 内容也比较多, 同理, 我们下一篇单独解读Redis事件机制。


aeMain(server.el):
    // 函数实现
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
    
aeDeleteEventLoop(server.el);
    // 函数实现
    aeApiFree(eventLoop);
    zfree(eventLoop->events);
    zfree(eventLoop->fired);

    /* Free the time events list. */
    aeTimeEvent *next_te, *te = eventLoop->timeEventHead;
    while (te) {
        next_te = te->next;
        zfree(te);
        te = next_te;
    }
    zfree(eventLoop);
// 结束
return 0;

End

至此Redis的启动流程解读完成, 可以看到Redis对处理很多操作系统细节方面做的非常到位, 非常值得我们学习, 下一篇我们开始分析Redis的时间和线程机制,希望各位看官给一个小小的赞, 一起进步 :)