Redis核心原理與實踐--Redis啟動過程原始碼分析
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核心原理與實踐》
京東連結