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