Redis Server 启动

Redis整体架构

Redis是典型的 Client-Server 架构,类似于MySQL和RPC框架。
Redis Server类似于RPC框架Server,在客户端使用前需要先启动Redis Server。那么有一个问题,Redis Server启动后会做哪些操作?需要哪些配置,有没有什么坑?

(1) Redis Server启动入口-main函数

源码地址 https://github.com/redis/redis/blob/6.0/src/server.c

//file: src/server.c

// #L5297  5297行
int main(int argc, char **argv) {

    // 1. 初始化配置 设置默认值
    initServerConfig();

    /* We need to init sentinel right now as parsing the configuration file
     * in sentinel mode will have the effect of populating the sentinel
     * data structures with master nodes to monitor. */
    // 2. 判断server是否设置为哨兵模式 
    if (server.sentinel_mode) {
        initSentinelConfig();
        // 初始化哨兵
        initSentinel();
    }

    /* Check if we need to start in redis-check-rdb/aof mode. We just execute
     * the program main. However the program is part of the Redis executable
     * so that we can easily execute an RDB check on loading errors. */
    // 3. 持久化 rdb aof  
    if (strstr(argv[0],"redis-check-rdb") != NULL)
        redis_check_rdb_main(argc,argv,NULL);
    else if (strstr(argv[0],"redis-check-aof") != NULL)
        redis_check_aof_main(argc,argv);

    // 4. 加载Server配置 
    loadServerConfig(configfile,options);

    // 5. 启动RedisServer时 初始化配置及资源
    initServer();

    // 6. 循环处理请求及事件,直到服务器关闭为止
    aeMain(server.el); 

}

(1.1) 部分细节

// 设置时区
setlocale(LC_COLLATE,"");
tzset(); /* Populates 'timezone' global. */

// 
zmalloc_set_oom_handler(redisOutOfMemoryHandler);

// 
uint8_t hashseed[16];
getRandomBytes(hashseed,sizeof(hashseed));

// 省略部分代码...

// 哨兵
server.sentinel_mode = checkForSentinelMode(argc,argv);

(2) Redis主要参数类型

Redis 运行所需的各种参数,都统一定义在了server.h文件的 redisServer 结构体中。

各种参数划分为了七大类型,包括 通用参数、数据结构参数、网络参数、持久化参数、主从复制参数、切片集群参数、性能优化参数。

参数类型 作用
通用参数 Redis Server运行基本配置
数据结构参数 定义内存紧凑型数据结构使用条件
网络参数 定义了Server监听地址、端口、TCP缓冲区大小等参数
持久化参数 定义了RDB和AOF运行时需要的参数
主从复制参数 定义了主从库复制时的关键机制,包括主库信息,主从复制方式、主从复制积压缓冲区大小、从库是否只读等
切片集群参数 定义了切片集群运行时的关键机制,包括是否启动切片集群、切片洁群是否支持故障结合、集群节点心跳超时时间等
性能优化参数 包括2方面内容 \n 一方面是Server运行时的监控机制,比如慢查询触发条件、延迟监控的触发阈值等;、\n 另一方面主要是和Redis提供的优化机制相关,比如惰性删除机制、主动内存碎片整理机制等。

(2.1) Redis参数的设置方法

Redis 对运行参数的设置实际上会经过三轮赋值,分别是默认配置值、命令行启动参数,以及配置文件配置值。

第一轮,Redis 在 main 函数中会先调用 initServerConfig 函数,为各种参数设置默认值。
第二轮,main 函数就会对 Redis 程序启动时的命令行参数进行逐一解析。调用 loadServerConfig 函数进行第二轮赋值。


(3) mian方法-初始化RedisServer

初始化RedisServer initServer

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

(3.1) initServer-创建DB

// file: server.c

void initServer(void) {

    // 创建db
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);

    /* Create the Redis databases, and initialize other internal state. */
    for (j = 0; j < server.dbnum; j++) {
        // 创建当前db的字典 (哈希表)
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        // 创建当前db的过期key哈希表
        server.db[j].expires = dictCreate(&keyptrDictType,NULL);
        // 过期键游标
        server.db[j].expires_cursor = 0;
        // 为被BLPOP阻塞的key创建哈希表
        server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
        // 为将执行PUSH的阻塞key创建哈希表
        server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
        // 为被MULTI/WATCH操作监听的key创建哈希表
        server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].id = j;
        server.db[j].avg_ttl = 0;
        server.db[j].defrag_later = listCreate();
        listSetFreeMethod(server.db[j].defrag_later,(void (*)(void*))sdsfree);
    }

}

(3.2) initServer-事件处理

RedisServer启动initServer流程

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

// file: server.c

void initServer(void) {

    // 1.  创建epoll
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);

    // 2. 监听端口
    listenToPort(server.port,server.ipfd,&server.ipfd_count)

    // 3. 创建时间处理器 事件处理器 

    // 3.1 创建时间回调,这个是我们处理许多后台操作 的方式,像客户端连接超时,过期key 等等
    /* Create the timer callback, this is our way to process many background
     * operations incrementally, like clients timeout, eviction of unaccessed
     * expired keys and so forth. */
    aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);

    // 3.2 为接收新建立的 TCP sockets 连接 设置事件处理器(acceptTcpHandler)
    for (j = 0; j < server.ipfd_count; j++) {
        // 为每一个db 注册accept事件处理器  (只是注册 后面会用到)
        aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL);
    }
}

其他

// file: server.c

void initServer(void) {

    createSharedObjects();
    adjustOpenFilesLimit();

    // 
    evictionPoolAlloc(); /* Initialize the LRU keys pool. */
}

(4) main方法-循环处理请求

循环处理请求 aeMain()

// file: src/ae.c 

/*
 * 循环处理事件
 * 
 * @param *eventLoop 
 */ 
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    // 循环处理事件
    while (!eventLoop->stop) {
        // 处理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}
// file: src/ae.c 

/*
 * 处理事件
 * 
 * @param *eventLoop
 * @param flags
 */ 
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    // 省略部分细节...

    // 调用多路复用API获取就绪事件
    numevents = aeApiPoll(eventLoop, tvp);

    // 处理写事件
    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
    // 处理读事件
    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                        
}

其实整个 Redis 的工作过程,就只需要理解清楚 main 函数中调用的 initServeraeMain 这两个函数就足够了。


(5) 思考

Redis 启动流程,主要的工作有:

1、初始化前置操作(设置时区、随机种子)

2、初始化 Server 的各种默认配置(server.c 的 initServerConfig 函数),默认配置见 server.h 中的 CONFIG_DEFAULT_XXX,比较典型的配置有:

  • 默认端口
  • 定时任务频率
  • 数据库数量
  • AOF 刷盘策略
  • 淘汰策略
  • 数据结构转换阈值
  • 主从复制参数

3、加载配置启动参数,覆盖默认配置(config.c 的 loadServerConfig 函数):

  • 解析命令行参数
  • 解析配置文件

3、初始化 Server(server.c 的 initServer 函数),例如会初始化:

  • 共享对象池
  • 客户端链表
  • 从库链表
  • 监听端口
  • 全局哈希表
  • LRU 池
  • 注册定时任务函数
  • 注册监听请求函数

4、启动事件循环(ae.c 的 aeMain 函数)

  • 处理请求
  • 处理定时任务

这里补充一下,初始化 Server 完成后,Redis 还会启动 3 类后台线程(server.c 的 InitServerLast 函数),协助主线程工作(异步释放 fd、AOF 每秒刷盘、lazyfree)。

课后题:Redis 源码的 main 函数在调用 initServer 函数之前,会执行如下的代码片段,你知道这个代码片段的作用是什么吗?

int main(int argc, char **argv) {

server.supervised = redisIsSupervised(server.supervised_mode);
int background = server.daemonize && !server.supervised;
if (background) daemonize();

}

Redis 可以配置以守护进程的方式启动(配置文件 daemonize = yes),也可以把 Redis 托管给 upstart 或 systemd 来启动 / 停止(supervised = upstart|systemd|auto)。

参考资料

[1] Redis源码剖析与实战-08 | Redis server启动后会做哪些操作?