一臺Redis伺服器,4核,16G記憶體且沒有任何硬體上的問題。持續高壓運行了大約3個月,儲存了大約14G的資料,設定了比較完備的Save引數。而就是這臺主機,在一次重起之後,丟失了大量的資料,14G的資料最終只恢復了幾百兆而已。
正常情況下,像Redis這樣定期回寫磁碟的記憶體資料庫,丟失幾個資料也是在情理之中,可超過80%資料丟失率實在太離譜。排除了誤操作的可能性之後,開始尋找原因。
重啟動時的日誌:
[26641] 21 Dec 09:46:34 * Slave ask for synchronization
[26641] 21 Dec 09:46:34 * Starting BGSAVE for SYNC
[26641] 21 Dec 09:46:34 # Can’t save in background: fork: Cannot allocate memory
[26641] 21 Dec 09:46:34 * Replication failed, can’t BGSAVE
[26641] 21 Dec 09:46:34 # Received SIGTERM, scheduling shutdown…
[26641] 21 Dec 09:46:34 # User requested shutdown…
很明顯的一個問題,系統不能在後臺儲存,fork程序失敗。
翻查了幾個月的日誌,發覺系統在頻繁報錯:
[26641] 18 Dec 04:02:14 * 1 changes in 900 seconds. Saving…
[26641] 18 Dec 04:02:14 # Can’t save in background: fork: Cannot allocate memory
系統不能在後臺儲存,fork程序時無法指定記憶體。
對原始碼進行跟蹤,在src/rdb.c中定位了這個報錯:
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start; if (server.bgsavechildpid != -1) return REDIS_ERR;
if (server.vm_enabled) waitEmptyIOJobsQueue();
server.dirty_before_bgsave = server.dirty;
start = ustime();
if ((childpid = fork()) == 0) {
/* Child */
if (server.vm_enabled) vmReopenSwapFile();
if (server.ipfd > 0) close(server.ipfd);
if (server.sofd > 0) close(server.sofd);
if (rdbSave(filename) == REDIS_OK) {
_exit(0);
} else {
_exit(1);
}
} else {
/* Parent */
server.stat_fork_time = ustime()-start;
if (childpid == -1) {
redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return REDIS_ERR;
}
redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
server.bgsavechildpid = childpid;
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
資料丟失的問題總算搞清楚了!
Redis的資料回寫機制分同步和非同步兩種,
- 同步回寫即SAVE命令,主程序直接向磁盤迴寫資料。在資料大的情況下會導致系統假死很長時間,所以一般不是推薦的。
- 非同步回寫即BGSAVE命令,主程序fork後,複製自身並通過這個新的程序回寫磁碟,回寫結束後新程序自行關閉。由於這樣做不需要主程序阻塞,系統不會假死,一般預設會採用這個方法。
個人感覺方法2採用fork主程序的方式很拙劣,但似乎是唯一的方法。記憶體中的熱資料隨時可能修改,要在磁碟上儲存某個時間的記憶體映象必須要凍結。凍結就會導致假死。fork一個新的程序之後等於複製了當時的一個記憶體映象,這樣主程序上就不需要凍結,只要子程序上操作就可以了。
在小記憶體的程序上做一個fork,不需要太多資源,但當這個程序的記憶體空間以G為單位時,fork就成為一件很恐怖的操作。何況在16G記憶體的主機上fork 14G記憶體的程序呢?肯定會報記憶體無法分配的。更可氣的是,越是改動頻繁的主機上fork也越頻繁,fork操作本身的代價恐怕也不會比假死好多少。
找到原因之後,直接修改核心引數vm.overcommit_memory = 1
Linux核心會根據引數vm.overcommit_memory引數的設定決定是否放行。
- 如果 vm.overcommit_memory = 1,直接放行
- vm.overcommit_memory = 0:則比較 此次請求分配的虛擬記憶體大小和系統當前空閒的實體記憶體加上swap,決定是否放行。
- vm.overcommit_memory = 2:則會比較 程序所有已分配的虛擬記憶體加上此次請求分配的虛擬記憶體和系統當前的空閒實體記憶體加上swap,決定是否放行。