Redis核心原理與實踐--Redis啟動過程原始碼分析

binecy發表於2021-10-28

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核心原理與實踐》
京東連結

相關文章