1. 程式人生 > >redis 突然大量逐出導致讀寫請求block

redis 突然大量逐出導致讀寫請求block

服務 long 字節 tno 過程 aof 測試的 pull 文件

現象

redis作為緩存場景使用,內存耗盡時,突然出現大量的逐出,在這個逐出的過程中阻塞正常的讀寫請求,導致 redis 短時間不可用;

背景

redis 中的LRU是如何實現的?

  1. 當mem_used內存已經超過maxmemory的設定,對於所有的讀寫請求,都會觸發redis.c/freeMemoryIfNeeded(void)函數以清理超出的內存。
  2. 這個清理過程是阻塞的,直到清理出足夠的內存空間。
  3. 這裏的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

redis 突然大量逐出導致讀寫請求block