線上Redis高并發(fā)性能調(diào)優(yōu)實(shí)踐
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|??丶謙信
來源 |? urlify.cn/YBJryu
66套java從入門到精通實(shí)戰(zhàn)課程分享
項(xiàng)目背景
最近,做一個(gè)按優(yōu)先級(jí)和時(shí)間先后排隊(duì)的需求。用 Redis 的 sorted set 做排隊(duì)隊(duì)列。
主要使用的 Redis 命令有, zadd, zcount, zscore, zrange 等。
測(cè)試完畢后,發(fā)到線上,發(fā)現(xiàn)有大量接口請(qǐng)求返回超時(shí)熔斷(超時(shí)時(shí)間為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)
且有一個(gè)怪異的現(xiàn)象,只有寫庫的邏輯報(bào)錯(cuò),即 zadd 操作。像 zadd, zcount, zscore 這些操作全部能正常執(zhí)行。
還有就是報(bào)錯(cuò)和正常執(zhí)行交錯(cuò)持續(xù)。即假設(shè)每分鐘有1000個(gè) Redis 操作,其中900個(gè)正常,100個(gè)報(bào)錯(cuò)。而不是報(bào)錯(cuò)后,Redis 就不能正常使用了。
問題排查
1.連接池泄露?
從上面的現(xiàn)象基本可以排除連接池泄露的可能,如果連接未被釋放,那么一旦開始報(bào)錯(cuò),后面的 Redis 請(qǐng)求基本上都會(huì)失敗。而不是有90%都可正常執(zhí)行。
但 Jedis 客戶端據(jù)說有高并發(fā)下連接池泄露的問題,所以為了排除一切可能,還是升級(jí)了 Jedis 版本,發(fā)布上線,發(fā)現(xiàn)沒什么用。
2.硬件原因?
排查 Redis 客戶端服務(wù)器性能指標(biāo),CPU利用率10%,內(nèi)存利用率75%,磁盤利用率10%,網(wǎng)絡(luò)I/O上行 1.12M/s,下行 2.07M/s。接口單實(shí)例QPS均值300左右,峰值600左右。
Redis 服務(wù)端連接總數(shù)徘徊在2000+,CPU利用率5.8%,內(nèi)存使用率49%,QPS1500-2500。
硬件指標(biāo)似乎也沒什么問題。
3.Redis參數(shù)配置問題?
JedisPoolConfig?config?=?new?JedisPoolConfig();
config.setMaxTotal?(200);????????//?最大連接數(shù)
config.setMinIdle?(5);???????????//?最小空閑連接數(shù)
config.setMaxIdle?(50);??????????//?最大空閑連接數(shù)
config.setMaxWaitMillis?(1000?*?1);????//?最長等待時(shí)間
config.setTestOnReturn?(false);
config.setTestOnBorrow?(false);
config.setTestWhileIdle?(true);
config.setTimeBetweenEvictionRunsMillis?(30?*?1000);
config.setNumTestsPerEvictionRun?(50);基本上大部分公司的配置包括網(wǎng)上博客提供的配置其實(shí)都和上面差不多,看不出有什么問題。
這里我嘗試把最大連接數(shù)調(diào)整到500,發(fā)布到線上,并沒什么卵用,報(bào)錯(cuò)數(shù)反而變多了。
4.連接數(shù)統(tǒng)計(jì)
在 Redis Master 庫上執(zhí)行命令:client list。打印出當(dāng)前所有連接到服務(wù)器的客戶端IP,并過濾出當(dāng)前服務(wù)的IP地址的連接。
發(fā)現(xiàn)均未達(dá)到最大連接數(shù),確實(shí)排除了連接泄露的可能。

5.最大連接數(shù)調(diào)優(yōu)和壓測(cè)
既然連接遠(yuǎn)未打滿,說明不需要設(shè)置那么大的連接數(shù)。而 Redis 服務(wù)端又是單線程讀寫。客戶端創(chuàng)建過多連接,只會(huì)耗費(fèi)資源,反而拖累性能。

? ? ?使用以上代碼,在本機(jī)使用 JMeter 壓測(cè)300個(gè)線程,連續(xù)請(qǐng)求30秒。
首先把最大連接數(shù)設(shè)為500,成功率:99.61%
請(qǐng)求成功:82004次,TP90耗時(shí)目測(cè)在50-80ms左右。
請(qǐng)求失敗322次,全部為請(qǐng)求服務(wù)器超時(shí):socket read timeout,耗時(shí)2s后,由 Jedis 自行熔斷。
?。ㄟ@種情況造成數(shù)據(jù)不一致,實(shí)際上服務(wù)端已執(zhí)行了命令,只是客戶端讀取返回結(jié)果超時(shí))。

再把最大連接數(shù)設(shè)為20,成功率:98.62%(有一定幾率100%成功)
請(qǐng)求成功:85788次,TP90耗時(shí)在10ms左右。
? ? 請(qǐng)求失敗:1200次,全部為等待客戶端連接超時(shí):Caused by: java.util.NoSuchElementException: Timeout waiting for idle object,熔斷時(shí)間為1秒。


? 再將最大連接數(shù)調(diào)整為50,成功率:100%
? 請(qǐng)求成功:85788次, TP90耗時(shí)10ms。
請(qǐng)求失?。?次。


綜上,Redis 服務(wù)端單線程讀寫,連接數(shù)太多并沒卵用,反而會(huì)消耗更多資源。最大連接數(shù)配置太小,不能滿足并發(fā)需求,線程會(huì)因?yàn)槟貌坏娇臻e連接而超時(shí)退出。
在滿足并發(fā)的前提下,maxTotal連接數(shù)越小越好。在300線程并發(fā)下,最大連接數(shù)設(shè)為50,可以穩(wěn)定運(yùn)行。
基于以上結(jié)論,嘗試調(diào)整 Redis 參數(shù)配置并發(fā)布上線,但以上實(shí)驗(yàn)只執(zhí)行了 zadd 命令,仍未解決一個(gè)問題:為什么只有寫庫報(bào)錯(cuò)?
果然,發(fā)布上線后,接口超時(shí)次數(shù)有所減少,響應(yīng)時(shí)間有所提升,但仍有報(bào)錯(cuò),沒能解決此問題。
6.插曲 - Redis鎖
在優(yōu)化此服務(wù)的同時(shí),把同事使用的另一個(gè) Redis 客戶端一起優(yōu)化了,結(jié)果同事的接口過了一天開始大面積報(bào)錯(cuò),接口響應(yīng)時(shí)間達(dá)到8個(gè)小時(shí)。
排查發(fā)現(xiàn),同事的接口僅使用 Redis 作為分布式鎖。而這個(gè) RedisLock 類是從其他服務(wù)拿過來直接用的,自旋時(shí)間設(shè)置過長,這個(gè)接口又是超高并發(fā)。
最大連接數(shù)設(shè)為50后,鎖資源競(jìng)爭(zhēng)激烈,直接導(dǎo)致大部分線程自旋把連接池耗盡了。于是又緊急把最大連接池恢復(fù)到200,問題得以解決。
由此可見,在分布式鎖的場(chǎng)景下,配置不能完全參考讀寫 Redis 操作的配置。
7.排查服務(wù)端持久化
在把客戶端研究了好幾遍之后,發(fā)現(xiàn)并沒有什么可以優(yōu)化的了,于是開始懷疑是服務(wù)端的問題。
持久化是一直沒研究過的問題。在查閱了網(wǎng)上的一些博客,發(fā)現(xiàn)持久化確實(shí)有可能阻塞讀寫IO的。
?
“1) 對(duì)于沒有持久化的方式,讀寫都在數(shù)據(jù)量達(dá)到800萬的時(shí)候,性能下降幾倍,此時(shí)正好是達(dá)到內(nèi)存10G,Redis開始換出到磁盤的時(shí)候。并且從那以后再也沒辦法重新振作起來,性能比Mongodb還要差很多。
2) 對(duì)于AOF持久化的方式,總體性能并不會(huì)比不帶持久化方式差太多,都是在到了千萬數(shù)據(jù)量,內(nèi)存占滿之后讀的性能只有幾百。
3) 對(duì)于Dump持久化方式,讀寫性能波動(dòng)都比較大,可能在那段時(shí)候正在Dump也有關(guān)系,并且在達(dá)到了1400萬數(shù)據(jù)量之后,讀寫性能貼底了。在Dump的時(shí)候,不會(huì)進(jìn)行換出,而且所有修改的數(shù)據(jù)還是創(chuàng)建的新頁,內(nèi)存占用比平時(shí)高不少,超過了15GB。而且Dump還會(huì)壓縮,占用了大量的CPU。也就是說,在那個(gè)時(shí)候內(nèi)存、磁盤和CPU的壓力都接近極限,性能不差才怪?!??---- 引用自lovecindywang 的博客園博客
“內(nèi)存越大,觸發(fā)持久化的操作阻塞主線程的時(shí)間越長
Redis是單線程的內(nèi)存數(shù)據(jù)庫,在redis需要執(zhí)行耗時(shí)的操作時(shí),會(huì)fork一個(gè)新進(jìn)程來做,比如bgsave,bgrewriteaof。Fork新進(jìn)程時(shí),雖然可共享的數(shù)據(jù)內(nèi)容不需要復(fù)制,但會(huì)復(fù)制之前進(jìn)程空間的內(nèi)存頁表,這個(gè)復(fù)制是主線程來做的,會(huì)阻塞所有的讀寫操作,并且隨著內(nèi)存使用量越大耗時(shí)越長。例如:內(nèi)存20G的redis,bgsave復(fù)制內(nèi)存頁表耗時(shí)約為750ms,redis主線程也會(huì)因?yàn)樗枞?50ms?!??? ? ?---- 引用自CSDN博客
而我們的Redis實(shí)例總內(nèi)存20G,內(nèi)存使用了50%,keys數(shù)量達(dá)4000w。
主從集群,從庫不做持久化,主庫使用RDB持久化。rdb的save參數(shù)是默認(rèn)值。(這也恰好能解釋通為什么寫庫報(bào)錯(cuò),讀庫正常)
且此 Redis 已使用了幾年,里面可能存在大量的key已經(jīng)不使用了,但未設(shè)置過期時(shí)間。
然而,像 Redis、MySQL 這種都是由數(shù)據(jù)中臺(tái)負(fù)責(zé),我們并無權(quán)查看服務(wù)端日志,這個(gè)事情也不好推動(dòng),中臺(tái)會(huì)說客戶端使用的有問題,建議調(diào)整參數(shù)。
所以最佳解決方案可能是,重新申請(qǐng) Redis 實(shí)例,逐步把項(xiàng)目中使用的 Redis 遷移到新實(shí)例,并注意設(shè)置過期時(shí)間。遷移完成后,把老的 Redis 實(shí)例廢棄回收。
小結(jié)
1)如果簡(jiǎn)單的在網(wǎng)上搜索,Could not get a resource from the pool ,?基本都是些連接未釋放的問題。
然而很多原因可能導(dǎo)致 Jedis 報(bào)這個(gè)錯(cuò),這條信息并不是異常堆棧的最頂層。
? ? ? ?2)Redis其實(shí)只適合作為緩存,而不是數(shù)據(jù)庫或是存儲(chǔ)。它的持久化方式適用于救救急啥的,不太適合當(dāng)作一個(gè)普通功能來用。
3)還是建議任何數(shù)據(jù)都設(shè)置過期時(shí)間,哪怕設(shè)1年呢。不然老的項(xiàng)目可能已經(jīng)都廢棄了,殘留在 Redis 里的 key,其他人也不敢刪。
? ? ? ?4)不要存放垃圾數(shù)據(jù)到 Redis 中,及時(shí)清理無用數(shù)據(jù)。業(yè)務(wù)下線了,就把相關(guān)數(shù)據(jù)清理掉。
粉絲福利:108本java從入門到大神精選電子書領(lǐng)取
???
?長按上方鋒哥微信二維碼?2 秒 備注「1234」即可獲取資料以及 可以進(jìn)入java1234官方微信群
感謝點(diǎn)贊支持下哈?
