碰到一個悲催的事情:一台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,決定是否放行。