現象 redis作為緩存場景使用,記憶體耗盡時,突然出現大量的逐出,在這個逐出的過程中阻塞正常的讀寫請求,導致 redis 短時間不可用; 背景 redis 中的LRU是如何實現的? 1. 當mem_used記憶體已經超過maxmemory的設定,對於所有的讀寫請求,都會觸發redis.c/freeMe ...
現象
redis作為緩存場景使用,記憶體耗盡時,突然出現大量的逐出,在這個逐出的過程中阻塞正常的讀寫請求,導致 redis 短時間不可用;
背景
redis 中的LRU是如何實現的?
- 當mem_used記憶體已經超過maxmemory的設定,對於所有的讀寫請求,都會觸發redis.c/freeMemoryIfNeeded(void)函數以清理超出的記憶體。
- 這個清理過程是阻塞的,直到清理出足夠的記憶體空間。
- 這裡的LRU或TTL策略並不是針對redis的所有key,而是以配置文件中的maxmemory-samples個key作為樣本池進行抽樣清理。
maxmemory-samples在redis-3.0.0中的預設配置為5,如果增加,會提高LRU或TTL的精準度,redis作者測試的結果是當這個配置為10時已經非常接近全量LRU的精準度.
原因
逐出qps突增非常大的原因:一次需要逐出釋放太多的空間會導致阻塞;具體的原因是 mem_tofree 的計算邏輯有問題;
mem_tofree 統計的是:實際已分配的記憶體總量 - AOF 緩衝區相關的記憶體;
如果這時候有rehash,會臨時分配一個桶來做rehash,這部分記憶體未排除,所以在rehash階段,算出來的mem_tofree 就會很大,造成一個時刻需要逐出大量的key,逐出的loop是阻塞的,這個階段會block redis的請求;
逐出qps的計算:
freeMemoryIfNeeded(...)
// 計算出 Redis 目前占用的記憶體總數,但有兩個方面的記憶體不會計算在內:
// 1)從伺服器的輸出緩衝區的記憶體
// 2)AOF 緩衝區的記憶體
// 3)AOF 重寫緩衝區中的記憶體
mem_used = zmalloc_used_memory();
if (slaves) {
listIter li;
listNode *ln;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = listNodeValue(ln);
unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
if (obuf_bytes > mem_used)
mem_used = 0;
else
mem_used -= obuf_bytes;
}
}
if (server.aof_state != REDIS_AOF_OFF) {
mem_used -= sdslen(server.aof_buf);
mem_used -= aofRewriteBufferSize();
}
// 計算需要釋放多少位元組的記憶體
mem_tofree = mem_used - server.maxmemory;
propagateExpire(db,keyobj);
// 計算刪除鍵所釋放的記憶體數量
delta = (long long) zmalloc_used_memory();
dbDelete(db,keyobj);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta;
// 對淘汰鍵的計數器增一
server.stat_evictedkeys++;
解決方案
github上 @Rosanta 給出的解決方案:釋放記憶體的迴圈邏輯中最多執行一定次數,達到閾值了就不再逐出,到下個請求來時再釋放一點空間;這個方案的好處是不會 block 整個進程,正常的業務讀寫請求無影響;潛在問題是可能單次寫入的數據比釋放的空間還大,導致總的記憶體是一直上升,而不是下降;
@antirez 給的方案:同樣是迭代刪除,但會加個標誌,保證在迭代刪除的邏輯下記憶體是逐漸下降的,而如果是上升的,還是會block住正常的請求(要控制主總的記憶體大小);
詳見:
https://github.com/antirez/redis/pull/4583
ref
關於 redis 4.0的逐出演算法優化
http://antirez.com/news/109