redis中伺服器流程分析
redis中伺服器流程分析
1 背景
前面總結了redis中資料庫的鍵空間和redis中的事件模型,本節總結下Redis如何處理命令?以及Redis中重要的serverCron
週期性事件。
本篇主要介紹幾面幾個點:
- Redis初始化過程
- 連線請求、處理命令、傳送回覆的流程
- serverCron週期性事件的內部流程
2 initServer初始化伺服器
伺服器初始化過程主要有以下幾步:
- 初始化redisServer結構體
- 讀取配置資訊
- 為相應的資料結構分配空間
- 讀取AOF或者RDB恢復資料庫狀態
- 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函式流程:
- 開啟AOF檔案
- 建立偽客戶端
- 讀取一行命令
- 使用偽客戶端執行命令
- 檔案是否讀取完畢?否則繼續執行3,是則下一步
- 關閉檔案,釋放偽客戶端
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 命令請求
先說下命令總體的執行流程:
- 客戶端鍵入命令,轉換為Redis的協議格式,傳送給伺服器
- 伺服器的可寫事件被啟用,呼叫readQueryFromClient讀入命令,儲存到redisClient.queryBuf緩衝區中
- 分析client緩衝區中的命令請求,查詢命令表,儲存到redisClient.cmd中
- 呼叫命令的執行函式,redisCommand.proc
- 做後續工作,比如慢查詢日誌、AOF工作
- 向客戶端返回回覆
下面分步來說。
客戶端伺服器通訊協議
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);
}
}
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中完成眾多工作,會定期對伺服器自身的狀態進行檢查和調整,
- 更新伺服器的各類統計資訊,比如時間、記憶體佔用、每秒執行的命令次數
- 呼叫databaseCron,清理資料庫過期鍵值,對字典進行收縮操作
- 處理SIGTERM訊號,關閉伺服器
- 呼叫clientCron,關閉和清理失效客戶端
- 執行被延遲的BGRWRITEAOF,因為在BGSAVE期間,客戶端的BGRWRITEAOF會被延遲
- 檢查BGSAVE和BGRWRITEAOF子程式的執行狀態,如果已經執行完,則需要執行後續步驟
- 將AOF緩衝區內容寫入AOF檔案
- 如果是主伺服器,則定義對從伺服器同步
- 叢集模式,則定期同步和連線測試
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
- 黃健巨集. Redis設計與實現[M]. 機械工業出版社, 2014.
相關文章
- redis中multi與pipeline介紹分析Redis
- 【redis】關於查詢和分析redis中的bigkeys問題Redis
- printk流程分析
- printk 流程分析
- Lifecycle 流程分析
- Redis複製流程:圖解Redis圖解
- 伺服器重啟流程伺服器
- Redis 中的事務分析,Redis 中的事務可以滿足ACID屬性嗎?Redis
- redis 效能分析Redis
- Flutter setState流程分析Flutter
- Shiro中Subject物件的建立與繫結流程分析物件
- Redis主從複製流程概述Redis
- Redis 分析工具 redis-rdb-toolsRedis
- 【Redis】redis阻塞監控項分析Redis
- APP中RN頁面熱更新流程-ReactNative原始碼分析APPReact原始碼
- redis client原理分析Redisclient
- Linux效能分析流程圖Linux流程圖
- View 繪製流程分析View
- nodejs啟動流程分析NodeJS
- cuttag分析流程(部分存疑)
- FlutterApp啟動流程分析FlutterAPP
- Activity啟動流程分析
- Unbound啟動流程分析
- Redis | 第7章 Redis 伺服器《Redis設計與實現》Redis伺服器
- 詳解Python 中視覺化資料分析工作流程Python視覺化
- 【Redis】redis各型別資料儲存分析Redis型別
- Redis 例項分析工具Redis
- Redux流程分析與實現Redux
- go http請求流程分析GoHTTP
- Linux:uboot啟動流程分析Linuxboot
- 執行流程原始碼分析原始碼
- WindowManager呼叫流程原始碼分析原始碼
- obs推流核心流程分析
- EGADS框架處理流程分析框架
- Redis中的事務處理機制分析與總結Redis
- 展館中數字沙盤專案的製作流程分析
- RxJava2原始碼分析(一):基本流程分析RxJava原始碼
- 嵌入式Redis伺服器在Spring Boot測試中的使用Redis伺服器Spring Boot