Redis Server启动过程
今天,我们先来学习下 Redis server 的启动过程。
我们知道,main 函数是 Redis 整个运行程序的入口,并且 Redis 实例在运行时,也会从这个 main 函数开始执行。同时,由于 Redis 是典型的 Client-Server 架构,一旦 Redis 实例开始运行,Redis server 也就会启动,而 main 函数其实也会负责 Redis server 的启动运行。
其中,Redis 运行的基本控制逻辑是在server.c文件中完成的,而 main 函数就是在 server.c 中。
你可以掌握 Redis 针对以下三个问题的实现思路:
- Redis server 启动后具体会做哪些初始化操作?
- Redis server 初始化时有哪些关键配置项?
- Redis server 如何开始处理客户端请求?
好了,接下来,我们先从 main 函数开始,来了解下它在 Redis server 中的设计实现思路。
main 函数:Redis server 的入口一般来说,一个使用 C 开发的系统软件启动运行的代码逻辑,都是实现在了 main 函数当中,所以在正式了解 Redis 中 main 函数的实现之前,我想先给你分享一个小 Tips,就是你在阅读学习一个系统的代码时,可以先找下 main 函数,看看它的执行过程。那么,对于 Redis 的 main 函数来说,我把它执行的工作分成了五个阶段。
阶段一:基本初始化
在这个阶段,main 函数主要是完成一些基本的初始化工作,包括设置 server 运行的时区、设置哈希函数的随机种子等。这部分工作的主要调用函数如下所示:
// 设置时区
setlocale(LC_COLLATE,"");
tzset(); /* Populates 'timezone' global. */
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
srand(time(NULL)^getpid());
srandom(time(NULL)^getpid());
gettimeofday(&tv,NULL);
init_genrand64(((long long) tv.tv_sec * 1000000 + tv.tv_usec) ^ getpid());
crc64_init();
/* Store umask value. Because umask(2) only offers a set-and-get API we have
* to reset it and restore it back. We do this early to avoid a potential
* race condition with threads that could be creating files or directories.
*/
umask(server.umask = umask(0777));
// 设置随机种子
uint8_t hashseed[16];
getRandomBytes(hashseed,sizeof(hashseed));
dictSetHashFunctionSeed(hashseed);
这里,你需要注意的是,在 main 函数的开始部分,有一段宏定义覆盖的代码。这部分代码的作用是,如果定义了 REDIS_TEST 宏定义,并且 Redis server 启动时的参数符合测试参数,那么 main 函数就会执行相应的测试程序。
这段宏定义的代码如以下所示,其中的示例代码就是调用 ziplist 的测试函数 ziplistTest:
#ifdef REDIS_TEST
// 如果启动参数有test和ziplist,那么就调用ziplistTest函数进行ziplist的测试
if (argc >= 3 && !strcasecmp(argv[1], "test")) {
int accurate = 0;
for (j = 3; j < argc; j++) {
if (!strcasecmp(argv[j], "--accurate")) {
accurate = 1;
}
}
if (!strcasecmp(argv[2], "all")) {
int numtests = sizeof(redisTests)/sizeof(struct redisTest);
for (j = 0; j < numtests; j++) {
redisTests[j].failed = (redisTests[j].proc(argc,argv,accurate) != 0);
}
/* Report tests result */
int failed_num = 0;
for (j = 0; j < numtests; j++) {
if (redisTests[j].failed) {
failed_num++;
printf("[failed] Test - %s\n", redisTests[j].name);
} else {
printf("[ok] Test - %s\n", redisTests[j].name);
}
}
printf("%d tests, %d passed, %d failed\n", numtests,
numtests-failed_num, failed_num);
return failed_num == 0 ? 0 : 1;
} else {
redisTestProc *proc = getTestProcByName(argv[2]);
if (!proc) return -1; /* test not found */
return proc(argc,argv,accurate);
}
return 0;
}
#endif
阶段二:检查哨兵模式,并检查是否要执行 RDB 检测或 AOF 检测
Redis server 启动后,可能是以哨兵模式运行的,而哨兵模式运行的 server 在参数初始化、参数设置,以及 server 启动过程中要执行的操作等方面,与普通模式 server 有所差别。所以,main 函数在执行过程中需要根据 Redis 配置的参数,检查是否设置了哨兵模式。如果有设置哨兵模式的话,main 函数会调用 initSentinelConfig 函数,对哨兵模式的参数进行初始化设置,以及调用 initSentinel 函数,初始化设置哨兵模式运行的 server。有关哨兵模式运行的 Redis server 相关机制,我会在后面详细介绍。下面的代码展示了 main 函数中对哨兵模式的检查,以及对哨兵模式的初始化,你可以看下:
/* 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. */
//判断server是否设置为哨兵模式
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 函数对这部分内容的检查和调用:
/* 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程序,调用redis_check_rdb_main函数检测RDB文件
if (strstr(argv[0],"redis-check-rdb") != NULL)
redis_check_rdb_main(argc,argv,NULL);
//如果运行的是redis-check-aof程序,调用redis_check_aof_main函数检测AOF文件
else if (strstr(argv[0],"redis-check-aof") != NULL)
redis_check_aof_main(argc,argv);
阶段三:运行参数解析
在这一阶段,main 函数会对命令行传入的参数进行解析,并且调用 loadServerConfig 函数,对命令行参数和配置文件中的参数进行合并处理,然后为 Redis 各功能模块的关键参数设置合适的取值,以便 server 能高效地运行。
/* Load the server configuration from the specified filename.
* The function appends the additional configuration directives stored
* in the 'options' string to the config file before loading.
*
* Both filename and options can be NULL, in such a case are considered
* empty. This way loadServerConfig can be used to just load a file or
* just load a string. */
void loadServerConfig(char *filename, char config_from_stdin, char *options) {
sds config = sdsempty();
char buf[CONFIG_MAX_LINE+1];
FILE *fp;
/* Load the file content */
// 加载文件内容
if (filename) {
if ((fp = fopen(filename,"r")) == NULL) {
serverLog(LL_WARNING,"Fatal error, can't open config file '%s': %s", filename, strerror(errno));
exit(1);
}
while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
config = sdscat(config,buf);
fclose(fp);
}
// 追加从命令行输入的参数
/* Append content from stdin */
if (config_from_stdin) {
serverLog(LL_WARNING,"Reading config from stdin");
fp = stdin;
while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
config = sdscat(config,buf);
}
/* Append the additional options */
// 追加额外的命令
if (options) {
config = sdscat(config,"\n");
config = sdscat(config,options);
}
loadServerConfigFromString(config);
sdsfree(config);
}
阶段四:初始化 server
在完成对运行参数的解析和设置后,main 函数会调用 initServer 函数,对 server 运行时的各种资源进行初始化工作。这主要包括了 server 资源管理所需的数据结构初始化、键值对数据库初始化、server 网络框架初始化等。
void initServer(void) {
int j;
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
makeThreadKillable();
if (server.syslog_enabled) {
openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT, server.syslog_facility);
}
// 从配置系统设置默认值后初始化
/* Initialization after setting defaults from the config system. */
server.aof_state = server.aof_enabled ? AOF_ON : AOF_OFF;
server.hz = server.config_hz;
server.pid = getpid();
server.in_fork_child = CHILD_TYPE_NONE;
server.main_thread_id = pthread_self();
server.current_client = NULL;
server.errors = raxNew();
server.fixed_time_expire = 0;
server.clients = listCreate();
server.clients_index = raxNew();
server.clients_to_close = listCreate();
server.slaves = listCreate();
server.monitors = listCreate();
server.clients_pending_write = listCreate();
server.clients_pending_read = listCreate();
server.clients_timeout_table = raxNew();
server.replication_allowed = 1;
server.slaveseldb = -1; /* Force to emit the first SELECT command. */
server.unblocked_clients = listCreate();
server.ready_keys = listCreate();
server.clients_waiting_acks = listCreate();
server.get_ack_from_slaves = 0;
server.client_pause_type = 0;
server.paused_clients = listCreate();
server.events_processed_while_blocked = 0;
server.system_memory_size = zmalloc_get_memory_size();
server.blocked_last_cron = 0;
server.blocking_op_nesting = 0;
if ((server.tls_port || server.tls_replication || server.tls_cluster)
&& tlsConfigure(&server.tls_ctx_config) == C_ERR) {
serverLog(LL_WARNING, "Failed to configure TLS. Check logs for more info.");
exit(1);
}
createSharedObjects();
adjustOpenFilesLimit();
const char *clk_msg = monotonicInit();
serverLog(LL_NOTICE, "monotonic clock: %s", clk_msg);
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
serverLog(LL_WARNING,
"Failed creating the event loop. Error message: '%s'",
strerror(errno));
exit(1);
}
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
// 打开用户命令的TCP监听套接字
/* Open the TCP listening socket for the user commands. */
if (server.port != 0 &&
listenToPort(server.port,&server.ipfd) == C_ERR) {
serverLog(LL_WARNING, "Failed listening on port %u (TCP), aborting.", server.port);
exit(1);
}
if (server.tls_port != 0 &&
listenToPort(server.tls_port,&server.tlsfd) == C_ERR) {
serverLog(LL_WARNING, "Failed listening on port %u (TLS), aborting.", server.tls_port);
exit(1);
}
// 打开监听Unix的套接字
/* Open the listening Unix domain socket. */
if (server.unixsocket != NULL) {
unlink(server.unixsocket); /* don't care if this fails */
server.sofd = anetUnixServer(server.neterr,server.unixsocket,
server.unixsocketperm, server.tcp_backlog);
if (server.sofd == ANET_ERR) {
serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);
exit(1);
}
anetNonBlock(NULL,server.sofd);
anetCloexec(server.sofd);
}
// 如果没有监听的socket那就放弃退出
/* Abort if there are no listening sockets at all. */
if (server.ipfd.count == 0 && server.tlsfd.count == 0 && server.sofd < 0) {
serverLog(LL_WARNING, "Configured to not listen anywhere, exiting.");
exit(1);
}
// 创建Redis数据库,并初始化数据库的内部状态
/* Create the Redis databases, and initialize other internal state. */
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
server.db[j].expires_cursor = 0;
server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
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);
}
// 初始化LRU键的pool
evictionPoolAlloc(); /* Initialize the LRU keys pool. */
server.pubsub_channels = dictCreate(&keylistDictType,NULL);
server.pubsub_patterns = dictCreate(&keylistDictType,NULL);
server.cronloops = 0;
server.in_eval = 0;
server.in_exec = 0;
server.propagate_in_transaction = 0;
server.client_pause_in_transaction = 0;
server.child_pid = -1;
server.child_type = CHILD_TYPE_NONE;
server.rdb_child_type = RDB_CHILD_TYPE_NONE;
server.rdb_pipe_conns = NULL;
server.rdb_pipe_numconns = 0;
server.rdb_pipe_numconns_writing = 0;
server.rdb_pipe_buff = NULL;
server.rdb_pipe_bufflen = 0;
server.rdb_bgsave_scheduled = 0;
server.child_info_pipe[0] = -1;
server.child_info_pipe[1] = -1;
server.child_info_nread = 0;
aofRewriteBufferReset();
server.aof_buf = sdsempty();
server.lastsave = time(NULL); /* At startup we consider the DB saved. */
server.lastbgsave_try = 0; /* At startup we never tried to BGSAVE. */
server.rdb_save_time_last = -1;
server.rdb_save_time_start = -1;
server.dirty = 0;
resetServerStats();
/* A few stats we don't want to reset: server startup time, and peak mem. */
server.stat_starttime = time(NULL);
server.stat_peak_memory = 0;
server.stat_current_cow_bytes = 0;
server.stat_current_cow_updated = 0;
server.stat_current_save_keys_processed = 0;
server.stat_current_save_keys_total = 0;
server.stat_rdb_cow_bytes = 0;
server.stat_aof_cow_bytes = 0;
server.stat_module_cow_bytes = 0;
server.stat_module_progress = 0;
for (int j = 0; j < CLIENT_TYPE_COUNT; j++)
server.stat_clients_type_memory[j] = 0;
server.cron_malloc_stats.zmalloc_used = 0;
server.cron_malloc_stats.process_rss = 0;
server.cron_malloc_stats.allocator_allocated = 0;
server.cron_malloc_stats.allocator_active = 0;
server.cron_malloc_stats.allocator_resident = 0;
server.lastbgsave_status = C_OK;
server.aof_last_write_status = C_OK;
server.aof_last_write_errno = 0;
server.repl_good_slaves_count = 0;
/* 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. */
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. */
if (createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) != C_OK) {
serverPanic("Unrecoverable error creating TCP socket accept handler.");
}
if (createSocketAcceptHandler(&server.tlsfd, acceptTLSHandler) != C_OK) {
serverPanic("Unrecoverable error creating TLS socket accept handler.");
}
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");
/* Register a readable event for the pipe used to awake the event loop
* when a blocked client in a module needs attention. */
if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
moduleBlockedClientPipeReadable,NULL) == AE_ERR) {
serverPanic(
"Error registering the readable event for the module "
"blocked clients subsystem.");
}
/* Register before and after sleep handlers (note this needs to be done
* before loading persistence since it is used by processEventsWhileBlocked. */
aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep);
/* Open the AOF file if needed. */
if (server.aof_state == AOF_ON) {
server.aof_fd = open(server.aof_filename,
O_WRONLY|O_APPEND|O_CREAT,0644);
if (server.aof_fd == -1) {
serverLog(LL_WARNING, "Can't open the append-only file: %s",
strerror(errno));
exit(1);
}
}
/* 32 bit instances are limited to 4GB of address space, so if there is
* no explicit limit in the user provided configuration we set a limit
* at 3 GB using maxmemory with 'noeviction' policy'. This avoids
* useless crashes of the Redis instance for out of memory. */
if (server.arch_bits == 32 && server.maxmemory == 0) {
serverLog(LL_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
}
if (server.cluster_enabled) clusterInit();
replicationScriptCacheInit();
scriptingInit(1);
slowlogInit();
latencyMonitorInit();
/* Initialize ACL default password if it exists */
ACLUpdateDefaultUserPassword(server.requirepass);
}
而在调用完 initServer 后,main 函数还会再次判断当前 server 是否为哨兵模式。如果是哨兵模式,main 函数会调用 sentinelIsRunning 函数,设置启动哨兵模式。
/* This function gets called when the server is in Sentinel mode, started,
* loaded the configuration, and is ready for normal operations. */
// 当服务器处于Sentinel模式时调用此函数,已加载配置,并已准备好进行正常操作。
void sentinelIsRunning(void) {
int j;
/* If this Sentinel has yet no ID set in the configuration file, we
* pick a random one and persist the config on disk. From now on this
* will be this Sentinel ID across restarts. */
// 如果这个哨兵还没有在配置文件中设置ID,我们随机选取一个并将配置持久保存在磁盘上。从现在开始,将在重新启动时使用此哨兵ID。
for (j = 0; j < CONFIG_RUN_ID_SIZE; j++)
if (sentinel.myid[j] != 0) break;
if (j == CONFIG_RUN_ID_SIZE) {
/* Pick ID and persist the config. */
getRandomHexChars(sentinel.myid,CONFIG_RUN_ID_SIZE);
sentinelFlushConfig();
}
/* Log its ID to make debugging of issues simpler. */
serverLog(LL_WARNING,"Sentinel ID is %s", sentinel.myid);
/* We want to generate a +monitor event for every configured master
* at startup. */
// 我们希望在启动时为每个配置的主机生成一个监视器事件
sentinelGenerateInitialMonitorEvents();
}
否则的话,main 函数会调用 loadDataFromDisk 函数,从磁盘上加载 AOF 或者是 RDB 文件,以便恢复之前的数据。
/* Function called at startup to load RDB or AOF file in memory. */
void loadDataFromDisk(void) {
long long start = ustime();
// 如果AOF配置开启的话
if (server.aof_state == AOF_ON) {
// 加载AOF配置
if (loadAppendOnlyFile(server.aof_filename) == C_OK)
serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
}
// 否则没有开启AOF,加载RDB文件
else {
rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
errno = 0; /* Prevent a stale value from affecting error checking */
if (rdbLoad(server.rdb_filename,&rsi,RDBFLAGS_NONE) == C_OK) {
serverLog(LL_NOTICE,"DB loaded from disk: %.3f seconds",
(float)(ustime()-start)/1000000);
/* Restore the replication ID / offset from the RDB file. */
// 从RDB文件还原复制 ID/偏移量
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 采用了事件驱动框架,来并发处理不同客户端的连接和读写请求。所以,main 函数执行到最后时,会调用 aeMain 函数进入事件驱动框架,开始循环处理各种触发的事件。我把刚才介绍的五个阶段涉及到的关键操作,画在了下面的图中,你可以再回顾下。
那么,在这五个阶段当中,阶段三、四和五其实就包括了 Redis server 启动过程中的关键操作。所以接下来,我们就来依次学习下这三个阶段中的主要工作。
Redis 运行参数解析与设置
我们知道,Redis 提供了丰富的功能,既支持多种键值对数据类型的读写访问,还支持数据持久化保存、主从复制、切片集群等。而这些功能的高效运行,其实都离不开相关功能模块的关键参数配置。
举例来说,Redis 为了节省内存,设计了内存紧凑型的数据结构来保存 Hash、Sorted Set 等键值对类型。但是在使用了内存紧凑型的数据结构之后,如果往数据结构存入的元素个数过多或元素过大的话,键值对的访问性能反而会受到影响。因此,为了平衡内存使用量和系统访问性能,我们就可以通过参数,来设置和调节内存紧凑型数据结构的使用条件。
也就是说,掌握这些关键参数的设置,可以帮助我们提升 Redis 实例的运行效率。
不过,Redis 的参数有很多,我们无法在一节课中掌握所有的参数设置。所以下面,我们可以先来学习下 Redis 的主要参数类型,这样就能对各种参数形成一个全面的了解。同时,我也会给你介绍一些和 server 运行关系密切的参数及其设置方法,以便你可以配置好这些参数,让 server 高效运行起来。
Redis 的主要参数类型
首先,Redis 运行所需的各种参数,都统一定义在了server.h文件的 redisServer 结构体中。根据参数作用的范围,我把各种参数划分为了七大类型,包括通用参数、数据结构参数、网络参数、持久化参数、主从复制参数、切片集群参数、性能优化参数。具体你可以参考下面表格中的内容。
这样,如果你能按照上面的划分方法给 Redis 参数进行归类,那么你就可以发现,这些参数实际和 Redis 的主要功能机制是相对应的。所以,如果你要深入掌握这些参数的典型配置值,你就需要对相应功能机制的工作原理有所了解。
好,现在我们就了解了 Redis 的七大参数类型,以及它们基本的作用范围,那么下面我们就接着来学习下,Redis 是如何针对这些参数进行设置的。
Redis 参数的设置方法
Redis 对运行参数的设置实际上会经过三轮赋值,分别是默认配置值、命令行启动参数,以及配置文件配置值。
首先,Redis 在 main 函数中会先调用 initServerConfig 函数,为各种参数设置默认值。参数的默认值统一定义在 server.h 文件中,都是以 CONFIG_DEFAULT 开头的宏定义变量。下面的代码显示的是部分参数的默认值,你可以看下。
/* Static server configuration */
// server后台任务的默认运行频率
#define CONFIG_DEFAULT_HZ 10 /* Time interrupt calls/sec. */
// server后台任务的最小运行频率
#define CONFIG_MIN_HZ 1
// server后台任务的最大运行频率
#define CONFIG_MAX_HZ 500
// server监听的默认TCP端口
#define MAX_CLIENTS_PER_CLOCK_TICK 200 /* HZ is adapted based on that. */
#define CONFIG_MAX_LINE 1024
#define CRON_DBS_PER_CALL 16
#define NET_MAX_WRITES_PER_EVENT (1024*64)
#define PROTO_SHARED_SELECT_CMDS 10
#define OBJ_SHARED_INTEGERS 10000
#define OBJ_SHARED_BULKHDR_LEN 32
#define LOG_MAX_LEN 1024 /* Default maximum length of syslog messages.*/
#define AOF_REWRITE_ITEMS_PER_CMD 64
#define AOF_READ_DIFF_INTERVAL_BYTES (1024*10)
#define CONFIG_AUTHPASS_MAX_LEN 512
#define CONFIG_RUN_ID_SIZE 40
#define RDB_EOF_MARK_SIZE 40
#define CONFIG_REPL_BACKLOG_MIN_SIZE (1024*16) /* 16k */
#define CONFIG_BGSAVE_RETRY_DELAY 5 /* Wait a few secs before trying again. */
#define CONFIG_DEFAULT_PID_FILE "/var/run/redis.pid"
#define CONFIG_DEFAULT_CLUSTER_CONFIG_FILE "nodes.conf"
#define CONFIG_DEFAULT_UNIX_SOCKET_PERM 0
#define CONFIG_DEFAULT_LOGFILE ""
#define NET_HOST_STR_LEN 256 /* Longest valid hostname */
#define NET_IP_STR_LEN 46 /* INET6_ADDRSTRLEN is 46, but we need to be sure */
#define NET_ADDR_STR_LEN (NET_IP_STR_LEN+32) /* Must be enough for ip:port */
#define NET_HOST_PORT_STR_LEN (NET_HOST_STR_LEN+32) /* Must be enough for hostname:port */
#define CONFIG_BINDADDR_MAX 16
#define CONFIG_MIN_RESERVED_FDS 32
#define CONFIG_DEFAULT_PROC_TITLE_TEMPLATE "{title} {listen-addr} {server-mode}"
/* Synchronous read timeout - slave side */
// 同步读取超时-从机端
#define CONFIG_REPL_SYNCIO_TIMEOUT 5
在 server.h 中提供的默认参数值,一般都是典型的配置值。因此,如果你在部署使用 Redis 实例的过程中,对 Redis 的工作原理不是很了解,就可以使用代码中提供的默认配置。当然,如果你对 Redis 各功能模块的工作机制比较熟悉的话,也可以自行设置运行参数。你可以在启动 Redis 程序时,在命令行上设置运行参数的值。比如,如果你想将 Redis server 监听端口从默认的 6379 修改为 7379,就可以在命令行上设置 port 参数为 7379,如下所示:
./redis-server --port 7379
这里,你需要注意的是,Redis 的命令行参数设置需要使用**两个减号“–”**来表示相应的参数名,否则的话,Redis 就无法识别所设置的运行参数。
Redis 在使用 initServerConfig 函数对参数设置默认配置值后
void initServerConfig(void) {
int j;
updateCachedTime(1);
getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
server.runid[CONFIG_RUN_ID_SIZE] = '\0';
changeReplicationId();
clearReplicationId2();
server.hz = CONFIG_DEFAULT_HZ; /* Initialize it ASAP, even if it may get
updated later after loading the config.
This value may be used before the server
is initialized. */
server.timezone = getTimeZone(); /* Initialized by tzset(). */
server.configfile = NULL;
server.executable = NULL;
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
server.bindaddr_count = 0;
server.unixsocketperm = CONFIG_DEFAULT_UNIX_SOCKET_PERM;
server.ipfd.count = 0;
server.tlsfd.count = 0;
server.sofd = -1;
server.active_expire_enabled = 1;
server.skip_checksum_validation = 0;
server.saveparams = NULL;
server.loading = 0;
server.loading_rdb_used_mem = 0;
server.logfile = zstrdup(CONFIG_DEFAULT_LOGFILE);
server.aof_state = AOF_OFF;
server.aof_rewrite_base_size = 0;
server.aof_rewrite_scheduled = 0;
server.aof_flush_sleep = 0;
server.aof_last_fsync = time(NULL);
atomicSet(server.aof_bio_fsync_status,C_OK);
server.aof_rewrite_time_last = -1;
server.aof_rewrite_time_start = -1;
server.aof_lastbgrewrite_status = C_OK;
server.aof_delayed_fsync = 0;
server.aof_fd = -1;
server.aof_selected_db = -1; /* Make sure the first time will not match */
server.aof_flush_postponed_start = 0;
server.pidfile = NULL;
server.active_defrag_running = 0;
server.notify_keyspace_events = 0;
server.blocked_clients = 0;
memset(server.blocked_clients_by_type,0,
sizeof(server.blocked_clients_by_type));
server.shutdown_asap = 0;
server.cluster_configfile = zstrdup(CONFIG_DEFAULT_CLUSTER_CONFIG_FILE);
server.cluster_module_flags = CLUSTER_MODULE_FLAG_NONE;
server.migrate_cached_sockets = dictCreate(&migrateCacheDictType,NULL);
server.next_client_id = 1; /* Client IDs, start from 1 .*/
server.loading_process_events_interval_bytes = (1024*1024*2);
unsigned int lruclock = getLRUClock();
atomicSet(server.lruclock,lruclock);
resetServerSaveParams();
appendServerSaveParams(60*60,1); /* save after 1 hour and 1 change */
appendServerSaveParams(300,100); /* save after 5 minutes and 100 changes */
appendServerSaveParams(60,10000); /* save after 1 minute and 10000 changes */
/* Replication related */
server.masterauth = NULL;
server.masterhost = NULL;
server.masterport = 6379;
server.master = NULL;
server.cached_master = NULL;
server.master_initial_offset = -1;
server.repl_state = REPL_STATE_NONE;
server.repl_transfer_tmpfile = NULL;
server.repl_transfer_fd = -1;
server.repl_transfer_s = NULL;
server.repl_syncio_timeout = CONFIG_REPL_SYNCIO_TIMEOUT;
server.repl_down_since = 0; /* Never connected, repl is down since EVER. */
server.master_repl_offset = 0;
/* Replication partial resync backlog */
server.repl_backlog = NULL;
server.repl_backlog_histlen = 0;
server.repl_backlog_idx = 0;
server.repl_backlog_off = 0;
server.repl_no_slaves_since = time(NULL);
/* Failover related */
server.failover_end_time = 0;
server.force_failover = 0;
server.target_replica_host = NULL;
server.target_replica_port = 0;
server.failover_state = NO_FAILOVER;
/* Client output buffer limits */
for (j = 0; j < CLIENT_TYPE_OBUF_COUNT; j++)
server.client_obuf_limits[j] = clientBufferLimitsDefaults[j];
/* Linux OOM Score config */
for (j = 0; j < CONFIG_OOM_COUNT; j++)
server.oom_score_adj_values[j] = configOOMScoreAdjValuesDefaults[j];
/* Double constants initialization */
R_Zero = 0.0;
R_PosInf = 1.0/R_Zero;
R_NegInf = -1.0/R_Zero;
R_Nan = R_Zero/R_Zero;
/* Command table -- we initialize it here as it is part of the
* initial configuration, since command names may be changed via
* redis.conf using the rename-command directive. */
server.commands = dictCreate(&commandTableDictType,NULL);
server.orig_commands = dictCreate(&commandTableDictType,NULL);
populateCommandTable();
server.delCommand = lookupCommandByCString("del");
server.multiCommand = lookupCommandByCString("multi");
server.lpushCommand = lookupCommandByCString("lpush");
server.lpopCommand = lookupCommandByCString("lpop");
server.rpopCommand = lookupCommandByCString("rpop");
server.zpopminCommand = lookupCommandByCString("zpopmin");
server.zpopmaxCommand = lookupCommandByCString("zpopmax");
server.sremCommand = lookupCommandByCString("srem");
server.execCommand = lookupCommandByCString("exec");
server.expireCommand = lookupCommandByCString("expire");
server.pexpireCommand = lookupCommandByCString("pexpire");
server.xclaimCommand = lookupCommandByCString("xclaim");
server.xgroupCommand = lookupCommandByCString("xgroup");
server.rpoplpushCommand = lookupCommandByCString("rpoplpush");
server.lmoveCommand = lookupCommandByCString("lmove");
/* Debugging */
server.watchdog_period = 0;
/* By default we want scripts to be always replicated by effects
* (single commands executed by the script), and not by sending the
* script to the slave / AOF. This is the new way starting from
* Redis 5. However it is possible to revert it via redis.conf. */
server.lua_always_replicate_commands = 1;
/* Client Pause related */
server.client_pause_type = CLIENT_PAUSE_OFF;
server.client_pause_end_time = 0;
initConfigValues();
}
接下来,main 函数就会对 Redis 程序启动时的命令行参数进行逐一解析。main 函数会把解析后的参数及参数值保存成字符串,接着,main 函数会调用 loadServerConfig 函数进行第二和第三轮的赋值。
/* Load the server configuration from the specified filename.
* The function appends the additional configuration directives stored
* in the 'options' string to the config file before loading.
*
* Both filename and options can be NULL, in such a case are considered
* empty. This way loadServerConfig can be used to just load a file or
* just load a string. */
void loadServerConfig(char *filename, char config_from_stdin, char *options) {
sds config = sdsempty();
char buf[CONFIG_MAX_LINE+1];
FILE *fp;
/* Load the file content */
if (filename) {
if ((fp = fopen(filename,"r")) == NULL) {
serverLog(LL_WARNING,
"Fatal error, can't open config file '%s': %s",
filename, strerror(errno));
exit(1);
}
while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
config = sdscat(config,buf);
fclose(fp);
}
/* Append content from stdin */
if (config_from_stdin) {
serverLog(LL_WARNING,"Reading config from stdin");
fp = stdin;
while(fgets(buf,CONFIG_MAX_LINE+1,fp) != NULL)
config = sdscat(config,buf);
}
/* Append the additional options */
if (options) {
config = sdscat(config,"\n");
config = sdscat(config,options);
}
loadServerConfigFromString(config);
sdsfree(config);
}
以下代码显示了 main 函数对命令行参数的解析,以及调用 loadServerConfig 函数的过程,你可以看下。
int main(int argc, char **argv) {
......
// 保存命令行参数
for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
......
// 如果参数个数大于等于2个
if (argc >= 2) {
j = 1; /* First option to parse in argv[] */
sds options = sdsempty();
/* 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);
}
}
/* Parse command line options
* Precedence wise, File, stdin, explicit options -- last config is the one that matters.
*
* First argument is the config file name? */
if (argv[1][0] != '-') {
/* Replace the config file in server.exec_argv with its absolute path. */
server.configfile = getAbsolutePath(argv[1]);
zfree(server.exec_argv[1]);
server.exec_argv[1] = zstrdup(server.configfile);
j = 2; // Skip this arg when parsing options
}
// 对每个运行时参数进行解析
while(j < argc) {
/* Either first or last argument - Should we read config from stdin? */
if (argv[j][0] == '-' && argv[j][1] == '\0' && (j == 1 || j == argc-1)) {
config_from_stdin = 1;
}
/* 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 config file
* and stdin input are parsed (if they exist). */
// 是否是两个--
else if (argv[j][0] == '-' && argv[j][1] == '-') {
/* Option name */
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++;
}
loadServerConfig(server.configfile, config_from_stdin, options);
if (server.sentinel_mode) loadSentinelConfigFromQueue();
sdsfree(options);
}
return 0;
}
这里你要知道的是,loadServerConfig 函数是在config.c文件中实现的,该函数是以 Redis 配置文件和命令行参数的解析字符串为参数,将配置文件中的所有配置项读取出来,形成字符串。紧接着,loadServerConfig 函数会把解析后的命令行参数,追加到配置文件形成的配置项字符串。
这样一来,配置项字符串就同时包含了配置文件中设置的参数,以及命令行设置的参数。
最后,loadServerConfig 函数会进一步调用 loadServerConfigFromString 函数,对配置项字符串中的每一个配置项进行匹配。一旦匹配成功,loadServerConfigFromString 函数就会按照配置项的值设置 server 的参数。以下代码显示了 loadServerConfigFromString 函数的部分内容。这部分代码是使用了条件分支,来依次比较配置项是否是“timeout”和“tcp-keepalive”,如果匹配上了,就将 server 参数设置为配置项的值。同时,代码还会检查配置项的值是否合理,比如是否小于 0。如果参数值不合理,程序在运行时就会报错。另外对于其他的配置项,loadServerConfigFromString 函数还会继续使用 else if 分支进行判断。
config.c文件中查看
loadServerConfigFromString(char *config) {
…
//参数名匹配,检查参数是否为“timeout“
if (!strcasecmp(argv[0],"timeout") && argc == 2) {
//设置server的maxidletime参数
server.maxidletime = atoi(argv[1]);
//检查参数值是否小于0,小于0则报错
if (server.maxidletime < 0) {
err = "Invalid timeout value"; goto loaderr;
}
}
//参数名匹配,检查参数是否为“tcp-keepalive“
else if (!strcasecmp(argv[0],"tcp-keepalive") && argc == 2) {
//设置server的tcpkeepalive参数
server.tcpkeepalive = atoi(argv[1]);
//检查参数值是否小于0,小于0则报错
if (server.tcpkeepalive < 0) {
err = "Invalid tcp-keepalive value"; goto loaderr;
}
}
…
}
好了,到这里,你应该就了解了 Redis server 运行参数配置的步骤,我也画了一张图,以便你更直观地理解这个过程。
在完成参数配置后,main 函数会开始调用 initServer 函数,对 server 进行初始化。所以接下来,我们继续来了解 Redis server 初始化时的关键操作。
initServer:初始化
Redis serverRedis server 的初始化操作,主要可以分成三个步骤。
- 第一步,Redis server 运行时需要对多种资源进行管理。
比如说,和 server 连接的客户端、从库等,Redis 用作缓存时的替换候选集,以及 server 运行时的状态信息,这些资源的管理信息都会在 initServer 函数中进行初始化。我给你举个例子,initServer 函数会创建链表来分别维护客户端和从库,并调用 evictionPoolAlloc 函数(在evict.c中)
/* Create a new eviction pool. */
void evictionPoolAlloc(void) {
struct evictionPoolEntry *ep;
int j;
ep = zmalloc(sizeof(*ep)*EVPOOL_SIZE);
for (j = 0; j < EVPOOL_SIZE; j++) {
ep[j].idle = 0;
ep[j].key = NULL;
ep[j].cached = sdsnewlen(NULL,EVPOOL_CACHED_SDS_SIZE);
ep[j].dbid = 0;
}
EvictionPoolLRU = ep;
}
采样生成用于淘汰的候选 key 集合。同时,initServer 函数还会调用 resetServerStats 函数(在 server.c 中)重置 server 运行状态信息。
/* Resets the stats that we expose via INFO or other means that we want
* to reset via CONFIG RESETSTAT. The function is also used in order to
* initialize these fields in initServer() at server startup. */
void resetServerStats(void) {
int j;
server.stat_numcommands = 0;
server.stat_numconnections = 0;
server.stat_expiredkeys = 0;
server.stat_expired_stale_perc = 0;
server.stat_expired_time_cap_reached_count = 0;
server.stat_expire_cycle_time_used = 0;
server.stat_evictedkeys = 0;
server.stat_keyspace_misses = 0;
server.stat_keyspace_hits = 0;
server.stat_active_defrag_hits = 0;
server.stat_active_defrag_misses = 0;
server.stat_active_defrag_key_hits = 0;
server.stat_active_defrag_key_misses = 0;
server.stat_active_defrag_scanned = 0;
server.stat_fork_time = 0;
server.stat_fork_rate = 0;
server.stat_total_forks = 0;
server.stat_rejected_conn = 0;
server.stat_sync_full = 0;
server.stat_sync_partial_ok = 0;
server.stat_sync_partial_err = 0;
server.stat_io_reads_processed = 0;
atomicSet(server.stat_total_reads_processed, 0);
server.stat_io_writes_processed = 0;
atomicSet(server.stat_total_writes_processed, 0);
for (j = 0; j < STATS_METRIC_COUNT; j++) {
server.inst_metric[j].idx = 0;
server.inst_metric[j].last_sample_time = mstime();
server.inst_metric[j].last_sample_count = 0;
memset(server.inst_metric[j].samples,0,
sizeof(server.inst_metric[j].samples));
}
atomicSet(server.stat_net_input_bytes, 0);
atomicSet(server.stat_net_output_bytes, 0);
server.stat_unexpected_error_replies = 0;
server.stat_total_error_replies = 0;
server.stat_dump_payload_sanitizations = 0;
server.aof_delayed_fsync = 0;
}
- 第二步,在完成资源管理信息的初始化后,initServer 函数会对 Redis 数据库进行初始化。
因为一个 Redis 实例可以同时运行多个数据库,所以 initServer 函数会使用一个循环,依次为每个数据库创建相应的数据结构。这个代码逻辑是实现在 initServer 函数中,它会为每个数据库执行初始化操作,包括创建全局哈希表,为过期 key、被 BLPOP 阻塞的 key、将被 PUSH 的 key 和被监听的 key 创建相应的信息表。
/* Create the Redis databases, and initialize other internal state. */
for (j = 0; j < server.dbnum; j++) {
//创建全局哈希表
server.db[j].dict = dictCreate(&dbDictType,NULL);
//创建过期key的信息表
server.db[j].expires = dictCreate(&dbExpiresDictType,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);
}
- 第三步,initServer 函数会为运行的 Redis server 创建事件驱动框架,并开始启动端口监听,用于接收外部请求。
注意,为了高效处理高并发的外部请求,initServer 在创建的事件框架中,针对每个监听 IP 上可能发生的客户端连接,都创建了监听事件,用来监听客户端连接请求。同时,initServer 为监听事件设置了相应的处理函数 acceptTcpHandler。
networking.c文件中查看
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
UNUSED(el);
UNUSED(mask);
UNUSED(privdata);
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
anetCloexec(cfd);
serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
}
}
这样一来,只要有客户端连接到 server 监听的 IP 和端口,事件驱动框架就会检测到有连接事件发生,然后调用 acceptTcpHandler 函数来处理具体的连接。你可以参考以下代码中展示的处理逻辑:
//创建事件循环框架
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
serverLog(LL_WARNING,"Failed creating the event loop. Error message: '%s'",strerror(errno));
exit(1);
}
// 给数据库分配内存
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
//开始监听设置的网络端口
/* Open the TCP listening socket for the user commands. */
if (server.port != 0 &&
listenToPort(server.port,&server.ipfd) == C_ERR) {
serverLog(LL_WARNING, "Failed listening on port %u (TCP), aborting.", server.port);
exit(1);
}
/* 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. */
//为server后台任务创建定时事件
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
那么到这里,Redis server 在完成运行参数设置和初始化后,就可以开始处理客户端请求了。为了能持续地处理并发的客户端请求,server 在 main 函数的最后,会进入事件驱动循环机制。而这就是接下来,我们要了解的事件驱动框架的执行过程。
执行事件驱动框架
事件驱动框架是 Redis server 运行的核心。该框架一旦启动后,就会一直循环执行,每次循环会处理一批触发的网络读写事件。关于事件驱动框架本身的设计思想与实现方法,这里,我们主要是学习 Redis 入口的 main 函数中,是如何转换到事件驱动框架进行执行的。
其实,进入事件驱动框架开始执行并不复杂,main 函数直接调用事件框架的主体函数 aeMain(在ae.c文件中)后,就进入事件处理循环了。
当然,在进入事件驱动循环前,main 函数会分别调用 aeSetBeforeSleepProc 和 aeSetAfterSleepProc 两个函数,来设置每次进入事件循环前 server 需要执行的操作,以及每次事件循环结束后 server 需要执行的操作。下面代码显示了这部分的执行逻辑,你可以看下。
int main(int argc, char **argv) {
…
/* Register before and after sleep handlers (note this needs to be done
* before loading persistence since it is used by processEventsWhileBlocked. */
aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);
...
}
总结
我们通过 server.c 文件中 main 函数的设计和实现思路,了解了 Redis server 启动后的五个主要阶段。在这五个阶段中,运行参数解析、server 初始化和执行事件驱动框架则是 Redis sever 启动过程中的三个关键阶段。所以相应的,我们需要重点关注以下三个要点。
- 第一,main 函数是使用 initServerConfig 给 server 运行参数设置默认值,然后会解析命令行参数,并通过 loadServerConfig 读取配置文件参数值,将命令行参数追加至配置项字符串。最后,Redis 会调用 loadServerConfigFromString 函数,来完成配置文件参数和命令行参数的设置。
- 第二,在 Redis server 完成参数设置后,initServer 函数会被调用,用来初始化 server 资源管理的主要结构,同时会初始化数据库启动状态,以及完成 server 监听 IP 和端口的设置。
- 第三,一旦 server 可以接收外部客户端的请求后,main 函数会把程序的主体控制权,交给事件驱动框架的入口函数,也就 aeMain 函数。aeMain 函数会一直循环执行,处理收到的客户端请求。到此为止,server.c 中的 main 函数功能就已经全部完成了,程序控制权也交给了事件驱动循环框架,Redis 也就可以正常处理客户端请求了。
实际上,Redis server 的启动过程从基本的初始化操作,到命令行和配置文件的参数解析设置,再到初始化 server 各种数据结构,以及最后的执行事件驱动框架,这是一个典型的网络服务器执行过程,你在开发网络服务器时,就可以作为参考。而且,掌握了启动过程中的初始化操作,还可以帮你解答一些使用中的疑惑。比如,Redis 启动时是先读取 RDB 文件,还是先读取 AOF 文件。如果你了解了 Redis server 的启动过程,就可以从 loadDataFromDisk 函数中看到,Redis server 会先读取 AOF;而如果没有 AOF,则再读取 RDB。所以,掌握 Redis server 启动过程,有助于你更好地了解 Redis 运行细节,这样当你遇到问题时,就知道还可以从启动过程中去溯源 server 的各种初始状态,从而助力你更好地解决问题。