最近,作一個按優先級和時間前後排隊的需求。用 Redis 的 sorted set 作排隊隊列。java
主要使用的 Redis 命令有, zadd, zcount, zscore, zrange 等。redis
測試完畢後,發到線上,發現有大量接口請求返回超時熔斷(超時時間爲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 這些操做所有能正常執行。socket
還有就是報錯和正常執行交錯持續。即假設每分鐘有1000個 Redis 操做,其中900個正常,100個報錯。而不是報錯後,Redis 就不能正常使用了。分佈式
從上面的現象基本能夠排除鏈接池泄露的可能,若是鏈接未被釋放,那麼一旦開始報錯,後面的 Redis 請求基本上都會失敗。而不是有90%均可正常執行。高併發
但 Jedis 客戶端聽說有高併發下鏈接池泄露的問題,因此爲了排除一切可能,仍是升級了 Jedis 版本,發佈上線,發現沒什麼用。
排查 Redis 客戶端服務器性能指標,CPU利用率10%,內存利用率75%,磁盤利用率10%,網絡I/O上行 1.12M/s,下行 2.07M/s。接口單實例QPS均值300左右,峯值600左右。
Redis 服務端鏈接總數徘徊在2000+,CPU利用率5.8%,內存使用率49%,QPS1500-2500。
硬件指標彷佛也沒什麼問題。
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,發佈到線上,並沒什麼卵用,報錯數反而變多了。
在 Redis Master 庫上執行命令:client list。打印出當前全部鏈接到服務器的客戶端IP,並過濾出當前服務的IP地址的鏈接。
發現均未達到最大鏈接數,確實排除了鏈接泄露的可能。
既然鏈接遠未打滿,說明不須要設置那麼大的鏈接數。而 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 命令,仍未解決一個問題:爲何只有寫庫報錯?
果真,發佈上線後,接口超時次數有所減小,響應時間有所提高,但仍有報錯,沒能解決此問題。
在優化此服務的同時,把同事使用的另外一個 Redis 客戶端一塊兒優化了,結果同事的接口過了一天開始大面積報錯,接口響應時間達到8個小時。
排查發現,同事的接口僅使用 Redis 做爲分佈式鎖。而這個 RedisLock 類是從其餘服務拿過來直接用的,自旋時間設置過長,這個接口又是超高併發。
最大鏈接數設爲50後,鎖資源競爭激烈,直接致使大部分線程自旋把鏈接池耗盡了。因而又緊急把最大鏈接池恢復到200,問題得以解決。
因而可知,在分佈式鎖的場景下,配置不能徹底參考讀寫 Redis 操做的配置。
在把客戶端研究了好幾遍以後,發現並無什麼能夠優化的了,因而開始懷疑是服務端的問題。
持久化是一直沒研究過的問題。在查閱了網上的一些博客,發現持久化確實有可能阻塞讀寫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 中,及時清理無用數據。業務下線了,就把相關數據清理掉。