8. redis的故障診斷與優化
8.1 常見緩存問題
8.1.1 緩存穿透
- 什麼叫緩存穿透?
一般的緩存系統,都是按照key去緩存查詢,如果不存在對應的value,就應該去後端系統查找(比如DB)。如果key對應的value是一定不存在的,並且對該key併發請求量很大,就會對後端系統造成很大的壓力。
也就是說,對不存在的key進行高併發訪問,導致數據庫壓力瞬間增大,這就叫做【緩存穿透】。
- 如何解決?
1:對查詢結果為空的情況也進行緩存,緩存時間設置短一點,或者該key對應的數據insert了之後清理緩存。
2:對一定不存在的key進行過濾。可以把所有的可能存在的key放到一個大的Bitmap中,查詢時通過該bitmap過 濾。(布隆表達式)
8.1.2 緩存雪崩
- 什麼叫緩存雪崩
當緩存服務器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給後端系統(比如DB)帶來很大壓 力。
- 如何解決
1:在緩存失效後,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和 寫緩存,其他線程等待。
2:不同的key,設置不同的過期時間,讓緩存失效的時間點儘量均勻。
3:做二級緩存,A1為原始緩存,A2為拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置為短期,A2設置為長 期(此點為補充)
8.1.3 緩存擊穿
- 什麼叫做緩存擊穿
對於一些設置了過期時間的key,如果這些key可能會在某些時間點被超高併發地訪問,是一種非常“熱點”的數據。這 個時候,需要考慮一個問題:緩存被“擊穿”的問題,這個和緩存雪崩的區別在於這裡針對某一key緩存,前者則是很多key。
緩存在某個時間點過期的時候,恰好在這個時間點對這個Key有大量的併發請求過來,這些請求發現緩存過期一般都會 從後端DB加載數據並回設到緩存,這個時候大併發的請求可能會瞬間把後端DB壓垮。
- 如何解決
使用redis的setnx互斥鎖先進行判斷,這樣其他線程就處於等待狀態,保證不會有大併發操作去操作數據庫。
<code>if(redis.sexnx()==1){
//先查詢緩存
//查詢數據庫
//加入緩存
}/<code>
8.2 啟動redis日誌功能
redis默認不記錄log文件, 需要在redis.conf文件修改logfile參數.如下:
- 日誌級別 loglevel notic
- 日誌路勁 logfile "/usr/local/redis/log/redis.log"
8.3 redis的監控狀態
8.3.1 RDB狀態監控
<code>bin/redis-cli info |grep rdb_/<code>
- rdb_changes_since_last_save表明上次RDB保存以後改變的key次數
- rdb_bgsave_in_progress 表示當前是否在進行 bgsave 操作,1 表示正在進行;0 表示沒有進行
- rdb_last_save_time 上次保存 RDB 文件的時間戳
- rdb_last_bgsave_time_sec 上次保存的耗時
- rdb_last_bgsave_status 上次保存的狀態
- rdb_current_bgsave_time_sec 目前保存 RDB 文件已花費的時間
8.3.2 aof的監控狀態
<code>bin/redis-cli info |grep aof_/<code>
- aof_enabledAOF文件是否啟用
- aof_rewrite_in_progress 表示當前是否在進行 AOF 日誌的重寫
- aof_rewrite_scheduled
- aof_last_rewrite_time_sec 上次寫入的時間戳
- aof_current_rewrite_time_sec:-1
- aof_last_bgrewrite_status:ok 上次寫入狀態
- aof_last_write_status:ok 上次寫入狀態
8.3.3 內存監控
<code>bin/redis-cli info |grep mem/<code>
- used_memory:13490096 //數據佔用了多少內存(字節)
- used_memory_human:12.87M //數據佔用了多少內存(帶單位的,可讀性好)
- used_memory_rss:13490096 //redis 佔用了多少內存
- used_memory_peak:15301192 //佔用內存的峰值(字節)
- used_memory_peak_human:14.59M //佔用內存的峰值(帶單位的,可讀性好)
- used_memory_lua:31744 //lua 引擎所佔用的內存大小(字節)
- mem_fragmentation_ratio:1.00 //內存碎片率
- mem_allocator:libc //redis 內存分配器版本,在編譯時指定的。有 libc、jemalloc、tcmalloc 這 3 種
8.4 redis慢查詢
8.4.1 慢查詢日誌
慢查詢日誌幫助開發和運維人員定位系統存在的慢操作。慢查詢日誌就是系統
在命令執行前後計算每條命令的執行時間,當超過預設閥值,就將這條命令的相關 信息(慢查詢 ID,發生時間戳,耗時,命令的詳細信息)記錄下來。
Redis 客戶端一條命令分為如下四部分執行:
需要注意的是,慢查詢日誌只是統計步驟 3)執行命令的時間,所以慢查詢並不代 表客戶端沒有超時問題。需要注意的是,慢查詢日誌只是統計步驟 3)執行命令的時間, 所以慢查詢並不代表客戶端沒有超時問題。
8.4.2 慢查詢參數
- 慢查詢的預設閥值 slowlog-log-slower-than
- slowlog-log-slower-than參數就是預設閥值,1000,如果一條命令的執行時間超過 10000 微妙,那麼它將被記錄 在慢查詢日誌中。
- 如果slowlog-log-slower-than的值是0,則會記錄所有命令。
- 如果slowlog-log-slower-than的值小於0,則任何命令都不會記錄日誌
- 對於高流量的場景,如果執行命令的時間在 1 毫秒以上,那 麼 redis 最多可支撐 OPS(每秒操作次數)不到 1000,因此高 OPS 場景的 REDIS 建議設置為 1 毫秒.
- 慢查詢日誌的長度slowlog-max-len
- slowlog-max-len只是說明了慢查詢日誌最多存儲多少條。Redis使用一個列表來存儲慢查詢日誌,showlog-max-len 就是列表的最大長度。 當慢查詢日誌已經到達列表的最大長度時,又有慢查詢日誌要進入列 表,則最早插入列表的日誌將會被移出列表,新日誌被插入列表的末 尾。
- slowlog-max-len的設置建議, 線上環境建議調大慢查詢日誌的列表,記錄慢查詢日誌時 Redis 會對長命令做截斷操作,並不會佔用大量內存。增大慢查詢列表可 以減緩慢查詢被剔除出列表的可能性。例如線上可以設置為 1000 以 上
8.4.3 慢查詢日誌的組成
慢查詢日誌由以下四個屬性組成:標識 ID,發生時間戳,命令耗時,執行命令和參數
8.5 redis的pipeline(管道)
簡單來說,PipeLine(管道)就是“批處理”操作。 由於網絡開銷延遲,就算 redis server 端有很強的處理能力,也會由於收到的client 消息少,而造成吞吐量小。當 client 使用 pipelining 發送命令時,redis server 必須將部分請求放到隊列中(使用內存),執行完畢後一次性發送結果;如果發送 的命令很多的話,建議對返回的結果加標籤,當然這也會增加使用的內存。
Pipeline 在某些場景下非常有用,比如有多個 command 需要被“及時的”提 交,而且他們對相應結果沒有互相依賴,對結果響應也無需立即獲得,那麼 pipeline 就可以充當這種“批處理”的工具;而且在一定程度上,可以較大的提升性能,性 能提升的原因主要是 TCP 連接中減少了“交互往返”的時間。
應用場景: 特別是有for循環取值時, 建議使用pipeline
代碼示例:
<code># 自定義一個自己需要的類
List<response>> resAll = new ArrayList();
// 獲取一個管道對象
Pipeline pipeline = jedisUtil.getReis().pipelined();
List<string> ids = new ArrayList();
ids.add("str1");
ids.add("str2");
ids.add("str3");
ids.forEach(id->{
Response<string> res = pipeline.get(id);
// 管道的對象結果都存在resAll中
resAll.add(res);
});
//pipeline.sync(); close內部有sync()方法調用
pipeline.close();
resAll.forEach(item -> {
System.out.println(Integer.valueof(res) + 100);
});/<string>/<string>/<response>/<code>
8.6 redis的噩耗: 阻塞
8.6.1 耗時長命令造成阻塞
- keys, sort等命令
當redis的數據量達到一定級別後(比如20G),阻塞操作對性能的影響尤為嚴重;keys命令用於查找所有符合給定模式 pattern 的 key,時間複雜度為O(N), N 為數據庫中 key 的數量。當數據庫中的個數達到千萬時,這個命令會造成讀寫線程阻塞數秒;類似的命令有sunion sort等操作;如果業務需求中一定要使用keys、sort等操作怎麼辦?
解決方案
在架構設計中,有“分流”一招,說的是將處理快的請求和處理慢的請求分離開來,否則,慢的影響到了快的,讓快的也快不起來;這在redis的設計中體現的非常明顯,redis的純內存操作,epoll非阻塞IO事件處理,這些快的放在一個線程中搞定,而持久化,AOF重寫、Master-slave同步數據這些耗時的操作就單開一個進程來處理,不要慢的影響到快的;同樣,既然需要使用keys這些耗時的操作,那麼我們就將它們剝離出去,比如單開一個redis slave結點,專門用於keys、sort等耗時的操作,這些查詢一般不會是線上的實時業務,查詢慢點就慢點,主要是能完成任務,而對於線上的耗時快的任務沒有影響
- smembers命令
smembers命令用於獲取集合全集,時間複雜度為O(N),N為集合中的數量;如果一個集合中保存了千萬量級的數據,一次取回也會造成事件處理線程的長時間阻塞;
解決方案
和sort,keys等命令不一樣,smembers可能是線上實時應用場景中使用頻率非常高的一個命令,這裡分流一招並不適合,我們更多的需要從設計層面來考慮;在設計時,我們可以控制集合的數量,將集合數一般保持在500個以內;比如原來使用一個鍵來存儲一年的記錄,數據量大,我們可以使用12個鍵來分別保存12個月的記錄,或者365個鍵來保存每一天的記錄,將集合的規模控制在可接受的範圍;
如果不容易將集合劃分為多個子集合,而堅持用一個大集合來存儲,那麼在取集合的時候可以考慮使用SRANDMEMBER key [count];隨機返回集合中的指定數量,當然,如果要遍歷集合中的所有元素,這個命令就不適合了;
- save命令
save命令使用事件處理線程進行數據的持久化;當數據量大的時候,會造成線程長時間阻塞(我們的生產上,reids內存中1個G保存需要12s左右),整個redis被block;save阻塞了事件處理的線程,我們甚至無法使用redis-cli查看當前的系統狀態,造成“何時保存結束,目前保存了多少”這樣的信息都無從得知;
解決方案
我沒有想到需要用到save命令的場景,任何時候需要持久化的時候使用bgsave都是合理的選擇(當然,這個命令也會帶來問題,後面聊到);
推介兩篇redis阻塞的文章:
https://blog.csdn.net/linbiaorui/article/details/79822318
http://colin115.iteye.com/blog/2263351
8.7 redis持久化故障診斷
- 使用 Java 客戶端,循環插入 20 個 200M 大小的數據,程序如下:
- 檢查 RDB 的狀態信息
<code>bin/redis-cli info |grep rdb_/<code>
- 檢查日誌文件: redis.log
- 解決問題:修改 vm.overcommit_memory 參數。關於 vm.overcommit_memory 不同的值說明:由於 RDB 文件寫的時候 fork 一個子進程。相當於複製了一個內存鏡像。 當時系統的內存是 4G,而 redis 佔用了近 3G 的內存,因此肯定會報內存無法 分配。如果 「vm.overcommit_memory」設置為 0,在可用內存不足的情況下, 就無法分配新的內存。如果 「vm.overcommit_memory」設置為 1。 那麼 redis將使用交換內存。
- 方法一: 修改內核參數 vi /etc/sysctl。設置 vm.overcommit_memory = 1 然後執行 sysctl -p
- 方法二: 使用交換內存並不是一個完美的方案。最好的辦法是擴大物 理內存。
- 0 表示檢查是否有足夠的內存可用,如果是,允許分配;如果內存不夠,拒絕該請求,並返回一個錯誤給應用程序。
- 1 允許分配超出物理內存加上交換內存的請求
- 2 內核總是返回 true
8.8 redis內存淘汰策略
8.8.1 最大緩存
在 redis 中,允許用戶設置最大使用內存大小maxmemory,默認為0,沒有指定最大緩存,如果有新的數據添 加,超過最大內存,則會使redis崩潰,所以一定要設置.
redis 內存數據集大小上升到一定大小的時候,就會實行數據淘汰策略
8.8.2 淘汰策略
redis淘汰策略配置: maxmemory-policy voltile-lru,支持熱配置
Redis提供6種數據淘汰策略:
- voltile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
- volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
- volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
- allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
- allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
- no-enviction(驅逐):禁止驅逐數據
8.8.3 LRU原理
LRU( ,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如 果數據最近被訪問過,那麼將來被訪問的幾率也更高”。
閱讀更多 故事凌 的文章