Redis 源码分析(七)服务器启动

redis服务器启动过程

redis经历了长时间的发展,已经从一个简单的内存数据库变为了一个复杂的内存数据库系统。所以启动就包含了很多子系统的初始化过程。我们这里讲启动的流程只能先梳理以下redis启动的脉络,一些子系统的初始化细节还是要放到后续的章节再讲。

redis启动的过程也是redisServer结构初始化的过程,该结构保存了redis server的所有配置信息,我尽量将这个结构中各个字段的用途进行解释如有疏漏还请指正。

入口

redis server的main函数在server.c这个文件中,之前说的redisServer结构保存在server.h中,这个结构足足有500多行,包括指令表、AOF日志、运行时状态、统计数据、配置数据等等,相同系统的控制变量写在一起,但仍有不少运行时状态和配置项是散落在各处的,我们只能在用到的时候获取其来源,这里先不做过多介绍。

 1int main(int argc, char **argv) {
 2    struct timeval tv;
 3    int j;
 4    char config_from_stdin = 0;
 5
 6    /* We need to initialize our libraries, and the server configuration. */
 7#ifdef INIT_SETPROCTITLE_REPLACEMENT
 8    /// 只是复制了一份argv[0]以及environ变量。environ变量用于重启服务,以及开sentinel的时候保证环境变量稳定。
 9    spt_init(argc, argv);
10#endif
11    setlocale(LC_COLLATE,"");
12    /// 通过环境变量TZ设置时区
13    tzset(); /* Populates 'timezone' global. */
14    /// 设置内存溢出时的处理函数,redis只是记录了两行out of memory日志
15    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
16    /// 随机数初始化,异或pid是为了防止时间重复,因为redis需要每台机器有一个唯一id,且这个id是从随机数中产生的。
17    srand(time(NULL)^getpid());
18    srandom(time(NULL)^getpid());
19    /// 获取当前时间
20    gettimeofday(&tv,NULL);
21    /// 还是随机数初始化,用于获取64位的随机数
22    init_genrand64(((long long) tv.tv_sec * 1000000 + tv.tv_usec) ^ getpid());
23    /// 64位crc初始化
24    crc64_init();
25
26    /* Store umask value. Because umask(2) only offers a set-and-get API we have
27     * to reset it and restore it back. We do this early to avoid a potential
28     * race condition with threads that could be creating files or directories.
29     */
30    /// 设置文件的权限掩码为0777(8进制)
31    umask(server.umask = umask(0777));
32
33    /// 设置dict的哈希函数种子
34    uint8_t hashseed[16];
35    getRandomBytes(hashseed,sizeof(hashseed));
36    dictSetHashFunctionSeed(hashseed);
37    /// 检查输入参数,并设置哨兵模式
38    server.sentinel_mode = checkForSentinelMode(argc,argv);

基本的初始化这样就完成了。接下来我们看后续的初始化过程:

 1    /// 调用初始化服务器配置,这里为redisServer结构中大部分字段设置了默认值
 2    initServerConfig();
 3    /// redis 增加了用户认证的模块,这里是其初始化的地方
 4    ACLInit(); /* The ACL subsystem must be initialized ASAP because the
 5                  basic networking code and client creation depends on it. */
 6    /// redis模块系统的初始化,这个需要点时间去看,先放一下。
 7    moduleInitModulesSystem();
 8    /// 空函数,忽略
 9    tlsInit();
10
11    /* Store the executable path and arguments in a safe place in order
12     * to be able to restart the server later. */
13    /// 复制命令行参数
14    server.executable = getAbsolutePath(argv[0]);
15    server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
16    server.exec_argv[argc] = NULL;
17    for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
18
19    /* We need to init sentinel right now as parsing the configuration file
20     * in sentinel mode will have the effect of populating the sentinel
21     * data structures with master nodes to monitor. */
22    if (server.sentinel_mode) {
23        /// 哨兵模式初始化哨兵配置
24        initSentinelConfig();
25        initSentinel();
26    }
27
28    /* Check if we need to start in redis-check-rdb/aof mode. We just execute
29     * the program main. However the program is part of the Redis executable
30     * so that we can easily execute an RDB check on loading errors. */
31
32    /// 针对redis的两个别名,分别进行初始化
33    if (strstr(argv[0],"redis-check-rdb") != NULL)
34        redis_check_rdb_main(argc,argv,NULL);
35    else if (strstr(argv[0],"redis-check-aof") != NULL)
36        redis_check_aof_main(argc,argv);

之后是对命令行参数的解析

 1if (argc >= 2) {
 2    j = 1; /* First option to parse in argv[] */
 3    sds options = sdsempty();
 4
 5    /* Handle special options --help and --version */
 6    if (strcmp(argv[1], "-v") == 0 ||
 7        strcmp(argv[1], "--version") == 0) version();
 8    if (strcmp(argv[1], "--help") == 0 ||
 9        strcmp(argv[1], "-h") == 0) usage();
10    
11    /// 内存测试,分配了一块大内存,之后进行随机读写操作
12    if (strcmp(argv[1], "--test-memory") == 0) {
13        if (argc == 3) {
14            memtest(atoi(argv[2]),50);
15            exit(0);
16        } else {
17            fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");
18            fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");
19            exit(1);
20        }
21    }
22
23    /* Parse command line options
24        * Precedence wise, File, stdin, explicit options -- last config is the one that matters.
25        *
26        * First argument is the config file name? */
27    
28    /// 第一个参数如果不是选项就将其作为配置文件路径
29    if (argv[1][0] != '-') {
30        /* Replace the config file in server.exec_argv with its absolute path. */
31        server.configfile = getAbsolutePath(argv[1]);
32        zfree(server.exec_argv[1]);
33        server.exec_argv[1] = zstrdup(server.configfile);
34        j = 2; // Skip this arg when parsing options
35    }
36    
37    /// 依次遍历后续参数
38    while(j < argc) {
39        /* Either first or last argument - Should we read config from stdin? */
40        /// 第一个或最后一个参数是'-'则配置从标准输入读取
41        if (argv[j][0] == '-' && argv[j][1] == '\0' && (j == 1 || j == argc-1)) {
42            config_from_stdin = 1;
43        }
44        /* All the other options are parsed and conceptually appended to the
45            * configuration file. For instance --port 6380 will generate the
46            * string "port 6380\n" to be parsed after the actual config file
47            * and stdin input are parsed (if they exist). */
48        /// 所有其他选项连为用空格分隔的字符串
49        else if (argv[j][0] == '-' && argv[j][1] == '-') {
50            /* Option name */
51            if (sdslen(options)) options = sdscat(options,"\n");
52            options = sdscat(options,argv[j]+2);
53            options = sdscat(options," ");
54        } else {
55            /* Option argument */
56            options = sdscatrepr(options,argv[j],strlen(argv[j]));
57            options = sdscat(options," ");
58        }
59        j++;
60    }
61
62    /// 加载服务器配置
63    loadServerConfig(server.configfile, config_from_stdin, options);
64    /// 如果是哨兵模式则加载哨兵配置
65    if (server.sentinel_mode) loadSentinelConfigFromQueue();
66    sdsfree(options);
67}

服务器配置有三个来源

  • 配置文件
  • 标准输入
  • 命令行参数 其中,标准输入可以与其他两个同时存在

在内存中以上的三部分会通过 换行符 进行分割,并将一行中多个 “选项 值” 进行折行处理,这样所有的配置会按每行 “选项 值” 这样的形式进行解析。

解析代码保存在config.c中的loadServerConfigFromString函数中

1lines = sdssplitlen(config,strlen(config),"\n",1,&totlines);

这个函数主要用于分割所有的选项,之后的代码会对选项做转小写处理,并验证选项的正确性。不正确的选项会被直接跳过。

 1sdstolower(argv[0]);
 2
 3/* Iterate the configs that are standard */
 4int match = 0;
 5for (standardConfig *config = configs; config->name != NULL; config++) {
 6    if ((!strcasecmp(argv[0],config->name) ||
 7        (config->alias && !strcasecmp(argv[0],config->alias))))
 8    {
 9        if (argc != 2) {
10            err = "wrong number of arguments";
11            goto loaderr;
12        }
13        if (!config->interface.set(config->data, argv[1], 0, &err)) {
14            goto loaderr;
15        }
16
17        match = 1;
18        break;
19    }
20}
21
22if (match) {
23    sdsfreesplitres(argv,argc);
24    continue;
25}

configs 定义了每个参数的参数名,别名,类型,初始化以及读写和重写操作的调用函数。这样我们就可以在设置选项值的时候进行一些操作,来使配置生效。

从网上摘了一份redis常用配置的表,先放在这里

1if (server.sentinel_mode) sentinelCheckConfigFile();

之后如果是哨兵模式,则检查配置文件是否存在且具备写权限。这是因为哨兵模式启动后redis会将原配置中哨兵相关的配置进行初始化和修改并回写本地配置文件。具体写了什么我们后面在redis模式中再进行讨论。

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

这几行是关于redis守护进程的配置,守护方式分为upstart和systemd两种,upstart方式需要设置UPSTART_JOB环境参数并置为非空,systemd则需要设置NOTIFY_SOCKET环境变量,并设为非空。

之后是这一行代码

1readOOMScoreAdj();

oom score 是系统的一项针对内存使用情况的评分,如果内存不足则系统会优先杀掉oom score 最高的进程,redis读取启动时的oom score并保存在内存中,redis配置中也包含对oom score的设置项,分别是

  • oom-score-adj
    • no : 不修改
    • yes : relative 相同效果
    • absolute : 使用 oom-score-adj-values 的值进行设置
    • relative : 基于进程开启时的初始值进行增减
  • oom-score-adj-values : 三个值分别对应master replic child(被fork出来的进程)

之后就是redis对服务器真正的初始化的地方了

1    initServer();
2    if (background || server.pidfile) createPidFile();
3    if (server.set_proc_title) redisSetProcTitle(NULL);
4    redisAsciiArt();
5    checkTcpBacklogSettings();

initServer 主要功能如下:

  • 初始化所有运行时的状态
  • 创建共享对象 createSharedObjects()
  • 设置最大客户端数量 adjustOpenFilesLimit()
  • 创建事件循环对象 aeCreateEventLoop()
  • 启动网络监听 listenToPort() tcp port 和 tls port
  • 启动Unix socket监听
  • 创建redis databse数据结构并初始化
  • 初始化LRU 键驱逐算法。驱逐池会保有16个可能被驱逐的键,若内存不足则优先从池中选择被删除的key。具体逻辑放到后面再讲
  • 创建定时器 aeCreateTimeEvent
  • 为每个socket设置Accept事件处理句柄
    • tcp socket : acceptTcpHandler()
    • tls socket : acceptTLSHandler()
    • unix socket : acceptUnixHandler()
  • 为redis module 设置事件处理接口 moduleBlockedClientPipeReadable()
  • 设置事件循环休眠前的处理句柄 beforeSleep()
  • 设置事件循环休眠后的处理句柄 afterSleep()
  • 打开AOF文件准备写入
  • 如果是集群模式,则进行集群初始化
  • 初始化脚本缓冲 replicationScriptCacheInit()
  • 初始化脚本 scriptingInit()
  • 慢查询日志初始化 slowlogInit()
  • 毛刺监控初始化 latencyMonitorInit()
  • ACL 默认密码初始化 ACLUpdateDefaultUserPassword()

服务器初始化完成后会

  • 写入pid到指定文件
  • 设置进程title (不知道干什么用的)
  • 打印redis Banner redisAsciiArt()
  • 检查进程最大监听上线 checkTcpBacklogSettings 小于tcp_backlog设置的数值时redis会报警告

在哨兵和非哨兵模式时redis还会分别做一些额外的事情

 1if (!server.sentinel_mode) {
 2    /* Things not needed when running in Sentinel mode. */
 3    serverLog(LL_WARNING,"Server initialized");
 4
 5    linuxMemoryWarnings();
 6
 7    moduleInitModulesSystemLast();
 8    moduleLoadFromQueue();
 9    ACLLoadUsersAtStartup();
10    InitServerLast();
11    loadDataFromDisk();
12    if (server.cluster_enabled) {
13        if (verifyClusterConfigWithData() == C_ERR) {
14            serverLog(LL_WARNING,
15                "You can't have keys in a DB different than DB 0 when in "
16                "Cluster mode. Exiting.");
17            exit(1);
18        }
19    }
20    if (server.ipfd.count > 0 || server.tlsfd.count > 0)
21        serverLog(LL_NOTICE,"Ready to accept connections");
22    if (server.sofd > 0)
23        serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
24    if (server.supervised_mode == SUPERVISED_SYSTEMD) {
25        if (!server.masterhost) {
26            redisCommunicateSystemd("STATUS=Ready to accept connections\n");
27        } else {
28            redisCommunicateSystemd("STATUS=Ready to accept connections in read-only mode. Waiting for MASTER <-> REPLICA sync\n");
29        }
30        redisCommunicateSystemd("READY=1\n");
31    }
32} else {
33    ACLLoadUsersAtStartup();
34    InitServerLast();
35    sentinelIsRunning();
36    if (server.supervised_mode == SUPERVISED_SYSTEMD) {
37        redisCommunicateSystemd("STATUS=Ready to accept connections\n");
38        redisCommunicateSystemd("READY=1\n");
39    }
40}

linuxMemoryWarnings() 这个函数里进行了两项内存分配机制的检查

  • overcommit 是指内存在已分配还未使用时,系统是否真实分配物理内存。
    • 0 :内存不足则返回失败
    • 1 :允许超量使用内存,包括交换区
    • 2 :允许超量使用内存,但不超过物理内存的50% + 交换区容量
  • THP
    Transparent Huge Pages 是大内存页支持,因为redis需要通过fork进程来完成一些任务(例如AOF)而开启THP会因为内存页过大,fork出的子进程在进行内存写操作时将大量时间花费在内存复制上,从而降低redis效率。
    • redis 检查是否开启了THP并尝试关闭

moduleInitModulesSystemLast() 里面只是创建了一个redisClient对象并保存到全局变量里,用于redis模块执行命令。

moduleLoadFromQueue() 将配置中通过loadmodule 选项设置的模块加载到内存中并初始化。

ACLLoadUsersAtStartup() 加载用户权限配置文件,该配置文件由aclfile选项或user选项指定,且不可同时设置。

InitServerLast() 初始化最后的服务器设置

  • bioInit() 初始化后台线程,后台线程处理函数为bioProcessBackgroundJobs
    进程分为三种类型

    • BIO_CLOSE_FILE : 用于文件关闭
    • BIO_AOF_FSYNC :用于AOF同步
    • BIO_LAZY_FREE :用于延迟释放
  • initThreadedIO()
    创建IO线程,线程处理函数为IOThreadMain() 主要用于处理client读写

loadDataFromDisk() 从AOF或RDB恢复数据

verifyClusterConfigWithData() cluster 模式开启的情况下对cluster配置进行检查。

最后的初始化步骤

1    redisSetCpuAffinity(server.server_cpulist);
2    setOOMScoreAdj(-1);
3
4    aeMain(server.el);
5    aeDeleteEventLoop(server.el);
6    return 0;

redisSetCpuAffinity() 设置CPU亲缘性,就是指定线程在哪个CPU上执行。如果线程切换比较频繁会导致CPU的L1,L2级缓存命中率降低。这是为了提升缓存命中。

setOOMScoreAdj(-1) 设置进程的OOM Score,-1代表自动检测服务类型 (MASTER或REPLICA),如果不想进程因为内存分配失败被杀就在配置中把oom_score_adj_values改为-1000即可。

aeMain(server.el); 事件循环,这里就到了redis接受外部输入处理逻辑的地方了。

aeDeleteEventLoop(server.el); 事件循环退出后的清理工作。

评论