写点什么

Redis 核心原理与实践 --Redis 启动过程源码分析

用户头像
binecy
关注
发布于: 刚刚
Redis核心原理与实践--Redis启动过程源码分析

Redis 服务器负责接收处理用户请求,为用户提供服务。Redis 服务器的启动命令格式如下:

redis-server [ configfile ] [ options ]
复制代码

configfile 参数指定配置文件。options 参数指定启动配置项,它可以覆盖配置文件中的配置项,如

redis-server /path/to/redis.conf --port 7777 --protected-mode no
复制代码

该命令启动 Redis 服务,并指定了配置文件/path/to/redis.conf,给出了两个启动配置项:port、protected-mode。

本文通过阅读 Redis 源码,分析 Redis 启动过程,内容摘自新书《Redis 核心原理与实践》。本文涉及 Redis 的很多概念,如事件循环器、ACL、Module、LUA、慢日志等,这些功能在作者新书《Redis 核心原理与实践》做了详尽分析,感兴趣的读者可以参考本书。

服务器定义

提示:本章代码如无特殊说明,均在 server.h、server.c 中。

Redis 中定义了 server.h/redisServer 结构体,存储 Redis 服务器信息,包括服务器配置项和运行时数据(如网络连接信息、数据库 redisDb、命令表、客户端信息、从服务器信息、统计信息等数据)。

struct redisServer {    pid_t pid;                      pthread_t main_thread_id;             char *configfile;               char *executable;               char **exec_argv;        ...}
复制代码

redisServer 中的属性很多,这里不一一列举,等到分析具体功能时再说明相关的 server 属性。server.h 中定义了一个 redisServer 全局变量:

extern struct redisServer server;
复制代码

本书说到的 server 变量,如无特殊说明,都是指该 redisServer 全局变量。例如,第 1 部分说过 server.list_max_ziplist_size 等属性,正是指该变量的属性。可以使用 INFO 命令获取服务器的信息,该命令主要返回以下信息:

  • server:有关 Redis 服务器的常规信息。

  • clients:客户端连接信息。

  • memory:内存消耗相关信息。

  • persistence:RDB 和 AOF 持久化信息。

  • stats:常规统计信息。

  • replication:主/副本复制信息。

  • cpu:CPU 消耗信息。

  • commandstats:Redis 命令统计信息。

  • cluster:Redis Cluster 集群信息。

  • modules:Modules 模块信息。

  • keyspace:数据库相关的统计信息。

  • errorstats:Redis 错误统计信息。

INFO 命令响应内容中除了 memory 和 cpu 等统计数据,其他数据大部分都保存在 redisServer 中。

main 函数

server.c/main 函数负责启动 Redis 服务:

int main(int argc, char **argv) {    ...    // [1]    server.sentinel_mode = checkForSentinelMode(argc,argv);    // [2]    initServerConfig();    ACLInit(); 
    moduleInitModulesSystem();    tlsInit();
    // [3]    server.executable = getAbsolutePath(argv[0]);    server.exec_argv = zmalloc(sizeof(char*)*(argc+1));    server.exec_argv[argc] = NULL;    for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
    // [4]    if (server.sentinel_mode) {        initSentinelConfig();        initSentinel();    }
    // [5]    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);
    // more}
复制代码

【1】检查该 Redis 服务器是否以 sentinel 模式启动。

【2】initServerConfig 函数将 redisServer 中记录配置项的属性初始化为默认值。ACLInit 函数初始化 ACL 机制,moduleInitModulesSystem 函数初始化 Module 机制。

【3】记录 Redis 程序可执行路径及启动参数,以便后续重启服务器。

【4】如果以 Sentinel 模式启动,则初始化 Sentinel 机制。

【5】如果启动程序是 redis-check-rdb 或 redis-check-aof,则执行 redis_check_rdb_main 或 redis_check_aof_main 函数,它们尝试检验并修复 RDB、AOF 文件后便退出程序。Redis 编译完成后,会生成 5 个可执行程序:

  • redis-server:Redis 执行程序。

  • redis-sentinel:Redis Sentinel 执行程序。

  • redis-cli:Redis 客户端程序。

  • redis-benchmark:Redis 性能压测工具。

  • redis-check-aof、redis-check-rdb:用于检验和修复 RDB、AOF 持久化文件的工具。

继续分析 main 函数:

int main(int argc, char **argv) {    ...    if (argc >= 2) {        j = 1;         sds options = sdsempty();        char *configfile = NULL;
        // [6]        if (strcmp(argv[1], "-v") == 0 ||            strcmp(argv[1], "--version") == 0) version();        ...
        // [7]        if (argv[j][0] != '-' || argv[j][1] != '-') {            configfile = argv[j];            server.configfile = getAbsolutePath(configfile);            zfree(server.exec_argv[j]);            server.exec_argv[j] = zstrdup(server.configfile);            j++;        }
       // [8]        while(j != argc) {            ...        }        // [9]        if (server.sentinel_mode && configfile && *configfile == '-') {            ...            exit(1);        }        // [10]        resetServerSaveParams();        loadServerConfig(configfile,options);        sdsfree(options);    }    ...}
复制代码

【6】对-v、--version、--help、-h、--test-memory 等命令进行优先处理。strcmp 函数比较两个字符串 str1、str2,若 str1=str2,则返回零;若 str1str2,则返回正数。

【7】如果启动命令的第二个参数不是以“--”开始的,则是配置文件参数,将配置文件路径转化为绝对路径,存入 server.configfile 中。

【8】读取启动命令中的启动配置项,并将它们拼接到一个字符串中。

【9】以 Sentinel 模式启动,必须指定配置文件,否则直接报错退出。

【10】config.c/resetServerSaveParams 函数重置 server.saveparams 属性(该属性存放 RDB SAVE 配置)。

config.c/loadServerConfig 函数从配置文件中加载所有配置项,并使用启动命令配置项覆盖配置文件中的配置项。

提示:config.c 中的 configs 数组定义了大多数配置选项与 server 属性的对应关系:

standardConfig configs[] = {    createBoolConfig("rdbchecksum", NULL, IMMUTABLE_CONFIG, server.rdb_checksum, 1, NULL, NULL),    createBoolConfig("daemonize", NULL, IMMUTABLE_CONFIG, server.daemonize, 0, NULL, NULL),    ...}
复制代码

配置项 rdbchecksum 对应 server.rdb_checksum 属性,默认值为 1(即 bool 值 yes),其他配置项以此类推。如果读者需要查找配置项对应的 server 属性和默认值,则可以从中查找。


下面继续分析 main 函数:

int main(int argc, char **argv) {    ...    // [11]        server.supervised = redisIsSupervised(server.supervised_mode);    int background = server.daemonize && !server.supervised;    if (background) daemonize();    // [12]    serverLog(LL_WARNING, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo");    ...
    // [13]    initServer();    if (background || server.pidfile) createPidFile();    ...
    if (!server.sentinel_mode) {        ...        // [14]        moduleLoadFromQueue();        ACLLoadUsersAtStartup();        InitServerLast();        loadDataFromDisk();        if (server.cluster_enabled) {            if (verifyClusterConfigWithData() == C_ERR) {                ...                exit(1);            }        }        ...    } else {        // [15]        InitServerLast();        sentinelIsRunning();        ...    }
    ...    // [16]    redisSetCpuAffinity(server.server_cpulist);    setOOMScoreAdj(-1);    // [17]    aeMain(server.el);    // [18]    aeDeleteEventLoop(server.el);    return 0;}
复制代码

【11】server.supervised 属性指定是否以 upstart 服务或 systemd 服务启动 Redis。

如果配置了 server.daemonize 且没有配置 server.supervised,则以守护进程的方式启动 Redis。

【12】打印启动日志。

【13】initServer 函数初始化 Redis 运行时数据,createPidFile 函数创建 pid 文件。

【14】如果非 Sentinel 模式启动,则完成以下操作:

(1)moduleLoadFromQueue 函数加载配置文件指定的 Module 模块;

(2)ACLLoadUsersAtStartup 函数加载 ACL 用户控制列表;

(3)InitServerLast 函数负责创建后台线程、I/O 线程,该步骤需在 Module 模块加载后再执行;

(4)loadDataFromDisk 函数从磁盘中加载 AOF 或 RDB 文件。

(5)如果以 Cluster 模式启动,那么还需要验证加载的数据是否正确。

【15】如果以 Sentinel 模式启动,则调用 sentinelIsRunning 函数启动 Sentinel 机制。

【16】尽可能将 Redis 主线程绑定到 server.server_cpulist 配置的 CPU 列表上,Redis 4 开始使用多线程,该操作可以减少不必要的线程切换,提高性能。

【17】启动事件循环器。事件循环器是 Redis 中的重要组件。在 Redis 运行期间,由事件循环器提供服务。【18】执行到这里,说明 Redis 服务已停止,aeDeleteEventLoop 函数清除事件循环器中的事件,最后退出程序。

Redis 初始化过程

下面看一下 initServer 函数,它负责初始化 Redis 运行时数据:

void initServer(void) {    int j;    // [1]    signal(SIGHUP, SIG_IGN);    signal(SIGPIPE, SIG_IGN);    setupSignalHandlers();    // [2]    makeThreadKillable();    // [3]    if (server.syslog_enabled) {        openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,            server.syslog_facility);    }
    // [4]    server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;    server.hz = server.config_hz;    server.pid = getpid();    ...

    // [5]    createSharedObjects();    adjustOpenFilesLimit();    // [6]    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);    if (server.el == NULL) {        ...        exit(1);    }
    // more}
复制代码

【1】设置 UNIX 信号处理函数,使 Redis 服务器收到 SIGINT 信号后退出程序。

【2】设置线程随时响应 CANCEL 信号,终止线程,以便停止程序。

【3】如果开启了 Unix 系统日志,则调用 openlog 函数与 Unix 系统日志建立输出连接,以便输出系统日志。

【4】初始化 server 中负责存储运行时数据的相关属性。

【5】createSharedObjects 函数创建共享数据集,这些数据可在各场景中共享使用,如小数字 0~9999、常用字符串+OK\r\n(命令处理成功响应字符串)、+PONG\r\n(ping 命令响应字符串)。adjustOpenFilesLimit 函数尝试修改环境变量,提高系统允许打开的文件描述符上限,避免由于大量客户端连接(Socket 文件描述符)导致错误。

【6】创建事件循环器。UNIX 编程:信号也称为软中断,信号是 UNIX 提供的一种处理异步事件的方法,程序通过设置回调函数告诉系统内核,在信号产生后要做什么操作。系统中很多场景会产生信号,例如:

  • 用户按下某些终端键,使终端产生信号。例如,用户在终端按下了中断键(一般为 Ctrl+C 组合键),会发送 SIGINT 信号通知程序停止运行。

  • 系统中发生了某些特定事件,例如,当 alarm 函数设置的定时器超时,内核发送 SIGALRM 信号,或者一个进程终止时,内核发送 SIGCLD 信号给其父进程。

  • 某些硬件异常,例如,除数为 0、无效的内存引用。

  • 程序中使用函数发送信号,例如,调用 kill 函数将任意信号发送给另一个进程。

感兴趣的读者可以自行深入了解 UNIX 编程相关内容。


接着分析 initServer 函数:

void initServer(void) {        server.db = zmalloc(sizeof(redisDb)*server.dbnum);
    // [7]    if (server.port != 0 &&        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)        exit(1);    ...
    // [8]    for (j = 0; j < server.dbnum; j++) {        server.db[j].dict = dictCreate(&dbDictType,NULL);        server.db[j].expires = dictCreate(&keyptrDictType,NULL);        ...    }
    // [9]    evictionPoolAlloc();     server.pubsub_channels = dictCreate(&keylistDictType,NULL);    server.pubsub_patterns = listCreate();    ...}
复制代码

【7】如果配置了 server.port,则开启 TCP Socket 服务,接收用户请求。

如果配置了 server.tls_ port,则开启 TLS Socket 服务,Redis 6.0 开始支持 TLS 连接。

如果配置了 server.unixsocket,则开启 UNIX Socket 服务。

如果上面 3 个选项都没有配置,则报错退出。

【8】初始化数据库 server.db,用于存储数据。

【9】evictionPoolAlloc 函数初始化 LRU/LFU 样本池,用于实现 LRU/LFU 近似算法。


继续初始化 server 中存储运行时数据的相关属性:

void initServer(void) {    ...    // [10]    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {        serverPanic("Can't create event loop timers.");        exit(1);    }
    // [11]    for (j = 0; j < server.ipfd_count; j++) {        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,            acceptTcpHandler,NULL) == AE_ERR)            {                serverPanic(                    "Unrecoverable error creating server.ipfd file event.");            }    }    ...
    // [12]    aeSetBeforeSleepProc(server.el,beforeSleep);    aeSetAfterSleepProc(server.el,afterSleep);
    // [13]    if (server.aof_state == AOF_ON) {        server.aof_fd = open(server.aof_filename,                               O_WRONLY|O_APPEND|O_CREAT,0644);        ...    }
    // [14]    if (server.arch_bits == 32 && server.maxmemory == 0) {        ...        server.maxmemory = 3072LL*(1024*1024); /* 3 GB */        server.maxmemory_policy = MAXMEMORY_NO_EVICTION;    }    // [15]    if (server.cluster_enabled) clusterInit();    replicationScriptCacheInit();    scriptingInit(1);    slowlogInit();    latencyMonitorInit();}
复制代码

【10】创建一个时间事件,执行函数为 serverCron,负责处理 Redis 中的定时任务,如清理过期数据、生成 RDB 文件等。

【11】分别为 TCP Socket、TSL Socks、UNIX Socket 注册监听 AE_READABLE 类型的文件事件,事件处理函数分别为 acceptTcpHandler、acceptTLSHandler、acceptUnixHandler,这些函数负责接收 Socket 中的新连接,本书后续会详细分析 acceptTcpHandler 函数。

【12】注册事件循环器的钩子函数,事件循环器在每次阻塞前后都会调用钩子函数。

【13】如果开启了 AOF,则预先打开 AOF 文件。

【14】如果 Redis 运行在 32 位操作系统上,由于 32 位操作系统内存空间限制为 4GB,所以将 Redis 使用内存限制为 3GB,避免 Redis 服务器因内存不足而崩溃。

【15】如果以 Cluster 模式启动,则调用 clusterInit 函数初始化 Cluster 机制。

  • replicationScriptCacheInit 函数初始化 server.repl_scriptcache_dict 属性。

  • scriptingInit 函数初始化 LUA 机制。

  • slowlogInit 函数初始化慢日志机制。

  • latencyMonitorInit 函数初始化延迟监控机制。


总结:

  • redisServer 结构体存储服务端配置项、运行时数据。

  • server.c/main 是 Redis 启动方法,负责加载配置,初始化数据库,启动网络服务,创建并启动事件循环器。

文章最后,介绍一下新书《Redis 核心原理与实践》,本书通过深入分析 Redis 6.0 源码,总结了 Redis 核心功能的设计与实现。通过阅读本书,读者可以深入理解 Redis 内部机制及最新特性,并学习到 Redis 相关的数据结构与算法、Unix 编程、存储系统设计,分布式系统架构等一系列知识。

经过该书编辑同意,我会继续在个人技术公众号(binecy)发布书中部分章节内容,作为书的预览内容,欢迎大家查阅,谢谢。

语雀平台预览:《Redis核心原理与实践》

京东链接

发布于: 刚刚阅读数: 2
用户头像

binecy

关注

还未添加个人签名 2020.08.26 加入

《Redis核心原理与实践》作者,欢迎关注个人技术公众号binecy

评论

发布
暂无评论
Redis核心原理与实践--Redis启动过程源码分析