开启掘金成长之旅!这是我参与「掘金日新计划 · 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-rdb 或 redis-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 的参数。
初始化 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_FILE、BIO_AOF_FSYNC、BIO_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_FSYNC :
AOF日志同步写回后台任务。 - 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 的事件驱动框架,大家有兴趣可以看一下,这里就不过多进行解释了。