共计 8562 个字符,预计需要花费 22 分钟才能阅读完成。
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,则返回零;若 str1<str2,则返回正数;若 str1>str2,则返回负数。
【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 外围原理与实际》
京东链接