說說 redis 的啓動流程。
首先要找到啓動函數,我們知道 C 程序從 main
函數開始,所以,就找到了“夢想”開始的地方 server.c
-> main
。
這裏主要講啓動過程中的主要部分,所以並不會一一涉及到。
大概啓動流程
initServerConfig 函數
整個代碼中最重要的結構體莫過於 struct redisServer server
,它以一個全局變量的形式出現。本函數主要是對它的成員進行賦值操作,這些成員基本上是可以通過 redis.conf 文件來配置。
大部分成員賦初值
比如:
server 字段 | 含義 |
---|---|
runid | 節點標識佔用 40B |
port | 啓動端口默認爲 6379 |
tcp_backlog | 默認 511B |
aof_fsync | 默認 aof 每秒刷盤,但是 aof 默認關閉 |
aof_filename | 默認 aof 文件名爲 appendonly.aof |
rdb_filename | 默認 rdb 文件名爲 dump.rdb |
cluster_node_timeout | 默認 15s,默認 cluster 模式關閉 |
默認 rdb 觸發條件
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
包含對 backlog 的相關設置。
Double constants initialization
浮點數據精度設置。
client output buffer limit
一共有三種類型,如下:
clientBufferLimitsConfig clientBufferLimitsDefaults[CLIENT_TYPE_OBUF_COUNT] = {
{0, 0, 0}, /* normal */
{1024*1024*256, 1024*1024*64, 60}, /* slave */
{1024*1024*32, 1024*1024*8, 60} /* pubsub */
};
redis 命令表
初始化 redis 命令表放到 server.commands
中,這主要是在 populateCommandTable
函數中完成的。
注意:考慮到在 redis.conf 配置文件中可以使用 rename-command 來對 Command 進行重命名(通常是爲了安全考慮而禁用某些命令),因此命令表保存了兩份,即 server.commands
和 server.orig_commands
。
同時還對一些經常查詢的命令單獨提出來,分別放到以下變量中,
struct redisCommand *delCommand, *multiCommand, *lpushCommand, *lpopCommand,
*rpopCommand, *sremCommand, *execCommand;
Slow log
默認時間爲 10ms。
sentinel 模式
以下方式進行該模式的開啓:
int checkForSentinelMode(int argc, char **argv) {
int j;
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1;
return 0;
}
使用命令行參數 --sentinel
,或者直接使用二進制文件 redis-sentinel
。
如果開啓了該模式,那麼進行相應的初始,沒開啓就跳過。
if (server.sentinel_mode) {
initSentinelConfig(); // sentinel 默認端口 26379
initSentinel(); // sentinel 變量賦初值
}
命令行參數解析並載入配置文件
主要還是獲得配置文件的絕對路徑 server.configfile = getAbsolutePath(configfile)
。
配置文件的載入有專門的函數
void loadServerConfig(char *filename, char *options){}
載入配置文件後,會覆蓋之前對於 server 的某些默認配置。實際上,當 redis-server 啓動後,一些配置可以通過 config get
命令查看,也可以通過 config set
命令進行修改,修改後 config rewrite
刷盤。
initServer 函數
不同於 initServerConfig
函數,該函數主要初始化一些 redis-server 運行中的成員。
信號處理
通過 redis 來複習下信號處理。
// 忽略SIGHUP和SIGPIPE信號
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
void setupSignalHandlers(void) {
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sigShutdownHandler;
sigaction(SIGTERM, &act, NULL);
sigaction(SIGINT, &act, NULL);
return;
}
主要是程序退出的善後工作。
系統日誌
if (server.syslog_enabled) {
openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
server.syslog_facility);
}
前提是使用到了系統的 rsyslog。
createSharedObjects 函數
該函數把一些常用的字符串保存起來,目的就是爲了減少不斷申請釋放時CPU時間,內存碎片等等。
比如 shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"))
。
額外說明的是,這裏還初始化了一個很大的共享數字對象,0 到 999。因此在設置 value 時可以使用這些數字可以減少內存的使用。
#define OBJ_SHARED_INTEGERS 10000
for (j = 0; j < OBJ_SHARED_INTEGERS; j++) { // 10000 個數字
shared.integers[j] = createObject(OBJ_STRING,(void*)(long)j);
shared.integers[j]->encoding = OBJ_ENCODING_INT;
}
struct sharedObjectsStruct shared
也是一個全局變量。
adjustOpenFilesLimit 函數
該函數根據配置文件中配置的最大 client 數量增大可以打開的最多文件數。
創建 eventLoop
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR)
這裏假設 io 多路複用使用的是 epoll,這也是用的最多的。
初始化數據庫對象
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
數據庫對象 struct redisDb
,有 16 個。
監聽 port 端口
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
exit(1);
監聽 server.port
,並把返回的 fd 存儲在 server.ipfd
中,有報錯就返回。
創建系統 cron 定時器
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create the serverCron time event.");
exit(1);
}
註冊定時時間,綁定回調函數 serverCron
,在該函數中我們可以看到,執行週期爲 1000/server.hz
ms,因此每秒會執行server.hz
(該值用戶可配)。
那爲什麼是這個頻率呢?redis 中對於事件處理在之前的一篇博客中寫過,可以參考下 Redis 中的事件,這裏也可以簡單回顧下。
時間事件處理函數 ae.c
-> processTimeEvents
中,會根據時間事件的回調返回值來決定這時一個週期事件還是一次性事件,即
{
int retval;
id = te->id;
retval = te->timeProc(eventLoop, id, te->clientData);
processed++;
if (retval != AE_NOMORE) {
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
te->id = AE_DELETED_EVENT_ID;
}
}
監聽/接收用戶請求
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.");
}
}
接收用戶請求(用戶連接會從這裏進來),監聽可讀事件,註冊回調函數 acceptTcpHandler
。
cluster 初始化
如果開啓了 cluster mode,會進行相應的初始化。
if (server.cluster_enabled) clusterInit();
其他環境初始化
replicationScriptCacheInit();
scriptingInit(1);
slowlogInit();
latencyMonitorInit();
bioInit();
設置進程名
這個函數很實用的,方便 ps 看到良好格式的進程名。一起來複習下。
void redisSetProcTitle(char *title) {
#ifdef USE_SETPROCTITLE
char *server_mode = "";
if (server.cluster_enabled) server_mode = " [cluster]";
else if (server.sentinel_mode) server_mode = " [sentinel]";
setproctitle("%s %s:%d%s",
title,
server.bindaddr_count ? server.bindaddr[0] : "*",
server.port,
server_mode);
#else
UNUSED(title);
#endif
}
加載持久化數據
如果不是以 sentinel 模式啓動的,那麼會加載持久化的數據,處理函數爲 loadDataFromDisk
。
如果開啓了 aof,那麼就加載 aof 文件,否則加載 rdb 文件。
loadAppendOnlyFile
該函數用來記載 aof 文件,主要流程就是創建一個僞客戶端,從 aof 文件中解析出來命令,讓 server 重新執行一遍。
if (buf[0] != '*') goto fmterr; // 判斷協議是否正確
if (buf[1] == '\0') goto readerr; // 判斷數據完整判斷
argc = atoi(buf+1);
if (argc < 1) goto fmterr;
argv = zmalloc(sizeof(robj*)*argc); // argc 個 robj 對象
fakeClient->argc = argc;
fakeClient->argv = argv;
for (j = 0; j < argc; j++) {
if (fgets(buf,sizeof(buf),fp) == NULL) { // 每行最多 128B
fakeClient->argc = j; /* Free up to j-1. */
freeFakeClientArgv(fakeClient);
goto readerr;
}
if (buf[0] != '$') goto fmterr;
len = strtol(buf+1,NULL,10); // 命令的長度
argsds = sdsnewlen(NULL,len);
if (len && fread(argsds,len,1,fp) == 0) {
sdsfree(argsds);
fakeClient->argc = j; /* Free up to j-1. */
freeFakeClientArgv(fakeClient);
goto readerr;
}
argv[j] = createObject(OBJ_STRING,argsds);
if (fread(buf,2,1,fp) == 0) { // \r\n
fakeClient->argc = j+1; /* Free up to j. */
freeFakeClientArgv(fakeClient);
goto readerr; /* discard CRLF */
}
}
cmd = lookupCommand(argv[0]->ptr);
if (!cmd) {
serverLog(LL_WARNING,"Unknown command '%s' reading the append only file", (char*)argv[0]->ptr);
exit(1);
}
// 用 fakeClient 執行命令
cmd->proc(fakeClient);
以上函數就是 aof 文件解析過程。
附上一段 redis 協議數據,方便分析函數。
*3
$3
SET
$2
xx
$2
yy
*3
注意:在加載 aof 文件過程中,會暫時關閉 aof。
rdbLoad
該函數用來加載 rdb 文件。與 aof 加載不同的是,解析 rdb 文件後直接放入內存中。
事件循環初始化
// 進入事件循環之前執行 beforeSleep() 函數
aeSetBeforeSleepProc(server.el,beforeSleep);
// 開始事件循環
aeMain(server.el);
// 服務器關閉,刪除事件循環
aeDeleteEventLoop(server.el);
小結
畫了一個流程圖,可以很好的體現以上流程。