redis中伺服器流程分析

TuxedoLinux發表於2018-06-05

redis中伺服器流程分析


1 背景

前面總結了redis中資料庫的鍵空間和redis中的事件模型,本節總結下Redis如何處理命令?以及Redis中重要的serverCron週期性事件。

本篇主要介紹幾面幾個點:

  1. Redis初始化過程
  2. 連線請求、處理命令、傳送回覆的流程
  3. serverCron週期性事件的內部流程

2 initServer初始化伺服器

伺服器初始化過程主要有以下幾步:

  1. 初始化redisServer結構體
  2. 讀取配置資訊
  3. 為相應的資料結構分配空間
  4. 讀取AOF或者RDB恢復資料庫狀態
  5. aeMain事件迴圈

2.1 redisServer結構體

每個redis例項都由一個redisServer結構體來表示,其包含眾多屬性,而首先就需要初始化相關的屬性。

在main主函式中呼叫initServerConfig來完成這部分工作。

主要內容有:

  • 設定伺服器執行ID
  • 設定配置檔案路徑
  • 設定serverCron的頻率
  • 設定埠號
  • 設定位長,32 or 64
  • 設定監聽套接字屬性,比如監聽佇列長度、keepalive選項
  • 設定AOF和RDB屬性
  • 設定ziplist,intset等適用條件
  • 初始化LRU時鐘
  • 建立命令表

initServerConfig執行結束之後,會判斷是否為sentinel模式,如果是,只需要初始化sentinel相關選項。

2.2 redis config

接著首先讀取redis-server的命令引數,比如--port 6379,或者在命令列中指定redis.conf檔案。

接下來,呼叫loadServerConfig來讀取配置檔案,讀取到的資訊儲存到一個sds字串中,然後將sds字串傳遞給config.c/loadServerConfigFromString來配置。

我們使用redis時,一般在redis.conf中配置相關選項,比如pid路徑、日誌檔案、埠、AOF和RDB機制、最大記憶體等資料。

2.3 資料結構分配空間

接下來會呼叫redis.c/demonize設定為守護程式,接著呼叫redis.c/initServer為相關redisServer中結構分配空間。

initServer函式的主要流程是:

  • 設定訊號處理函式,SIGTERM
  • 開啟日誌檔案,openlog
  • 建立客戶端連線連結串列、從伺服器連結串列
  • 建立共享物件,如:常見回覆ok,err,pong,-ERR no such key,1~10000直接的常用整數等
  • 呼叫aeCreateEventLoop建立事件迴圈結構體,重要
  • 初始化server.db陣列,迴圈對每個資料庫,即redisDb結構體,進行初始化
  • 開啟監聽埠
  • 註冊監聽事件,關聯事件處理器accptTcpHandler,重要
  • 註冊serverCron週期性事件,重要
  • 開啟AOF或RDB檔案
  • 初始化指令碼系統
  • 初始化慢查詢系統
  • 初始化BIO系統

initServer執行完畢後,會建立PID檔案,設定伺服器程式名字,下一步就應該從AOF或者RDB載入資料了。

2.4 讀取AOF或者RDB

redis.c/main呼叫loadDataFromDisk函式,該函式內部根據AOF,還是RDB,呼叫loadAppendOnlyFile或者rdbLoad來載入檔案。

AOF檔案載入流程,即loadAppendOnlyFile函式流程:

  1. 開啟AOF檔案
  2. 建立偽客戶端
  3. 讀取一行命令
  4. 使用偽客戶端執行命令
  5. 檔案是否讀取完畢?否則繼續執行3,是則下一步
  6. 關閉檔案,釋放偽客戶端

RDB檔案載入流程,即rdbLoad函式流程大概為:開啟檔案,迴圈讀取檔案的databases部分(需要了解RDB檔案的儲存結構),直到EOF結束。

2.5 主迴圈

執行完上述初始化過程後,redis.c/main呼叫aeMain進行事件迴圈,去處理檔案事件和週期性時間事件。

3 連線請求、命令處理、傳送回覆

3.1 連線請求

客戶端的連線請求由acceptTcpHandler來處理,該函式接受連線、建立客戶端、註冊客戶端的命令請求與回覆處理器、新增到server.clients連結串列中。

監聽套接字的請求處理器每次執行可以處理多個連線請求:

/*
 * 處理連線請求
 */
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[REDIS_IP_STR_LEN];
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    REDIS_NOTUSED(privdata);
    //處理多個連線請求,至多MAX_ACCEPTS_PER_CALL,當連線沒準備好時break(非阻塞)
    while(max--) {
        // accept 客戶端連線
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                redisLog(REDIS_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
        // 為客戶端建立客戶端狀態(redisClient)、新增到server.clients連結串列,並註冊讀寫事件處理器
        acceptCommonHandler(cfd,0);
    }
}

3.2 命令請求

先說下命令總體的執行流程:

  1. 客戶端鍵入命令,轉換為Redis的協議格式,傳送給伺服器
  2. 伺服器的可寫事件被啟用,呼叫readQueryFromClient讀入命令,儲存到redisClient.queryBuf緩衝區中
  3. 分析client緩衝區中的命令請求,查詢命令表,儲存到redisClient.cmd中
  4. 呼叫命令的執行函式,redisCommand.proc
  5. 做後續工作,比如慢查詢日誌、AOF工作
  6. 向客戶端返回回覆

下面分步來說。

客戶端伺服器通訊協議

Redis客戶端與伺服器的通訊協議是如下格式:

*<引數數量>
$<引數 1 的位元組數量> CR LF
<引數 1 的資料> CR LF
...
$<引數 N 的位元組數量> CR LF
<引數 N 的資料> CR LF

舉個栗子:

執行命令:set mykey myvalue

該命令的協議如下:

*3
$3
SET
$5
mykey
$7
myvalue</pre>

在redisClient.queryBuf儲存為:"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"

readQueryFromClient讀入命令

/*
 * 讀取客戶端的查詢緩衝區內容
 */
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = (redisClient*) privdata;
    int nread, readlen;
    size_t qblen;
    // ...........省略
    // 分配空間,讀入內容
    qblen = sdslen(c->querybuf);
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    nread = read(fd, c->querybuf+qblen, readlen);

    // 讀入出錯
    if (nread == -1) {
        if (errno == EAGAIN) { //非阻塞EAGIN
            nread = 0;
        } else {
            redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    // read返回0,可能是客戶端關閉了連線
    } else if (nread == 0) {
        redisLog(REDIS_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    }
    if (nread) {
        // 根據內容,更新查詢緩衝區(SDS) free 和 len 屬性,並將 '\0' 正確地放到內容的最後
        sdsIncrLen(c->querybuf,nread);
        // 記錄伺服器和客戶端最後一次互動的時間
        c->lastinteraction = server.unixtime;
        // 如果客戶端是 master 的話,更新它的複製偏移量
        if (c->flags & REDIS_MASTER) c->reploff += nread;
    } else {
        // 在 nread == -1 且 errno == EAGAIN 時執行
        server.current_client = NULL;
        return;
    }
    // ...省略部分
	
    // 從查詢快取重讀取內容,建立引數,並執行命令
    // 函式會執行到快取中的所有內容都被處理完為止
    processInputBuffer(c);

    server.current_client = NULL;
}

processInputBuffer處理緩衝區

其中會迴圈讀取client.queryBuf,呼叫processInlineBuffer來解析命令,然後呼叫processCommand執行命令,重置客戶端。

該過程可以如下虛擬碼表示:

void processInputBuffer(redisClient *c) {
    //迴圈讀取緩衝區資料
    while(sdslen(c->querybuf)) {
	    //解析緩衝區命令
		processInlineBuffer(c);
		//執行命令
		processCommand(c);
		//重置客戶端
		resetClient(c);
	}
}
重點說一下redisCommand結構
struct redisCommand {
    // 命令名字
    char *name;
    // 實現函式
    redisCommandProc *proc;
	//....

伺服器通過processInlineBuffer解析命令後,儲存到client.argc和client.argv中,然後呼叫processCommand,其內部會呼叫lookupCommand去查詢命令表。

命令表是一個字典,儲存在redisServer.commands中,字典的鍵是一個命令名字,比如set,get,del等;字典值是一個redisComand結構體,其記錄了一個redis命令的實現資訊。

查表的過程很簡單,直接通過dictFetchValue即可,將查詢結果儲存到client.cmd中。

/*
 * 根據給定命令名字(SDS),查詢命令
 */
struct redisCommand *lookupCommand(sds name) {
    return dictFetchValue(server.commands, name);
}

執行命令

processCommand中呼叫call函式,執行命令實現函式:

call(c,REDIS_CALL_FULL); --> c->cmd->proc(c);

call函式中更新統計資訊

call函式是真正執行命令的地方,是redis的核心函式,除了執行命令,還會統計命令執行時間,寫入慢查詢日誌,傳送給監視器,寫入AOF,傳播到slave節點。

// 呼叫命令的實現函式,執行命令
void call(redisClient *c, int flags) {
    //Note:其中省略了部分函式
	//轉發給監視器
    replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
    // 計算命令開始執行的時間
    start = ustime();
    // 執行實現函式
    c->cmd->proc(c);
    // 計算命令執行耗費的時間
    duration = ustime()-start;
    // 計算命令執行之後的 dirty 值
    dirty = server.dirty-dirty;
    // 如果有需要,將命令放到 SLOWLOG 裡面
    if (flags & REDIS_CALL_SLOWLOG && c->cmd->proc != execCommand)
        slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
    // 更新命令的統計資訊
    if (flags & REDIS_CALL_STATS) {
        c->cmd->microseconds += duration;
        c->cmd->calls++;
    }
    // 將命令複製到 AOF 和 slave 節點
    if (flags & REDIS_CALL_PROPAGATE) {
            propagate(c->cmd,c->db->id,c->argv,c->argc,flags);
    }
    server.stat_numcommands++;
}

回覆客戶端

在呼叫命令實現函式後,比如setCommand,它會呼叫addReply將回復儲存到客戶端的輸出緩衝區中,並註冊客戶端套接字的可寫事件,關聯到sendReplyToClient事件處理器,當套接字可寫時,會執行networking.c/sendReplyToClient,傳送回覆給客戶端。

經過上述步驟,就完成了Redis命令的執行過程。

4 週期性事件serverCron內部流程

redis在初始化過程中註冊了serverCron週期性事件,頻率預設hz=10,即每100毫秒執行一次。

serverCron中完成眾多工作,會定期對伺服器自身的狀態進行檢查和調整,

  1. 更新伺服器的各類統計資訊,比如時間、記憶體佔用、每秒執行的命令次數
  2. 呼叫databaseCron,清理資料庫過期鍵值,對字典進行收縮操作
  3. 處理SIGTERM訊號,關閉伺服器
  4. 呼叫clientCron,關閉和清理失效客戶端
  5. 執行被延遲的BGRWRITEAOF,因為在BGSAVE期間,客戶端的BGRWRITEAOF會被延遲
  6. 檢查BGSAVE和BGRWRITEAOF子程式的執行狀態,如果已經執行完,則需要執行後續步驟
  7. 將AOF緩衝區內容寫入AOF檔案
  8. 如果是主伺服器,則定義對從伺服器同步
  9. 叢集模式,則定期同步和連線測試

serverCron內部原始碼

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    int j;

    if (server.watchdog_period) watchdogScheduleSignal(server.watchdog_period);

    /* Update the time cache. */
    updateCachedTime();

    // 記錄伺服器執行命令的次數
    run_with_period(100) trackOperationsPerSecond();

	//更新LRU時間
    server.lruclock = getLRUClock();

    // 記錄伺服器的記憶體峰值
    if (zmalloc_used_memory() > server.stat_peak_memory)
        server.stat_peak_memory = zmalloc_used_memory();

    // 伺服器程式收到 SIGTERM 訊號,會在sigtermHandler中開啟shutdown_asap標誌,在此處則關閉伺服器
    if (server.shutdown_asap) {

        // 嘗試關閉伺服器
        if (prepareForShutdown(0) == REDIS_OK) exit(0);

        // 如果關閉失敗,那麼列印 LOG ,並移除關閉標識
        redisLog(REDIS_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
        server.shutdown_asap = 0;
    }

    // 列印資料庫的鍵值對資訊
    run_with_period(5000) {
        for (j = 0; j < server.dbnum; j++) {
            long long size, used, vkeys;

            // 可用鍵值對的數量
            size = dictSlots(server.db[j].dict);
            // 已用鍵值對的數量
            used = dictSize(server.db[j].dict);
            // 帶有過期時間的鍵值對數量
            vkeys = dictSize(server.db[j].expires);

            // 用 LOG 列印數量
            if (used || vkeys) {
                redisLog(REDIS_VERBOSE,"DB %d: %lld keys (%lld volatile) in %lld slots HT.",j,used,vkeys,size);
                /* dictPrintStats(server.dict); */
            }
        }
    }

    // 如果伺服器沒有執行在 SENTINEL 模式下,那麼列印客戶端的連線資訊
    if (!server.sentinel_mode) {
        run_with_period(5000) {
            redisLog(REDIS_VERBOSE,
                "%lu clients connected (%lu slaves), %zu bytes in use",
                listLength(server.clients)-listLength(server.slaves),
                listLength(server.slaves),
                zmalloc_used_memory());
        }
    }

    // 檢查客戶端,關閉超時客戶端,並釋放客戶端多餘的緩衝區
    clientsCron();

    // 對資料庫執行各種操作,刪除過期鍵,縮小字典
    databasesCron();

    // 如果 BGSAVE 和 BGREWRITEAOF 都沒有在執行,並且有一個 BGREWRITEAOF 在等待,那麼執行被延遲的BGREWRITEAOF
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
        server.aof_rewrite_scheduled)
    {
        rewriteAppendOnlyFileBackground();
    }

    // 檢查 BGSAVE 或者 BGREWRITEAOF子程式是否已經執行完畢
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
        int statloc;
        pid_t pid;

        // 接收子程式發來的訊號,非阻塞
        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
            int exitcode = WEXITSTATUS(statloc);
            int bysignal = 0;

            if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);

            // BGSAVE 執行完畢
            if (pid == server.rdb_child_pid) {
                backgroundSaveDoneHandler(exitcode,bysignal);

            // BGREWRITEAOF 執行完畢
            } else if (pid == server.aof_child_pid) {
                backgroundRewriteDoneHandler(exitcode,bysignal);

            } else {
                redisLog(REDIS_WARNING,
                    "Warning, detected child with unmatched pid: %ld",
                    (long)pid);
            }
            updateDictResizePolicy();
        }
    } else {

        // 既然沒有 BGSAVE 或者 BGREWRITEAOF 在執行,那麼檢查是否需要執行它們
        // 即:判斷是否滿足變化引數,遍歷所有儲存條件,看是否需要執行 BGSAVE 命令
         for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;
            // 檢查是否有某個儲存條件已經滿足了
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                (server.unixtime-server.lastbgsave_try >
                 REDIS_BGSAVE_RETRY_DELAY ||
                 server.lastbgsave_status == REDIS_OK))
            {
                redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
                    sp->changes, (int)sp->seconds);
                // 執行 BGSAVE
                rdbSaveBackground(server.rdb_filename);
                break;
            }
         }

        // 判斷是否需要進行AOF重寫,是則觸發BGREWRITEAOF
         if (server.rdb_child_pid == -1 &&
             server.aof_child_pid == -1 &&
             server.aof_rewrite_perc &&
             // AOF 檔案的當前大小大於執行 BGREWRITEAOF 所需的最小大小
             server.aof_current_size > server.aof_rewrite_min_size)
         {
            // 上一次完成 AOF 寫入之後,AOF 檔案的大小
            long long base = server.aof_rewrite_base_size ?
                            server.aof_rewrite_base_size : 1;

            // AOF 檔案當前的體積相對於 base 的體積的百分比
            long long growth = (server.aof_current_size*100/base) - 100;

            // 如果增長體積的百分比超過了 growth ,那麼執行 BGREWRITEAOF
            if (growth >= server.aof_rewrite_perc) {
                redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                // 執行 BGREWRITEAOF
                rewriteAppendOnlyFileBackground();
            }
         }
    }

    // 根據 AOF 政策,考慮是否需要將 AOF 緩衝區中的內容寫入到 AOF 檔案中
    run_with_period(1000) {
        if (server.aof_last_write_status == REDIS_ERR)
            flushAppendOnlyFile(0);
    }

    // 關閉那些需要非同步關閉的客戶端
    freeClientsInAsyncFreeQueue();

    // 清除被暫停的客戶端
    clientsArePaused();

    // 主從複製的cron函式,週期性執行,預設1秒一次
    // 重連線主伺服器、向主伺服器傳送 ACK 、判斷資料傳送失敗情況、斷開本伺服器超時的從伺服器,等等
    run_with_period(1000) replicationCron();

    // 叢集的cron函式,週期性執行,預設1秒10次,向各個節點傳送PING訊息進行故障檢測
    run_with_period(100) {
        if (server.cluster_enabled) clusterCron();
    }

    // sentinel 模式下的cron函式,週期性的傳送INFO命令、PING命令、執行故障轉移等
    run_with_period(100) {
        if (server.sentinel_mode) sentinelTimer();
    }

    // 叢集操作相關,不懂此處
    run_with_period(1000) {
        migrateCloseTimedoutSockets();
    }

    // 增加 loop 計數器
    server.cronloops++;

    //返回時間間隔,代表週期性時間
    return 1000/server.hz;
}

5 Rerference

  1. 黃健巨集. Redis設計與實現[M]. 機械工業出版社, 2014.

相關文章