曹工說Redis源碼(7)– redis server 的周期執行任務,到底要做些啥

文章導航

Redis源碼系列的初衷,是幫助我們更好地理解Redis,更懂Redis,而怎麼才能懂,光看是不夠的,建議跟著下面的這一篇,把環境搭建起來,後續可以自己閱讀源碼,或者跟著我這邊一起閱讀。由於我用c也是好幾年以前了,些許錯誤在所難免,希望讀者能不吝指出。

曹工說Redis源碼(1)– redis debug環境搭建,使用clion,達到和調試java一樣的效果

曹工說Redis源碼(2)– redis server 啟動過程解析及簡單c語言基礎知識補充

曹工說Redis源碼(3)– redis server 啟動過程完整解析(中)

曹工說Redis源碼(4)– 通過redis server源碼來理解 listen 函數中的 backlog 參數

曹工說Redis源碼(5)– redis server 啟動過程解析,以及EventLoop每次處理事件前的前置工作解析(下)

曹工說Redis源碼(6)– redis server 主循環大體流程解析

本講主題

本講,聚焦於redis的周期執行任務。redis啟動起來後,基本就剩下兩件事,上一講的主流程分析中,已經講到了。1個是處理客戶端請求,2就是指向周期任務。處理客戶端請求,大概會細分為:處理客戶端連接事件(客戶端連接到redis)、客戶端讀寫事件(客戶端發送請求,redis返迴響應);

周期任務呢,就是本講主題,let’s go。

周期任務的大體流程

周期任務,上一講已經提到,其就是一個函數指針,具體實現,就是redis.c中的 serverCron 函數。

該函數的大的流程,按照程式碼中的執行順序,我們先了解下:

  1. 註冊一個watchdog,註冊方式是通過一個timer,註冊了該timer之後,會定期給當前進程,觸發一個SIGALRM訊號,觸發了這個訊號後,會幹嘛呢,會回調位於 debug.c 文件中的 watchdogSignalHandler方法,這個方法,主要是在redis執行一些命令時,超過指定時長後,列印一些debug日誌。

    可以參考:

    Redis 2.6 的新特性:Watchdog(看門狗)

    Redis software watchdog

  2. 更新server時間,redis server在很多時候,都需要獲取當前時間,就像我們寫業務程式碼差不多,但是,redis比較扣,扣什麼?扣性能。在不需要獲取當前時間的時候,redis覺得,獲取一個不那麼準確的時間就行了。所以,就快取了一個全局時間,這個全局時間,什麼時候刷新呢,就在這個周期任務中。

    大家仔細看注釋吧:

    /* We take a cached value of the unix time in the global state because with
     * virtual memory and aging there is to store the current time in objects at
     * every object access, and accuracy is not needed. To access a global var is
     * a lot faster than calling time(NULL) */
    void updateCachedTime(void) {
        server.unixtime = time(NULL);
        server.mstime = mstime();
    }
    

    簡單翻譯下,就是說,每個對象,每次被訪問的時候,有個access-time,這個時間,不需要那麼精確,沒必要每次去new date(),使用快取的時間就行了,這樣能比較快。全局時間,快取在server.unixtime 和 server.mstime中。

  3. 計算redis的ops,類似於tps;這個操作,不是每次該周期任務時,都要執行,而是自定義執行的周期,總體來說,沒有本周期任務那麼頻繁。

    redis中,定義了一個宏來實現這個功能,比如:

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

    這個就是,每100ms執行一次上面的這個操作。

    這個怎麼去計算ops(operation per second)呢?看下面的程式碼即懂:

    void trackOperationsPerSecond(void) {
    
        // 計算兩次抽樣之間的時間長度,毫秒格式
        long long t = mstime() - server.ops_sec_last_sample_time;
    
        // 計算兩次抽樣之間,執行了多少個命令
        long long ops = server.stat_numcommands - server.ops_sec_last_sample_ops;
    
        long long ops_sec;
    
        //1 計算距離上一次抽樣之後,每秒執行命令的數量
        ops_sec = t > 0 ? (ops * 1000 / t) : 0;
    	...
    }
    

    1處,分子分母,大家一看,應該就懂了。ops = 一段時間內的操作數量/ 時間長度。

  4. 刷新伺服器的 LRU 時間,目前,我覺得可以簡單理解為:redis的空間大小是有限的,假設機器記憶體10g,那麼不可能把資料庫的幾個t的數據都放redis,所以基本是放熱數據,那不熱的數據怎麼辦?被清除。清除的演算法,就是lru。每個key,不管設沒設過期時間,都會維護一個lruClock,即最近一次被訪問的時間。

    計算一個對象的空閑時長,就是用伺服器的LRU時間 減去 key的LRU時間。

    // 使用近似 LRU 演算法,計算出給定對象的閑置時長
    unsigned long long estimateObjectIdleTime(robj *o) {
        unsigned long long lruclock = LRU_CLOCK();
        if (lruclock >= o->lru) {
            return (lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;
        } else {
            return (lruclock + (REDIS_LRU_CLOCK_MAX - o->lru)) *
                        REDIS_LRU_CLOCK_RESOLUTION;
        }
    }
    

    網上的一篇文章寫得不錯,可以參考:

    redis的LRU策略理解

  5. 記錄伺服器的記憶體峰值

        /* Record the max memory used since the server was started. */
        // 記錄伺服器的記憶體峰值
        if (zmalloc_used_memory() > server.stat_peak_memory)
            server.stat_peak_memory = zmalloc_used_memory();
    

    什麼時候用呢?好像只在info命令里看到使用了。

  6. 判斷伺服器的關閉標識是否打開,如打開,則關閉

    // 伺服器進程收到 SIGTERM 訊號,關閉伺服器
        if (server.shutdown_asap) {
            // 嘗試關閉伺服器
            if (prepareForShutdown(0) == REDIS_OK) exit(0);
         }
    
  7. 列印資料庫的鍵值對資訊、客戶端資訊

    單純的log操作,唯一注意的是,要把日誌級別調到REDIS_VERBOSE才看得到

  8. 檢查客戶端空閑時長,關閉空閑超時的客戶端

    int clientsCronHandleTimeout(redisClient *c) {
    
        // 獲取當前時間
        time_t now = server.unixtime;
    
        // 伺服器設置了 maxidletime 時間
        if (server.maxidletime &&
            ...
            // 客戶端最後一次與伺服器通訊的時間已經超過了 maxidletime 時間
            (now - c->lastinteraction > server.maxidletime)) {
            redisLog(REDIS_VERBOSE, "Closing idle client");
            // 關閉超時客戶端
            freeClient(c);
            return 1;
        }
        ...
    }
    
  9. 對資料庫執行各種操作

    /* This function handles 'background' operations we are required to do
     * incrementally in Redis databases, such as active key expiring, resizing,
     * rehashing. */
    // 對資料庫執行刪除過期鍵,調整大小,以及主動和漸進式 rehash
    void databasesCron(void)
    

    看注釋可知,大概有如下工作:刪除過期key,hash表的rehash,hash的size調整(如果字典的使用率低,會縮小其佔用的記憶體大小)

    後續會詳解這部分。

  10. 如果當前沒有aof或者rdb後台任務正在執行,且server之前被schedule了一個aof rewrite後台任務,則執行

    aof 重寫。(aof記錄了每一條命令,時間長了,會重複,比如先把key a設為1,再設為2,再設為3,這樣,aof中有3條記錄,實際上,只需要一條即可,所以會重寫)

    aof 重寫在一個子進程中進行,子進程完成後,會給當前進程發送訊號,所以,當前進程會一直等待訊號,等待子進程完成後,自己再做些處理。

    比如,主進程要做什麼處理呢?在 aof 重寫期間,主進程可能還是要不斷地處理命令(這裡不會無限期等待,這次等不到就到下一次周期任務時再等),這期間,處理的命令,不能記錄到aof文件中,免得影響正在進行aof 重寫的子進程,所以,主進程會把這期間的命令,記錄到一個小本本上。

    等到子進程寫完了,主進程再把小本本上的aof命令,寫到aof日誌文件里。

  11. 如果當前沒有aof或者rdb後台任務在執行,也沒有被schedule 一個aof rewrite任務,那麼,上面這步中的全部操作,都不會發生。

    此時,會去檢查,當前是否滿足aof 重寫、rdb 保存的條件。

    比如,rdb不是一般需要配置如下參數嗎:

    save 900 1
    save 300 10
    save 60 10000
    

    此時,就會去檢查,這些參數,是否滿足,如果滿足,就要開始進行rdb後台保存。

    或者,當以下的aof參數滿足時,也會觸發aof重寫:

    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    
  12. 根據配置的aof fsync策略,決定是否要刷新到文件中

    前面我們說的aof寫日誌文件,不一定真的就寫入了文件,可能還在OS cache中,需要調用 fsync 才能寫入到文件中。

    這裡即對應配置文件中的:

    # appendfsync always
    appendfsync everysec
    # appendfsync no
    

    默認每秒執行一次fsync,性能和數據安全性的折衷。

  13. 涉及slave、cluster、sentinel的部分操作

    如果運行在以上幾種模式下,會涉及到對應的一些周期操作,後續再涉及這塊。

總結

本講的主題大概是這些,其中,細節部分,比如資料庫的周期任務等,留待下講繼續。