Redis 启动时会干什么

361 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情 这篇文章我们来看一下 Redis Server 在启动时会干些什么?

我们看一下 Redis 启动的入口函数,它是 server.c 文件里面 main 函数。代码如下:

int main(int argc, char **argv) {
    struct timeval tv;
    int j;

    /* We need to initialize our libraries, and the server configuration. */
#ifdef INIT_SETPROCTITLE_REPLACEMENT
    spt_init(argc, argv);
#endif
    // 设置时区
    setlocale(LC_COLLATE,"");
    tzset(); /* Populates 'timezone' global. */
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);

    // 获取随机种子
    char hashseed[16];
    getRandomHexChars(hashseed,sizeof(hashseed));
    dictSetHashFunctionSeed((uint8_t*)hashseed);
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    // 设置配置文件
    initServerConfig();
    moduleInitModulesSystem();

    /* Store the executable path and arguments in a safe place in order
     * to be able to restart the server later. */
    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]);

    /* 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. */
    // 判断是否是哨兵模式
    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. */
    // 检查我们是否需要以redis-check-rdb/aof模式启动,执行相应的main函数
    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);

    if (argc >= 2) {
        j = 1; /* First option to parse in argv[] */
        sds options = sdsempty();
        char *configfile = NULL;

        /* Handle special options --help and --version */
        if (strcmp(argv[1], "-v") == 0 ||
            strcmp(argv[1], "--version") == 0) version();
        if (strcmp(argv[1], "--help") == 0 ||
            strcmp(argv[1], "-h") == 0) usage();
        if (strcmp(argv[1], "--test-memory") == 0) {
            if (argc == 3) {
                memtest(atoi(argv[2]),50);
                exit(0);
            } else {
                fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");
                fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");
                exit(1);
            }
        }

        /* First argument is the config file name? */
        if (argv[j][0] != '-' || argv[j][1] != '-') {
            configfile = argv[j];
            server.configfile = getAbsolutePath(configfile);
            /* Replace the config file in server.exec_argv with
             * its absolute path. */
            zfree(server.exec_argv[j]);
            server.exec_argv[j] = zstrdup(server.configfile);
            j++;
        }

        /* All the other options are parsed and conceptually appended to the
         * configuration file. For instance --port 6380 will generate the
         * string "port 6380\n" to be parsed after the actual file name
         * is parsed, if any. */
        while(j != argc) {
            if (argv[j][0] == '-' && argv[j][1] == '-') {
                /* Option name */
                if (!strcmp(argv[j], "--check-rdb")) {
                    /* Argument has no options, need to skip for parsing. */
                    j++;
                    continue;
                }
                if (sdslen(options)) options = sdscat(options,"\n");
                options = sdscat(options,argv[j]+2);
                options = sdscat(options," ");
            } else {
                /* Option argument */
                options = sdscatrepr(options,argv[j],strlen(argv[j]));
                options = sdscat(options," ");
            }
            j++;
        }
        if (server.sentinel_mode && configfile && *configfile == '-') {
            serverLog(LL_WARNING,
                "Sentinel config from STDIN not allowed.");
            serverLog(LL_WARNING,
                "Sentinel needs config file on disk to save state.  Exiting...");
            exit(1);
        }
        resetServerSaveParams();
        loadServerConfig(configfile,options);
        sdsfree(options);
    }

    serverLog(LL_WARNING, "oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo");
    serverLog(LL_WARNING,
        "Redis version=%s, bits=%d, commit=%s, modified=%d, pid=%d, just started",
            REDIS_VERSION,
            (sizeof(long) == 8) ? 64 : 32,
            redisGitSHA1(),
            strtol(redisGitDirty(),NULL,10) > 0,
            (int)getpid());

    if (argc == 1) {
        serverLog(LL_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis");
    } else {
        serverLog(LL_WARNING, "Configuration loaded");
    }

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

    initServer();
    if (background || server.pidfile) createPidFile();
    redisSetProcTitle(argv[0]);
    redisAsciiArt();
    checkTcpBacklogSettings();

    
    if (!server.sentinel_mode) {
        /* Things not needed when running in Sentinel mode. */
        serverLog(LL_WARNING,"Server initialized");
    #ifdef __linux__
        linuxMemoryWarnings();
    #if defined (__arm64__)
        int ret;
        if ((ret = linuxMadvFreeForkBugCheck())) {
            if (ret == 1)
                serverLog(LL_WARNING,"WARNING Your kernel has a bug that could lead to data corruption during background save. "
                                     "Please upgrade to the latest stable kernel.");
            else
                serverLog(LL_WARNING, "Failed to test the kernel for a bug that could lead to data corruption during background save. "
                                      "Your system could be affected, please report this error.");
            if (!checkIgnoreWarning("ARM64-COW-BUG")) {
                serverLog(LL_WARNING,"Redis will now exit to prevent data corruption. "
                                     "Note that it is possible to suppress this warning by setting the following config: ignore-warnings ARM64-COW-BUG");
                exit(1);
            }
        }
    #endif /* __arm64__ */
    #endif /* __linux__ */
        moduleLoadFromQueue();
        InitServerLast();
        // 从 aof 或 rdb 加载数据
        loadDataFromDisk();
        if (server.cluster_enabled) {
            if (verifyClusterConfigWithData() == C_ERR) {
                serverLog(LL_WARNING,
                    "You can't have keys in a DB different than DB 0 when in "
                    "Cluster mode. Exiting.");
                exit(1);
            }
        }
        if (server.ipfd_count > 0)
            serverLog(LL_NOTICE,"Ready to accept connections");
        if (server.sofd > 0)
            serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
    } else {
        InitServerLast();
        sentinelIsRunning();
    }

    /* Warning the user about suspicious maxmemory setting. */
    if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
        serverLog(LL_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);
    }

    aeSetBeforeSleepProc(server.el,beforeSleep);
    aeSetAfterSleepProc(server.el,afterSleep);
    // 启动事件驱动框架
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
    return 0;
}

我对上面的代码做了简单的注释,下面我们来分析分析 Redis 的启动流程。

Redis 启动流程

基本初始化

一开始 main 函数主要是完成一些基本的初始化操作,包括设置 server 运⾏的时区、设置哈希函数的随机种⼦等。

...
    //设置时区
    setlocale(LC_COLLATE,"");
    tzset(); /* Populates 'timezone' global. */
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);
    // 设置随机种子
    char hashseed[16];
    getRandomHexChars(hashseed,sizeof(hashseed));
    dictSetHashFunctionSeed((uint8_t*)hashseed);
...

同时还会初始化配置参数以及初始化 module,分别是 initServerConfig 函数和 moduleInitModulesSystem 函数。

检查哨兵模式,并检查是否要执⾏RDB检测或AOF检测

Redis Server 可能是以哨兵模式运⾏的。而在哨兵模式下,server 在参数初始化、参数设置以及 server 启动过程中要执⾏的操作等方面与普通模式 server 有所差别。main 函数在执⾏过程中需要根据 Redis 配置的参数,检查是否设置了哨兵模式。如果是设置了哨兵模式,则会使用 initSentinelConfig 函数对哨兵模式的配置进行初始化,然后调用 initSentinel 函数以哨兵模式运行 server

 ...
     // 校验是否是哨兵模式
     server.sentinel_mode = checkForSentinelMode(argc,argv);
 ...
    // 是否是哨兵模式
    if (server.sentinel_mode) {
        // 加载哨兵配置
        initSentinelConfig();
        // 初始化哨兵模式
        initSentinel();
    }
 ...

除了检查哨兵模式以外,main 函数还会检查是否要执⾏ RDB 检查或 AOF 检查,这对应了实际运⾏的程序是 redis-check-rdbredis-check-aof。在这种情况下,main 函数会调⽤ redis_check_rdb_main 函数或 redis_check_aof_main 函数,检测 RDB ⽂件或 AOF ⽂件。下⾯的代码就展⽰了 main 函数对这部分内容的检查和调用:

...
   if (strstr(argv[0],"redis-check-rdb") != NULL)
        // 如果运⾏的是 redis-check-rdb 程序,调用 redis_check_rdb_main 函数检测 RDB ⽂件
        redis_check_rdb_main(argc,argv,NULL);
    else if (strstr(argv[0],"redis-check-aof") != NULL)
        // 如果运⾏的是 redis-check-aof 程序,调用 redis_check_aof_main 函数检测 AOF ⽂件
        redis_check_aof_main(argc,argv);

...

运⾏参数解析

在这⼀阶段,main 函数会对命令⾏传⼊的参数进⾏解析,并且调⽤ loadServerConfig 函数,对命令⾏参数和配置⽂件中的参数进行合并处理,然后为 Redis 各功能模块的关键参数设置合适的取值,以便 server 能高效地运转。下面我们来看一下 Redis 参数的设置过程。

首先,Redis 会先调⽤ initServerConfig 函数,为各种参数设置默认值。参数的默认值统⼀定义在 server.h ⽂件中,都是以 CONFIG_DEFAULT 开头的宏定义变量。

Redis 在使用 initServerConfig 函数初始化之后,会对 Redis 程序启动时的命令行参数进行逐一解析。

...
     // 保存命令行参数
     for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
...
    // 对每个运⾏时参数进行解析
    while(j != argc) {
            if (argv[j][0] == '-' && argv[j][1] == '-') {
                /* Option name */
                if (!strcmp(argv[j], "--check-rdb")) {
                    /* Argument has no options, need to skip for parsing. */
                    j++;
                    continue;
                }
                if (sdslen(options)) options = sdscat(options,"\n");
                options = sdscat(options,argv[j]+2);
                options = sdscat(options," ");
            } else {
                /* Option argument */
                options = sdscatrepr(options,argv[j],strlen(argv[j]));
                options = sdscat(options," ");
            }
            j++;
        }

最后 Redis 会调用 loadServerConfig 函数会把解析后的命令行参数追加到配置⽂件形成的配置项字符串。最后 loadServerConfig 函数会调用 loadServerConfigFromString 函数对配置项字符串中的每个配置项进行匹配,一旦配置成功就会按照配置项里面的内容设置 server 的参数。

配置加载.png

初始化 server

Redis 使用 initServer 函数对服务进行初始化,然后用 InitServerLast 函数启动异步线程,最后用 loadDataFromDisk 函数从 aof/rdb 中初始化数据。

initServer

我们首先看一下 initServer 函数具体做了什么。 第一步对 Redis server 运⾏时需要对多种资源进行管理。

第二步对 Redis 数据库进行初始化。Redis 中会有多个数据库,initServer 中会对每个数据库进行初始化

...
    for (j = 0; j < server.dbnum; j++) {
        // 创建全局哈希表
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        // 创建过期key的哈希表
        server.db[j].expires = dictCreate(&keyptrDictType,NULL);
        // 为被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();
    }
...

第三步会初始化事件驱动框架,并设置定时事件和为每个客户端设置了监听事件,监听函数是 acceptTcpHandler 来监听每个客户端的连接事件。我们看一下代码中是如何实现的:

...
    // 创建事件循环框架
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
...
    // 创建定时事件
    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
    }

    /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    // 为每个链接设置连接事件监听函数
    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.");
            }
    }

InitServerLast

面试中常问的 Redis 是不是单线程,下面我们就来揭晓这个答案。InitServerLast 主要是创建后台线程的,把部分任务交给后台线程处理。所以从这里看 Redis 运行时已经不是单个线程在运行了,还会有后台线程在运行。我们看一下 InitServerLast 函数的实现:

void InitServerLast() {
    bioInit();
    server.initial_memory_usage = zmalloc_used_memory();
}

InitServerLast 主要调用了 bioInit 函数用来创建后台线程,我们重点了解一下 bioInit 函数做了什么事情。我们看一下代码实现:

void bioInit(void) {
    pthread_attr_t attr;
    pthread_t thread;
    size_t stacksize;
    int j;

    /* Initialization of state vars and objects */
    // 初始化锁的线程数组
    for (j = 0; j < BIO_NUM_OPS; j++) {
        pthread_mutex_init(&bio_mutex[j],NULL);
        pthread_cond_init(&bio_newjob_cond[j],NULL);
        pthread_cond_init(&bio_step_cond[j],NULL);
        bio_jobs[j] = listCreate();
        bio_pending[j] = 0;
    }

    /* Set the stack size as by default it may be small in some system */
    pthread_attr_init(&attr);
    pthread_attr_getstacksize(&attr,&stacksize);
    if (!stacksize) stacksize = 1; /* The world is full of Solaris Fixes */
    while (stacksize < REDIS_THREAD_STACK_SIZE) stacksize *= 2;
    pthread_attr_setstacksize(&attr, stacksize);

    /* Ready to spawn our threads. We use the single argument the thread
     * function accepts in order to pass the job ID the thread is
     * responsible of. */
    // 创建线程
    for (j = 0; j < BIO_NUM_OPS; j++) {
        void *arg = (void*)(unsigned long) j;
        if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
            serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
            exit(1);
        }
        bio_threads[j] = thread;
    }
}

我们看到里面有一些数组,我们来看一下它们是如何定义的

// 保存线程描述符的数组
static pthread_t bio_threads[BIO_NUM_OPS];
// 保存互斥锁的数组
static pthread_mutex_t bio_mutex[BIO_NUM_OPS];
// 保存条件变量的两个数组
static pthread_cond_t bio_newjob_cond[BIO_NUM_OPS];
static pthread_cond_t bio_step_cond[BIO_NUM_OPS];

我们看到它们的数组大小都是 BIO_NUM_OPS,这是个宏定义,我们看一下 bio.h 文件,除此之外还定义了 BIO_CLOSE_FILEBIO_AOF_FSYNCBIO_LAZY_FREE 这三个宏定义。

/* Background job opcodes */
#define BIO_CLOSE_FILE    0 /* Deferred close(2) syscall. */
#define BIO_AOF_FSYNC     1 /* Deferred AOF fsync. */
#define BIO_LAZY_FREE     2 /* Deferred objects freeing. */
#define BIO_NUM_OPS       3

通过这几个宏定义我们可以看出 Redis 会创建三个后台线程,通过上面三个操作码代表不同的后台任务。

  • BIO_CLOSE_FILE : 文件关闭后台任务。
  • BIO_AOF_FSYNCAOF 日志同步写回后台任务。
  • BIO_LAZY_FREE : 惰性删除后台任务。
...
   for (j = 0; j < BIO_NUM_OPS; j++) {
        void *arg = (void*)(unsigned long) j;
        if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
            serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
            exit(1);
        }
        bio_threads[j] = thread;
    }
...

从创建线程这个过程看,Redis 会创建三个线程,这三个线程的执行函数都是 bioProcessBackgroundJobs 函数,只是传入的参数 arg 分别为 0,1,2。这里的用意是什么呢,需要我们看一下 bioProcessBackgroundJobs 函数的实现:

     ...
     // 获取传入参数
     unsigned long type = (unsigned long) arg;
     ...
     if (type == BIO_CLOSE_FILE) {
            // 异步释放 fd
            close((long)job->arg1);
        } else if (type == BIO_AOF_FSYNC) {
            // AOF 每秒刷盘
            redis_fsync((long)job->arg1);  
        } else if (type == BIO_LAZY_FREE) {
            // 惰性删除
            /* What we free changes depending on what arguments are set:
             * arg1 -> free the object at pointer.
             * arg2 & arg3 -> free two dictionaries (a Redis DB).
             * only arg3 -> free the skiplist. */
            if (job->arg1)
                // 释放一个普通的对象
                lazyfreeFreeObjectFromBioThread(job->arg1);
            else if (job->arg2 && job->arg3)
                // 释放全局redisDb对象的dict字典和expires字典,用户flushdb
                lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);
            else if (job->arg3)
                // 释放cluster的slots_to_keys对象
                lazyfreeFreeSlotsMapFromBioThread(job->arg3);
        } else {
            serverPanic("Wrong job type in bioProcessBackgroundJobs().");
        }
        ...

bioProcessBackgroundJobs 函数会根据不同的 arg 调用不同的方法。后台线程这里就简要分析到这里,后面可能会详细分析这里的实现。

loadDataFromDisk

loadDataFromDisk 函数主要是加载 AOF 文件或 RDB 文件,我们可以看一下 loadDataFromDisk 的实现:

void loadDataFromDisk(void) {
    long long start = ustime();
    // 判断是否开启 AOF,如果开启则从 AOF 文件中加载数据
    if (server.aof_state == AOF_ON) {
        if (loadAppendOnlyFile(server.aof_filename) == C_OK)
            serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
    } else {
        // 如果未开启 AOF 则会从 RDB 中加载数据
        rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
        if (rdbLoad(server.rdb_filename,&rsi) == C_OK) {
            serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
                (float)(ustime()-start)/1000000);

            /* Restore the replication ID / offset from the RDB file. */
            if ((server.masterhost ||
                (server.cluster_enabled &&
                nodeIsSlave(server.cluster->myself))) &&
                rsi.repl_id_is_set &&
                rsi.repl_offset != -1 &&
                /* Note that older implementations may save a repl_stream_db
                 * of -1 inside the RDB file in a wrong way, see more
                 * information in function rdbPopulateSaveInfo. */
                rsi.repl_stream_db != -1)
            {
                memcpy(server.replid,rsi.repl_id,sizeof(server.replid));
                server.master_repl_offset = rsi.repl_offset;
                /* If we are a slave, create a cached master from this
                 * information, in order to allow partial resynchronizations
                 * with masters. */
                replicationCacheMasterUsingMyself();
                selectDb(server.cached_master,rsi.repl_stream_db);
            }
        } else if (errno != ENOENT) {
            serverLog(LL_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno));
            exit(1);
        }
    }
}

面试中常问的 Redis 文件的加载顺序是怎样的,可以从上面的实现中可以看出 Redis 首先会加载 AOF 文件来初始化数据,如果没有 AOF 才会从 RDB 文件中加载数据。

启动事件驱动框架

事件驱动框架是 Redis server 运⾏的核⼼。该框架启动后就会⼀直循环执⾏,每次循环会处理⼀批触发的⽹络读写事件。事件驱动框架在 initSever 函数中完成了创建,我们看一下该框架是如何启动的:

    // 创建事件循环框架,在 initServer 函数中执行
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
...
    // 设置进入事件循环流程前执行的函数
    aeSetBeforeSleepProc(server.el,beforeSleep);
    // 设置退出事件循环流程后执行的函数
    aeSetAfterSleepProc(server.el,afterSleep);
    // 启动事件驱动框架
    aeMain(server.el);
...

我在 从一条Redis命令是如何执行了解事件驱动 中详细介绍了 Redis 的事件驱动框架,大家有兴趣可以看一下,这里就不过多进行解释了。