曹工說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 函式。
該函式的大的流程,按照程式碼中的執行順序,我們先了解下:
-
註冊一個watchdog,註冊方式是通過一個timer,註冊了該timer之後,會定期給當前程序,觸發一個
SIGALRM
訊號,觸發了這個訊號後,會幹嘛呢,會回撥位於 debug.c 檔案中的watchdogSignalHandler
方法,這個方法,主要是在redis執行一些命令時,超過指定時長後,列印一些debug日誌。可以參考:
Redis 2.6 的新特性:Watchdog(看門狗)
Redis software watchdog
-
更新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中。
-
計算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 = 一段時間內的運算元量/ 時間長度。
-
重新整理伺服器的 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策略理解
-
記錄伺服器的記憶體峰值
/* 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命令裡看到使用了。
-
判斷伺服器的關閉標識是否開啟,如開啟,則關閉
// 伺服器程序收到 SIGTERM 訊號,關閉伺服器 if (server.shutdown_asap) { // 嘗試關閉伺服器 if (prepareForShutdown(0) == REDIS_OK) exit(0); }
-
列印資料庫的鍵值對資訊、客戶端資訊
單純的log操作,唯一注意的是,要把日誌級別調到
REDIS_VERBOSE
才看得到 -
檢查客戶端空閒時長,關閉空閒超時的客戶端
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; } ... }
-
對資料庫執行各種操作
/* 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調整(如果字典的使用率低,會縮小其佔用的記憶體大小)
後續會詳解這部分。
-
如果當前沒有aof或者rdb後臺任務正在執行,且server之前被schedule了一個aof rewrite後臺任務,則執行
aof 重寫。(aof記錄了每一條命令,時間長了,會重複,比如先把key a設為1,再設為2,再設為3,這樣,aof中有3條記錄,實際上,只需要一條即可,所以會重寫)
aof 重寫在一個子程序中進行,子程序完成後,會給當前程序傳送訊號,所以,當前程序會一直等待訊號,等待子程序完成後,自己再做些處理。
比如,主程序要做什麼處理呢?在 aof 重寫期間,主程序可能還是要不斷地處理命令(這裡不會無限期等待,這次等不到就到下一次週期任務時再等),這期間,處理的命令,不能記錄到aof檔案中,免得影響正在進行aof 重寫的子程序,所以,主程序會把這期間的命令,記錄到一個小本本上。
等到子程序寫完了,主程序再把小本本上的aof命令,寫到aof日誌檔案裡。
-
如果當前沒有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
-
根據配置的aof fsync策略,決定是否要重新整理到檔案中
前面我們說的aof寫日誌檔案,不一定真的就寫入了檔案,可能還在OS cache中,需要呼叫 fsync 才能寫入到檔案中。
這裡即對應配置檔案中的:
# appendfsync always appendfsync everysec # appendfsync no
預設每秒執行一次fsync,效能和資料安全性的折衷。
-
涉及slave、cluster、sentinel的部分操作
如果執行在以上幾種模式下,會涉及到對應的一些週期操作,後續再涉及這塊。
總結
本講的主題大概是這些,其中,細節部分,比如資料庫的週期任務等,留待下講繼續