專案背景
最近,做一個按優先順序和時間先後排隊的需求。用 Redis 的 sorted set 做排隊佇列。
主要使用的 Redis 命令有, zadd, zcount, zscore, zrange 等。
測試完畢後,發到線上,發現有大量介面請求返回超時熔斷(超時時間為3s)。
Error日誌列印的異常堆疊為:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection timed out (Connection timed out)
Caused by: java.net.ConnectException: Connection timed out (Connection timed out)
且有一個怪異的現象,只有寫庫的邏輯報錯,即 zadd 操作。像 zadd, zcount, zscore 這些操作全部能正常執行。
還有就是報錯和正常執行交錯持續。即假設每分鐘有1000個 Redis 操作,其中900個正常,100個報錯。而不是報錯後,Redis 就不能正常使用了。
問題排查
1.連線池洩露?
從上面的現象基本可以排除連線池洩露的可能,如果連線未被釋放,那麼一旦開始報錯,後面的 Redis 請求基本上都會失敗。而不是有90%都可正常執行。
但 Jedis 客戶端據說有高併發下連線池洩露的問題,所以為了排除一切可能,還是升級了 Jedis 版本,釋出上線,發現沒什麼用。
2.硬體原因?
排查 Redis 客戶端伺服器效能指標,CPU利用率10%,記憶體利用率75%,磁碟利用率10%,網路I/O上行 1.12M/s,下行 2.07M/s。介面單例項QPS均值300左右,峰值600左右。
Redis 服務端連線總數徘徊在2000+,CPU利用率5.8%,記憶體使用率49%,QPS1500-2500。
硬體指標似乎也沒什麼問題。
3.Redis引數配置問題?
1 JedisPoolConfig config = new JedisPoolConfig(); 2 config.setMaxTotal (200); // 最大連線數 3 config.setMinIdle (5); // 最小空閒連線數 4 config.setMaxIdle (50); // 最大空閒連線數 5 config.setMaxWaitMillis (1000 * 1); // 最長等待時間 6 config.setTestOnReturn (false); 7 config.setTestOnBorrow (false); 8 config.setTestWhileIdle (true); 9 config.setTimeBetweenEvictionRunsMillis (30 * 1000); 10 config.setNumTestsPerEvictionRun (50);
基本上大部分公司的配置包括網上部落格提供的配置其實都和上面差不多,看不出有什麼問題。
這裡我嘗試把最大連線數調整到500,釋出到線上,並沒什麼卵用,報錯數反而變多了。
4.連線數統計
在 Redis Master 庫上執行命令:client list。列印出當前所有連線到伺服器的客戶端IP,並過濾出當前服務的IP地址的連線。
發現均未達到最大連線數,確實排除了連線洩露的可能。
5.最大連線數調優和壓測
既然連線遠未打滿,說明不需要設定那麼大的連線數。而 Redis 服務端又是單執行緒讀寫。客戶端建立過多連線,只會耗費資源,反而拖累效能。
使用以上程式碼,在本機使用 JMeter 壓測300個執行緒,連續請求30秒。
首先把最大連線數設為500,成功率:99.61%
請求成功:82004次,TP90耗時目測在50-80ms左右。
請求失敗322次,全部為請求伺服器超時:socket read timeout,耗時2s後,由 Jedis 自行熔斷。
(這種情況造成資料不一致,實際上服務端已執行了命令,只是客戶端讀取返回結果超時)。
再把最大連線數設為20,成功率:98.62%(有一定機率100%成功)
請求成功:85788次,TP90耗時在10ms左右。
請求失敗:1200次,全部為等待客戶端連線超時:Caused by: java.util.NoSuchElementException: Timeout waiting for idle object,熔斷時間為1秒。
再將最大連線數調整為50,成功率:100%
請求成功:85788次, TP90耗時10ms。
請求失敗:0次。
綜上,Redis 服務端單執行緒讀寫,連線數太多並沒鳥用,反而會消耗更多資源。最大連線數配置太小,不能滿足併發需求,執行緒會因為拿不到空閒連線而超時退出。
在滿足併發的前提下,maxTotal連線數越小越好。在300執行緒併發下,最大連線數設為50,可以穩定執行。
基於以上結論,嘗試調整 Redis 引數配置併發布上線,但以上實驗只執行了 zadd 命令,仍未解決一個問題:為什麼只有寫庫報錯?
果然,釋出上線後,介面超時次數有所減少,響應時間有所提升,但仍有報錯,沒能解決此問題。
6.插曲 - Redis鎖
在優化此服務的同時,把同事使用的另一個 Redis 客戶端一起優化了,結果同事的介面過了一天開始大面積報錯,介面響應時間達到8個小時。
排查發現,同事的介面僅使用 Redis 作為分散式鎖。而這個 RedisLock 類是從其他服務拿過來直接用的,自旋時間設定過長,這個介面又是超高併發。
最大連線數設為50後,鎖資源競爭激烈,直接導致大部分執行緒自旋把連線池耗盡了。於是又緊急把最大連線池恢復到200,問題得以解決。
由此可見,在分散式鎖的場景下,配置不能完全參考讀寫 Redis 操作的配置。
7.排查服務端持久化
在把客戶端研究了好幾遍之後,發現並沒有什麼可以優化的了,於是開始懷疑是服務端的問題。
持久化是一直沒研究過的問題。在查閱了網上的一些部落格,發現持久化確實有可能阻塞讀寫IO的。
“1) 對於沒有持久化的方式,讀寫都在資料量達到800萬的時候,效能下降幾倍,此時正好是達到記憶體10G,Redis開始換出到磁碟的時候。並且從那以後再也沒辦法重新振作起來,效能比Mongodb還要差很多。
2) 對於AOF持久化的方式,總體效能並不會比不帶持久化方式差太多,都是在到了千萬資料量,記憶體佔滿之後讀的效能只有幾百。
3) 對於Dump持久化方式,讀寫效能波動都比較大,可能在那段時候正在Dump也有關係,並且在達到了1400萬資料量之後,讀寫效能貼底了。在Dump的時候,不會進行換出,而且所有修改的資料還是建立的新頁,記憶體佔用比平時高不少,超過了15GB。而且Dump還會壓縮,佔用了大量的CPU。也就是說,在那個時候記憶體、磁碟和CPU的壓力都接近極限,效能不差才怪。” ---- 引用自lovecindywang 的部落格園部落格
“記憶體越大,觸發持久化的操作阻塞主執行緒的時間越長
Redis是單執行緒的記憶體資料庫,在redis需要執行耗時的操作時,會fork一個新程式來做,比如bgsave,bgrewriteaof。 Fork新程式時,雖然可共享的資料內容不需要複製,但會複製之前程式空間的記憶體頁表,這個複製是主執行緒來做的,會阻塞所有的讀寫操作,並且隨著記憶體使用量越大耗時越長。例如:記憶體20G的redis,bgsave複製記憶體頁表耗時約為750ms,redis主執行緒也會因為它阻塞750ms。” ---- 引用自CSDN部落格
而我們的Redis例項總記憶體20G,記憶體使用了50%,keys數量達4000w。
主從叢集,從庫不做持久化,主庫使用RDB持久化。rdb的save引數是預設值。(這也恰好能解釋通為什麼寫庫報錯,讀庫正常)
且此 Redis 已使用了幾年,裡面可能存在大量的key已經不使用了,但未設定過期時間。
然而,像 Redis、MySQL 這種都是由資料中臺負責,我們並無權檢視服務端日誌,這個事情也不好推動,中臺會說客戶端使用的有問題,建議調整引數。
所以最佳解決方案可能是,重新申請 Redis 例項,逐步把專案中使用的 Redis 遷移到新例項,並注意設定過期時間。遷移完成後,把老的 Redis 例項廢棄回收。
小結
1)如果簡單的在網上搜尋,Could not get a resource from the pool , 基本都是些連線未釋放的問題。
然而很多原因可能導致 Jedis 報這個錯,這條資訊並不是異常堆疊的最頂層。
2)Redis其實只適合作為快取,而不是資料庫或是儲存。它的持久化方式適用於救救急啥的,不太適合當作一個普通功能來用。
3)還是建議任何資料都設定過期時間,哪怕設1年呢。不然老的專案可能已經都廢棄了,殘留在 Redis 裡的 key,其他人也不敢刪。
4)不要存放垃圾資料到 Redis 中,及時清理無用資料。業務下線了,就把相關資料清理掉。