本文導(dǎo)讀:
[1] 疫情當(dāng)前
[2] 應(yīng)用異常監(jiān)控
[3] Redis客戶端異常分析
[4] Redis客戶端問題引導(dǎo)分析
[5] 站在Redis客戶端視角分析
[6] 站在Redis服務(wù)端視角分析
[7] 資源池生產(chǎn)配置合理性分析
[8] 本文總結(jié)
[1] 疫情當(dāng)前
為響應(yīng)國家抗擊疫情的號召,全國有過億的企業(yè)職員選擇了遠(yuǎn)程辦公脓魏,IT科技大廠們也紛紛開啟了VPN模式兰吟,保障企業(yè)運(yùn)營。
既然這樣茂翔,我們該怎么做呢混蔼?苦逼的程序猿們只能加油干!來張圖看看老板們擔(dān)心的是什么珊燎?
不好意思,大BOSS們首先擔(dān)心的可不是員工身心健康悔政,而是工作效率的提升哦~
但是晚吞,據(jù)業(yè)內(nèi)人士預(yù)估,新冠肺炎疫情很有可能激發(fā)國內(nèi)企業(yè)信息化建設(shè)提速谋国。
對于企業(yè)而言槽地,值得期待的是,『遠(yuǎn)程辦公』讓企業(yè)看到辦公形式的更多可能性,有助于企業(yè)未來辦公形式的新嘗試捌蚊。
更為重要的是集畅,企業(yè)可以此為契機(jī),提升企業(yè)自身信息化建設(shè)缅糟,增強(qiáng)團(tuán)隊(duì)凝聚力與協(xié)同性挺智,在『危機(jī)』中平穩(wěn)運(yùn)行,甚至是發(fā)現(xiàn)機(jī)會溺拱。
筆者也不例外逃贝,本周已依照公司要求,開啟了遠(yuǎn)程辦公模式迫摔,本周的感受來說,工作效率上肯定會收到一些影響泥从。但因我們一季度目標(biāo)明確句占,所以每天可以按部就班按計(jì)劃如期進(jìn)行。
而且躯嫉,也因?yàn)橐咔榈挠绊懮春妫叽蠊冒舜笠潭急槐镌诩依锪耍竟灸扯说姆?wù) DAU 最近一段時間逆襲不斷上漲祈餐,付費(fèi)會員收入也隨之增長不少擂啥,對于我們來說算是個好消息。
在家遠(yuǎn)程辦公帆阳,不給國家添亂就好哇??哺壶!
接下來,我們繼續(xù)聊聊線上環(huán)境遇到的一個問題以及分析過程蜒谤。
[2] 應(yīng)用異常監(jiān)控
這不山宾,項(xiàng)目中有一個Redis客戶端的異常在疫情期間,出現(xiàn)在了你的面前鳍徽,雖然該異常是偶發(fā)资锰,有必要仔細(xì)分析下該異常出現(xiàn)的原由。
具體異常信息如下所示:
大家看截圖展示的異常信息阶祭,是不是很想問绷杜,這個異常顯示怎么這么「友好」?
沒錯濒募,是通過一款非常好用的實(shí)時異常監(jiān)控工具:Sentry來監(jiān)控到的鞭盟,這款工具在我們的項(xiàng)目中已經(jīng)接入并使用了很長一段時間了,對異常的監(jiān)控非常到位萨咳。
比如針對發(fā)生的異常懊缺,將具體訪問的整個URL、客戶端上報(bào)的信息、設(shè)備型號等信息作為TAGS收集上來鹃两,盡情的展示給你遗座,讓你盡快結(jié)合這些信息快速定位問題。
該服務(wù)部署在k8s容器環(huán)境下俊扳,在截圖中TAGS中途蒋,也能夠看到 server_name 代表的是Pod的hostname,這樣便能快速知道是哪個Pod出現(xiàn)的問題馋记,進(jìn)入容器平臺直接進(jìn)入到Pod內(nèi)部進(jìn)一步詳細(xì)分析号坡。
強(qiáng)烈推薦大家項(xiàng)目中接入Sentry
,因?yàn)樗坏泻芎糜玫漠惓V卫砥脚_梯醒,更為重要的是Sentry支持跨語言客戶端宽堆,比如支持Java、Andriod茸习、C++畜隶、Python、Go等大部分語言号胚,現(xiàn)成的客戶端易于接入和使用籽慢。
我想只要你的服務(wù)不卡死,如果出現(xiàn)問題猫胁,項(xiàng)目里輸出的日志中總會有一些 ERROR 級別的日志出現(xiàn)的箱亿,那么此時就交給Sentry,它會及時向你發(fā)出告警(郵件...)通知你弃秆。
[3] Redis客戶端異常分析
本項(xiàng)目中使用的Jedis(Redis的Java客戶端)届惋,提示異常信息 JedisConnectionException Unexpected end of stream
,在使用Redis過程中我還很少遇到這個問題驾茴,既然遇到了盼樟,這是不是緣分啊 :)
其實(shí)異常棧中已經(jīng)給出了詳細(xì)的調(diào)用過程,在哪里出現(xiàn)的問題锈至,順藤摸瓜根據(jù)這個堆棧去查找線索晨缴。
如何找到更為詳細(xì)的堆棧?別擔(dān)心峡捡,在上圖中點(diǎn)擊下 raw
會出現(xiàn)完整的異常堆棧的文本信息击碗,也方便復(fù)制拷貝出來分析。
如下所示:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:151)
at redis.clients.jedis.Protocol.read(Protocol.java:215)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
at redis.clients.jedis.BinaryJedis.auth(BinaryJedis.java:2139)
at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:108)
at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:888)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:432)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361)
...
根據(jù)以上信息们拙,發(fā)現(xiàn)是調(diào)用到 BinaryJedis.auth
驗(yàn)證Redis密碼時出錯的稍途,而且有 GenericObjectPool.borrowObject
表示借用對象的方法,GenericObjectPool是Apache開源項(xiàng)目的線程池砚婆,在很多開源項(xiàng)目中都能看到它的身影械拍。
說明是在伸手向資源池索要對象時突勇,在資源池里沒有拿到對象,那就只能創(chuàng)建一個坷虑,調(diào)用了 GenericObjectPool.create
甲馋,調(diào)用具體實(shí)現(xiàn)方法 JedisFactory.makeObject
創(chuàng)建Jedis對象時出錯的。
哦迄损?這么一看定躏,簡單一想猜測下,在創(chuàng)建新的對象時驗(yàn)證密碼時芹敌,可能因網(wǎng)絡(luò)不穩(wěn)定鸵熟,Redis-Server沒有正常返回異常信息導(dǎo)致的吹泡。
[4] Redis客戶端問題引導(dǎo)分析
在上文中陆盘,我們在異常堆棧中發(fā)現(xiàn)使用了線程池株汉,如果不使用資源池管理這些對象,會發(fā)生什么情況液茎?
如下所示矾削,每次使用Redis連接都會在客戶端重新創(chuàng)建Jedis對象,創(chuàng)建Jedis對象后豁护,連接Redis Server,這個過程會建立TCP連接(三次握手)欲间,完成操作后楚里,斷開TCP連接(四次揮手),當(dāng)遇到并發(fā)量稍大的請求猎贴,就會吃不消了班缎,消耗資源的同時,無法滿足應(yīng)用性能上的要求她渴。
如果使用了線程池达址,如下圖所示的樣子:
按需在資源池中初始化一定數(shù)量的對象,當(dāng)有客戶端請求到達(dá)時趁耗,從資源池里獲取對象沉唠,對象使用完成,再將對象丟回到資源池里苛败,給其他客戶端使用满葛。
這就是所謂的 「池化技術(shù)」,相信在你的項(xiàng)目中一定會用到的罢屈,比如數(shù)據(jù)庫連接池嘀韧、應(yīng)用服務(wù)器的線程池等等。
池化技術(shù)的優(yōu)勢就是能夠復(fù)用池中的對象缠捌,比如上述圖示中锄贷,避免了分配內(nèi)存和創(chuàng)建堆中對象的開銷;避免了因?qū)ο笾貜?fù)創(chuàng)建,進(jìn)而能避免了TCP連接的建立和斷開的資源開銷谊却;避免了釋放內(nèi)存和銷毀堆中對象的開銷柔昼,進(jìn)而減少垃圾收集器的負(fù)擔(dān);避免內(nèi)存抖動因惭,不必重復(fù)初始化對象狀態(tài)岳锁。
當(dāng)然,我們也可以自己來實(shí)現(xiàn)蹦魔,但是如果想寫出比較完善的對象池的資源管理功能激率,也需要花費(fèi)不少的精力,考慮的細(xì)節(jié)也是非常多的勿决。
站在巨人的肩膀上乒躺,在前文中提到的Jedis內(nèi)部是由 Apache Common Pool2 開源工具包來實(shí)現(xiàn)的,很多開源項(xiàng)目中應(yīng)用也是很廣泛的低缩。
而Jedis客戶端的很多參數(shù)都是來源于Apache Common Pool2的底層實(shí)現(xiàn)過程所需要的參數(shù)嘉冒。
這也是Jedis或者說一些Redis客戶端給用戶使用簡單的原因,但是簡單的同時咆繁,我們也要根據(jù)不同場景去合理配置好連接池的參數(shù)讳推,不合理的配置加上不合理的功能使用,可能會引起很多的問題玩般。
在回歸到前文的最開始的異常银觅,這些異常跟什么有關(guān)系呢?
從圖示中坏为,我們能知道客戶端使用了線程池究驴,可能跟線程池有關(guān)系;創(chuàng)建對象時匀伏,auth 驗(yàn)證密碼時出現(xiàn)了問題洒忧,而驗(yàn)證密碼前已經(jīng)發(fā)起了 connect 連接了,說明連接到了Redis Server够颠,所以 Redis Server 也脫離不了干系的熙侍。
跟 Redis Client 有關(guān)系,我們就要進(jìn)一步分析客戶端的參數(shù)摧找,連接池的參數(shù)是否合理核行。
跟 Redis Server 有關(guān)系,就要結(jié)合問題分析下服務(wù)端的參數(shù)蹬耘,相關(guān)配置參數(shù)是否合理芝雪。
[5] 站在Redis客戶端視角分析
既然講到了Redis客戶端,首先想到的是從客戶端配置的參數(shù)入手综苔。
直接從參數(shù)入手惩系,不如我們可以先接著對異常棧的分析位岔,從對象資源池入手去分析,看看這個對象池到底是怎樣管理的堡牡?
1抒抬、資源池對象管理
資源池中創(chuàng)建對象的過程如上圖所示。
Apache Common Pool2 既然是一個通用的資源池管理框架晤柄,內(nèi)部會定義好資源池的接口和規(guī)范擦剑,具體創(chuàng)建對象實(shí)現(xiàn)交由具體框架來實(shí)現(xiàn)。
1)從資源池獲取對象芥颈,會調(diào)用ObjectPool#borrowObject惠勒,如果沒有空閑對象,則調(diào)用PooledObjectFactory#makeObject創(chuàng)建對象爬坑,JedisFactory是具體的實(shí)現(xiàn)類纠屋。
2)創(chuàng)建完對象放到資源池中,返回給客戶端使用盾计。
3)使用完對象會調(diào)用ObjectPool#returnObject售担,其內(nèi)部會校驗(yàn)一些條件是否滿足,驗(yàn)證通過署辉,對象歸還給資源池族铆。
4)條件驗(yàn)證不通過,比如資源池已關(guān)閉哭尝、對象狀態(tài)不正確(Jedis連接失效)骑素、已超出最大空閑資源數(shù),則會調(diào)用 PooledObjectFactory#destoryObject從資源池中銷毀對象刚夺。
ObjectPool 和 KeyedObjectPool 是兩個基礎(chǔ)接口。
從定義的接口名上也能做下區(qū)分末捣,ObjectPool 接口資源池列表里存儲都是對象侠姑,默認(rèn)實(shí)現(xiàn)類GenericObjectPool,KeyedObjectPool 接口用鍵值對的方式維護(hù)對象箩做,默認(rèn)實(shí)現(xiàn)類是GenericKeyedObjectPool莽红。在實(shí)現(xiàn)過程會有很多公共的功能實(shí)現(xiàn),放在了BaseGenericObjectPool基礎(chǔ)實(shí)現(xiàn)類當(dāng)中邦邦。
SoftReferenceObjectPool 是一個比較特殊的實(shí)現(xiàn)安吁,在這個對象池實(shí)現(xiàn)中,每個對象都會被包裝到一個 SoftReference 中燃辖。SoftReference 軟引用鬼店,能夠在JVM GC過程中當(dāng)內(nèi)存不足時,允許垃圾回收機(jī)制在需要釋放內(nèi)存時回收對象池中的對象黔龟,避免內(nèi)存泄露的問題
PooledObject 是池化對象的接口定義妇智,池化的對象都會封裝在這里滥玷。DefaultPooledObject 是PooledObject 接口缺省實(shí)現(xiàn)類,PooledSoftReference 使用 SoftReference 封裝了對象巍棱,供SoftReferenceObjectPool 使用惑畴。
2、對象池參數(shù)詳解
查看對象池的參數(shù)配置航徙,一種方式是直接查找代碼或者官網(wǎng)文檔中的說明去查看如贷,另外介紹一種更為直觀的方式,因?yàn)?Common Pool2 工具資源池的管理都接入到 JMX 中到踏,所以可以通過如 Jconsole 等工具去查看暴露的屬性和操作杠袱。
第一種方式:
查找對應(yīng)配置類:
在 GenericObjectPoolConfig 和 BaseObjectPoolConfig 配置類對外提供的 setter 方法便是配置參數(shù),并且代碼里都有詳細(xì)的注釋說明夭禽。
第二種方式:
前提是你的應(yīng)用暴露了 JMX 的端口和IP霞掺,允許外部連接。
JVM 參數(shù)如下所示:
-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=IP地址
-Dcom.sun.management.jmxremote.port=端口
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
以上使用的是 Jconsole 工具類讹躯,點(diǎn)擊 MBean
在左側(cè)找到 org.apache.commons.pool2#GenericObjectPool#pool2
點(diǎn)擊屬性可以看到該類的所有屬性信息菩彬,其中除包括核心的配置屬性之外,還包括一些資源池的統(tǒng)計(jì)屬性潮梯。
核心配置屬性:
這些都是重點(diǎn)關(guān)注
的屬性骗灶,也是對外提供的可配置參數(shù)。
1)minIdle
資源池確保最少空閑的連接數(shù)秉馏,默認(rèn)值: 0
2)maxIdle
資源池允許最大空閑的連接數(shù)耙旦,默認(rèn)值: 8
3)maxTotal
資源池中最大連接數(shù),默認(rèn)值:8
4)maxWaitMillis
當(dāng)資源池連接用盡后萝究,調(diào)用者的最大等待時間免都,單位是毫秒,默認(rèn)值:-1帆竹,建議設(shè)置合理的值
5)testOnBorrow
向資源池借用連接時绕娘,是否做連接有效性檢測,無效連接會被移除栽连,默認(rèn)值:false 险领,業(yè)務(wù)量很大時建議為false,因?yàn)闀嘁淮蝡ing的開銷
6)testOnCreate
創(chuàng)建新的資源連接后秒紧,是否做連接有效性檢測绢陌,無效連接會被移除,默認(rèn)值:false 熔恢,業(yè)務(wù)量很大時建議為false脐湾,因?yàn)闀嘁淮蝡ing的開銷
7)testOnReturn
向資源池歸還連接時,是否做連接有效性檢測叙淌,無效連接會被移除沥割,默認(rèn)值:false耗啦,業(yè)務(wù)量很大時建議為false,因?yàn)闀嘁淮蝡ing的開銷
8) testWhileIdle
是否開啟空閑資源監(jiān)測机杜,默認(rèn)值:false
9)blockWhenExhausted
當(dāng)資源池用盡后帜讲,調(diào)用者是否要等待。默認(rèn)值:true椒拗,當(dāng)為true時似将,maxWaitMillis參數(shù)才會生效,建議使用默認(rèn)值
10)lifo
資源池里放池對象的方式蚀苛,LIFO
Last In First Out 后進(jìn)先出在验,true(默認(rèn)值),表示放在空閑隊(duì)列最前面堵未,false:放在空閑隊(duì)列最后面
空閑資源監(jiān)測配置屬性
當(dāng)需要對空閑資源進(jìn)行監(jiān)測時腋舌, testWhileIdle
參數(shù)開啟后與下列幾個參數(shù)組合完成監(jiān)測任務(wù)。
1)timeBetweenEvictionRunsMillis
空閑資源的檢測周期渗蟹,單位為毫秒块饺,默認(rèn)值:-1,表示不檢測雌芽,建議設(shè)置一個合理的值授艰,周期性運(yùn)行監(jiān)測任務(wù)
2)minEvictableIdleTimeMillis
資源池中資源最小空閑時間,單位為毫秒世落,默認(rèn)值:30分鐘(1000 * 60L * 30L)淮腾,當(dāng)達(dá)到該值后空閑資源將被移除,建議根據(jù)業(yè)務(wù)自身設(shè)定
3)numTestsPerEvictionRun
做空閑資源檢測時屉佳,每次的采樣數(shù)谷朝,默認(rèn)值:3,可根據(jù)自身應(yīng)用連接數(shù)進(jìn)行微調(diào)武花,如果設(shè)置為 -1徘禁,表示對所有連接做空閑監(jiān)測
3、空閑資源監(jiān)測源碼剖析
在資源池初始化之后髓堪,有個空閑資源監(jiān)測任務(wù)流程如下:
對應(yīng)源代碼:
創(chuàng)建資源池對象時,在構(gòu)造函數(shù)中初始化配合和任務(wù)的娘荡。
this.internalPool = new GenericObjectPool<T>(factory, poolConfig);
public GenericObjectPool(final PooledObjectFactory<T> factory,
final GenericObjectPoolConfig config) {
super(config, ONAME_BASE, config.getJmxNamePrefix());
if (factory == null) {
jmxUnregister(); // tidy up
throw new IllegalArgumentException("factory may not be null");
}
this.factory = factory;
// 創(chuàng)建空閑資源鏈表
idleObjects = new LinkedBlockingDeque<PooledObject<T>>(config.getFairness());
// 初始化配置
setConfig(config);
// 開啟資源監(jiān)測任務(wù)
startEvictor(getTimeBetweenEvictionRunsMillis());
}
final void startEvictor(final long delay) {
synchronized (evictionLock) {
// 當(dāng)資源池關(guān)閉時會觸發(fā)干旁,取消evictor任務(wù)
if (null != evictor) {
EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
evictor = null;
evictionIterator = null;
}
if (delay > 0) {
// 啟動evictor任務(wù)
evictor = new Evictor();
// 開啟定時任務(wù)
EvictionTimer.schedule(evictor, delay, delay);
}
}
}
Eviector 是個TimerTask,通過啟用的調(diào)度器炮沐,每間隔 timeBetweenEvictionRunsMillis
運(yùn)行一次争群。
class Evictor extends TimerTask {
@Override
public void run() {
final ClassLoader savedClassLoader =
Thread.currentThread().getContextClassLoader();
try {
...
// Evict from the pool
evict();
// Ensure min idle num
ensureMinIdle();
} finally {
// Restore the previous CCL
Thread.currentThread().setContextClassLoader(savedClassLoader);
}
}
}
evict() 移除方法源碼:
@Override
public void evict() throws Exception {
assertOpen();
if (idleObjects.size() > 0) {
PooledObject<T> underTest = null;
// 獲取清除策略
final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();
synchronized (evictionLock) {
final EvictionConfig evictionConfig = new EvictionConfig(
getMinEvictableIdleTimeMillis(),
getSoftMinEvictableIdleTimeMillis(),
getMinIdle());
final boolean testWhileIdle = getTestWhileIdle();
for (int i = 0, m = getNumTests(); i < m; i++) {
// ... 省略部分代碼
// underTest 代表每一個資源
boolean evict;
evict = evictionPolicy.evict(evictionConfig, underTest,
idleObjects.size());
// evict為true,銷毀對象
if (evict) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
// testWhileIdle為true校驗(yàn)資源有效性
if (testWhileIdle) {
boolean active = false;
try {
factory.activateObject(underTest);
active = true;
} catch (final Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
if (active) {
if (!factory.validateObject(underTest)) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
try {
factory.passivateObject(underTest);
} catch (final Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
}
}
}
//...
}
}
}
}
// ...
}
代碼里的默認(rèn)策略 evictionPolicy大年,由 org.apache.commons.pool2.impl.DefaultEvictionPolicy
提供默認(rèn)實(shí)現(xiàn)换薄。
// DefaultEvictionPolicy#evict()
@Override
public boolean evict(final EvictionConfig config, final PooledObject<T> underTest,
final int idleCount) {
if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
config.getMinIdle() < idleCount) ||
config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
return true;
}
return false;
}
1)當(dāng)空閑資源列表大小超過 minIdle 最小空閑資源數(shù)時玉雾,并且資源配置的 idleSoftEvictTime 小于資源空閑時間,返回 true轻要。
EvictionConfig 配置初始化時复旬,idleSoftEvictTime 如果使用的默認(rèn)值 -1 < 0,則賦予值為 Long.MAX_VALUE冲泥。
2)當(dāng)檢測的資源空閑時間過期后驹碍,即大于資源池配置的最小空閑時間,返回true凡恍。表示這些資源處于空閑狀態(tài)志秃,該時間段內(nèi)一直未被使用到。
以上兩個滿足其中任一條件嚼酝,則會銷毀資源對象浮还。
ensureIdle() 方法源代碼:
private void ensureIdle(final int idleCount, final boolean always) throws Exception {
if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
return;
}
// 資源池里保留idleCount(minIdle)最小資源數(shù)量
while (idleObjects.size() < idleCount) {
final PooledObject<T> p = create();
if (p == null) {
// Can't create objects, no reason to think another call to
// create will work. Give up.
break;
}
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}
以上就是對線程池的基本原理和參數(shù)的分析。
4闽巩、線程池對象狀態(tài)
線程池對象的狀態(tài)定義在 PooledObjectState 钧舌,是個枚舉類型,有以下值:
IDLE 處于空閑狀態(tài)
ALLOCATED 被使用中
EVICTION 正在被Evictor驅(qū)逐器驗(yàn)證
VALIDATION 正在驗(yàn)證
INVALID 驅(qū)逐測試或驗(yàn)證失敗并將被銷毀
ABANDONED 被拋棄狀態(tài)又官,對象取出后延刘,很久未歸還
RETURNING 歸還到對象池中
一張圖來了解下線程池狀態(tài)機(jī)轉(zhuǎn)換:
5、對象池初始化時機(jī)
思考個問題六敬,資源池里對象什么時候初始化進(jìn)去的碘赖?這里的資源池就是指上文圖中的 idleObjects
空閑資源對象緩存列表。是在創(chuàng)建對象時還是歸還對象時外构?
答案是歸還對象的時候
普泡。
某些場景,啟動后可能會出現(xiàn)超時現(xiàn)象审编,因?yàn)槊看握埱蠖紩?chuàng)建新的資源撼班,這個過程會有一定的開銷。
應(yīng)用啟動后我們可以提前做下線程池資源的預(yù)熱垒酬,示例代碼如下:
List<Jedis> minIdleList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = pool.getResource();
minIdleList.add(jedis);
jedis.ping();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = minIdleList.get(i);
jedis.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}
如果不了解原理砰嘁,可能以為上面的預(yù)熱代碼不大對吧,怎么獲取后又調(diào)用了 jedis.close()
呢勘究?字面上理解是把資源關(guān)閉了嘛矮湘。
一起看下線程池資源歸還對象的源碼就明白了。
GenericObjectPool#returnObject() 歸還對象方法源碼:
// GenericObjectPool#returnObject() 歸還方法
public void returnObject(final T obj) {
// allObjects是存儲所有對象資源的地方
final PooledObject<T> p = allObjects.get(new IdentityWrapper<T>(obj));
// ...
// 變更對象狀態(tài)
synchronized(p) {
final PooledObjectState state = p.getState();
if (state != PooledObjectState.ALLOCATED) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
}
p.markReturning(); // Keep from being marked abandoned
}
final long activeTime = p.getActiveTimeMillis();
// testOnReturn為true口糕,返還時驗(yàn)證資源有效性
if (getTestOnReturn()) {
if (!factory.validateObject(p)) {
try {
destroy(p);
} catch (final Exception e) {
swallowException(e);
}
try {
ensureIdle(1, false);
} catch (final Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return;
}
}
// ...
if (!p.deallocate()) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
}
// 獲取maxIdle缅阳,限制空閑資源保留的上限數(shù)量
final int maxIdleSave = getMaxIdle();
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
try {
destroy(p);
} catch (final Exception e) {
swallowException(e);
}
} else {
// 重點(diǎn)在這里,如果沒有超過maxIdle景描,則會將歸還的對象添加到 idleObjects 中
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}
updateStatsReturn(activeTime);
}
歸還對象時十办,首先會變更對象狀態(tài)從 ALLOCATED 到 RETURNING秀撇,如果 testOnReturn參數(shù) 為true,校驗(yàn)資源有效性(Jedis連接的有效性)向族,如果無效呵燕,則調(diào)用 destroy() 方法銷毀對象,當(dāng) maxIdle 未超過 idleObjects 資源列表大小時炸枣,則會將歸還的對象添加到 idleObjects 中虏等。
而在 borrorObject() 的借出對象方法中就是從 **idleObjects#pollFirst() **獲取對象的,沒有的話就會去創(chuàng)建适肠,對象最多不能超過 maxTotal 數(shù)量霍衫。
6、Jedis客戶端線程池參數(shù)
我們了解完 Apache Common Pool2 框架的線程池原理之后侯养,接下來看看 Jedis 里是如何包裝的敦跌。
線程池里的參數(shù)都是基于 JedisPoolConfig
來構(gòu)建的。
JedisPoolConfig Jedis資源池配置類默認(rèn)構(gòu)造函數(shù):
public class JedisPoolConfig extends GenericObjectPoolConfig {
public JedisPoolConfig() {
// defaults to make your life with connection pool easier :)
setTestWhileIdle(true);
setMinEvictableIdleTimeMillis(60000);
setTimeBetweenEvictionRunsMillis(30000);
setNumTestsPerEvictionRun(-1);
}
}
JedisPoolConfig 繼承了 GenericObjectPoolConfig逛揩,JedisPoolConfig 默認(rèn)構(gòu)造函數(shù)中會將 testWhileIdle 參數(shù)設(shè)置為true(默認(rèn)為false)柠傍,minEvictableIdleTimeMillis設(shè)置為60秒(默認(rèn)為30分鐘),timeBetweenEvictionRunsMillis設(shè)置為30秒(默認(rèn)為-1)辩稽,numTestsPerEvictionRun設(shè)置為-1(默認(rèn)為3)惧笛。
每個30秒執(zhí)行一次空閑資源監(jiān)測,發(fā)現(xiàn)空閑資源超過60秒未被使用逞泄,從資源池中移除患整。
創(chuàng)建 JedisPoolConfig 對象后,設(shè)置一些參數(shù):
// 創(chuàng)建 JedisPoolConfig 對象喷众,設(shè)置參數(shù)
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig()
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxIdle(60);
jedisPoolConfig.setMaxWaitMillis(1000);
jedisPoolConfig.setTestOnBorrow(false);
jedisPoolConfig.setTestOnReturn(true);
JedisPool
管理了Jedis 的線程池:
// JedisPool 構(gòu)造函數(shù)
public JedisPool(final GenericObjectPoolConfig poolConfig, final String host, int port,
int timeout, final String password) {
this(poolConfig, host, port, timeout, password, Protocol.DEFAULT_DATABASE, null);
}
public abstract class Pool<T> implements Closeable {
protected GenericObjectPool<T> internalPool;
// 抽象 Pool 構(gòu)造函數(shù)
public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
initPool(poolConfig, factory);
}
}
[6] 站在Redis服務(wù)端視角分析
既然猜測可能跟 Redis 服務(wù)端有關(guān)系各谚,就需要從跟客戶端的參數(shù)配置去分析下,是否會有所影響到千。
1昌渤、Redis客戶端緩沖區(qū)滿了
Redis有三種客戶端緩沖區(qū):
普通客戶端緩沖區(qū)(normal):
用于接受普通的命令,例如get憔四、set膀息、mset、hgetall等
slave客戶端緩沖區(qū)(slave):
用于同步master節(jié)點(diǎn)的寫命令了赵,完成復(fù)制潜支。
發(fā)布訂閱緩沖區(qū)(pubsub):
pubsub不是普通的命令,因此有單獨(dú)的緩沖區(qū)斟览。
Redis的客戶端緩沖區(qū)配置具體格式是:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
(1)class: 客戶端類型: normal、 slave辑奈、 pubsub
(2)hard limit: 如果客戶端使用的輸出緩沖區(qū)大于hard limit苛茂,客戶端會被立即關(guān)閉已烤。
(3)soft limit和soft seconds: 如果客戶端使用的輸出緩沖區(qū)超過了soft limit并且持續(xù)了soft limit秒,客戶端會被立即關(guān)閉
連接 Redis 查看 client-output-buffer-limit:
127.0.0.1:6379> config get client-output-buffer-limit
1) "client-output-buffer-limit"
2) "normal 0 0 0 slave 21474836480 16106127360 60 pubsub 33554432 8388608 60"
普通客戶端緩沖區(qū)normal類型的class妓羊、hard limit胯究、soft limit 都是 0,表示關(guān)閉緩沖區(qū)的限制躁绸。
如果緩沖期過小的裕循,就可能會導(dǎo)致的 Unexpected end of stream
異常。
2净刮、Redis服務(wù)器 timeout 設(shè)置不合理
Redis服務(wù)器會將超過 timeout
時間的閑置連接主動斷開剥哑。
查看服務(wù)器的timeout配置:
127.0.0.1:6379> config get timeout
1) "timeout"
2) "600"
timeout 配置為 600 秒,同一個連接等待閑置 10 分鐘后淹父,發(fā)現(xiàn)還沒有被使用株婴,Redis 就將該連接中斷掉了。
所以這里就會有個問題暑认,這里的 timeout
時間是要與上文中的 Jedis 線程池里的 空閑資源監(jiān)測任務(wù)
有關(guān)系的困介。
假設(shè) JedisPoolConfig 里的 timeBetweenEvictionRunsMillis
不設(shè)置,會使用默認(rèn)值 -1蘸际,不會啟動 Evictor 空閑監(jiān)測任務(wù)了座哩。
當(dāng)從資源池借出 Jedis 連接后,注意此時粮彤,如果過了 10 分鐘根穷,Redis 服務(wù)端已將這根連接給中斷了。
而客戶端還拿著這個 Jedis 連接去繼續(xù)操作 set驾诈、get 之類的命令缠诅,就會出現(xiàn) Unexpected end of stream
異常了。
示例演示:
為了方便演示乍迄,如下參數(shù)調(diào)整管引。
1)Redis服務(wù)器 timeout 初始化為 10秒
2)Java 測試代碼如下所示
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(" jedis.get(\"foo\"): " + jedis.get("foo"));
try {
Thread.sleep(12000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
輸出結(jié)果:
// 第一次輸出
jedis.get("foo"): bar
// sleep 12秒,Redis 服務(wù)器 timeout 等待 10秒斷開 Jedis 連接
// 再次執(zhí)行 jedis.get("foo") 闯两,異常出現(xiàn)了
Exception in thread "Thread-58" redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:151)
at redis.clients.jedis.Protocol.read(Protocol.java:215)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:259)
at redis.clients.jedis.Connection.getBulkReply(Connection.java:248)
at redis.clients.jedis.Jedis.get(Jedis.java:153)
所以褥伴,JedisPoolConfig 缺省構(gòu)造函數(shù)里,直接啟動了 Evictor 任務(wù)漾狼,在客戶端線程池里自身來監(jiān)測空閑的連接重慢,發(fā)現(xiàn)超過了 minEvictableIdleTimeMillis
設(shè)置的時間,從資源池里剔除逊躁。
避免客戶端獲取到了連接似踱,但是無法正常使用,導(dǎo)致一些異常的出現(xiàn)。
Redis服務(wù)器里的 timeout
這個值是否合理核芽,還是要結(jié)合自身業(yè)務(wù)場景來定囚戚。
據(jù)說阿里云Redis(公司內(nèi)沒用過)中 timeout 設(shè)置為 0,也就是不會主動關(guān)閉空閑連接轧简;緩沖區(qū)設(shè)置為 0 0 0 驰坊,也就是不會對客戶端緩沖區(qū)進(jìn)行限制,一般不會有問題哮独。
3拳芙、網(wǎng)絡(luò)不穩(wěn)定因素
回到本文開頭提到的 Sentry 告警的 JedisConnectionException 異常棧信息。
回顧異常棧如下所示:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:151)
at redis.clients.jedis.Protocol.read(Protocol.java:215)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
at redis.clients.jedis.BinaryJedis.auth(BinaryJedis.java:2139)
at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:108)
at
是在創(chuàng)建新的資源連接時皮璧,connect 之后的 auth 驗(yàn)證密碼時拋出了 Unexpected end of stream
舟扎。
經(jīng)過上述細(xì)致的分析,排除了 Redis 客戶端緩沖區(qū)滿 和 timeout 參數(shù)設(shè)置合理性之后恶导,剩下可能就跟網(wǎng)絡(luò)因素有關(guān)系了浆竭。此前,在容器外的虛擬機(jī)惨寿、物理機(jī)部署的應(yīng)用是沒有出現(xiàn)過此問題邦泄,當(dāng)前是在 k8s 容器內(nèi)偶爾出現(xiàn),需要運(yùn)維配合熟悉下容器的網(wǎng)絡(luò)架設(shè)裂垦,通過工具轉(zhuǎn)包來排查網(wǎng)絡(luò)問題顺囊,進(jìn)一步明確原因。
根據(jù)最終分析結(jié)果蕉拢,因「網(wǎng)絡(luò)抖動」之類偶發(fā)的問題特碳,可以在客戶端增加重試機(jī)制
來解決。
另外晕换,我們也在 k8s 容器里對 Redis 集群做了多次測試午乓,暫時也未能發(fā)現(xiàn)性能問題。
[7] 資源池生產(chǎn)配置合理性分析
如果你拿不準(zhǔn) Jedis 線程池參數(shù)設(shè)置的是否合理闸准,可以配置一些核心參數(shù)益愈,線上通過 JMX 工具去觀察。
再次看下 JMX 工具查看屬性:
CreatedCount:已創(chuàng)建的資源池對象數(shù)量
DestoryedCount:已銷毀的資源池對象總數(shù)量
DestoryedByEvictorCount:通過 Evictor 空閑監(jiān)測任務(wù)銷毀的資源池對象數(shù)量
BorrowedCount:從資源池借出對象的次數(shù)
ReturnedCount:歸還給資源池對象的次數(shù)
通過監(jiān)控可以看到 CreatedCount 為 6393夷家, DestoryedByEvictorCount 為 6381蒸其,說明大部分對象剛剛創(chuàng)建之后,沒過多久库快,都被空閑資源監(jiān)測 Evictor 任務(wù)給銷毀了摸袁。
根據(jù)前文中 Evictor 配置的參數(shù)「每隔 30 秒執(zhí)行一次任務(wù),如果池中對象超過 60 秒未使用义屏,對象即被銷毀掉」靠汁。
而 Redis 服務(wù)器端 timeout 是 10 分鐘蜂大,如果我們不想讓對象被銷毀的那么快,盡量保留在資源池中蝶怔,減少因創(chuàng)建新連接的開銷時間县爬,可以優(yōu)化空閑監(jiān)測任務(wù)的參數(shù)。
參數(shù)優(yōu)化示例:
// defaults to make your life with connection pool easier :)
jedisPoolConfig.setTestWhileIdle(true);
// 增加連接最小空閑時間添谊,在資源池里多保留一段時間
jedisPoolConfig.setMinEvictableIdleTimeMillis(180000);
// 檢測任務(wù)執(zhí)行時間周期
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
// 檢測任務(wù)執(zhí)行時,每次的采樣數(shù)察迟,比如設(shè)置為5
jedisPoolConfig.setNumTestsPerEvictionRun(-1);
根據(jù)參數(shù)分析斩狱,顯然 maxIdle 設(shè)置為 60, maxTotal 為 100過大了扎瓶,適當(dāng)調(diào)整該值所踊。
jedisPoolConfig.setMaxTotal(30);
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMinIdle(5);
jedisPoolConfig.setMaxWaitMillis(1000);
另外,根據(jù)空閑資源檢測任務(wù)中的驅(qū)逐策略分析概荷,可以利用 softMinEvictableIdleTimeMillis
和 minIdle
兩個參數(shù)組合使用秕岛,比如 softMinEvictableIdleTimeMillis 設(shè)置為 180 秒,minIdle 設(shè)置為 5误证,當(dāng)資源空閑時間超過 180 秒继薛,并且 idleObjects 空閑列表大小超過了 minIdle 最小空閑資源數(shù),才會將資源從池中移除掉愈捅。
由此遏考,保證了資源池有一定數(shù)量(minIdle)的資源連接存在,不會導(dǎo)致頻繁創(chuàng)建新的資源連接蓝谨。
遇到某些異常灌具,你也可以去 Jedis github 的 ISSUE里去搜索下是否有了答案。
本文的主要分析思路也是源于ISSUE#932和ISSUE#1092展開分析的譬巫,但是每個人遇到的問題都不同咖楣,解決方式也不一樣。
比如ISSUE#1092最后回復(fù)給出的答案:
在Redis服務(wù)端將 timeout 設(shè)置為0芦昔,這樣避免Redis主動斷開連接诱贿,然后客戶端 maxIdle 設(shè)置為 0。
這位仁兄對參數(shù)配置有些過于『暴力』烟零,這樣是不可取的瘪松,maxIdle 為 0,資源池沒有充分利用起來锨阿,每次請求都會新建資源連接宵睦,歸還后馬上就銷毀了。
不過他因此這么改墅诡,分析的原因是對的壳嚎,就是一根連接被Redis給斷開了桐智,客戶端還拿著在那使用呢,能不出問題嘛烟馅。
[8] 本文總結(jié)
本文由 Redis Java 客戶端的一個異常引出说庭,從監(jiān)控到的異常堆棧整個過程進(jìn)行了細(xì)致分析。
站在Jedis客戶端視角郑趁,對 Jedis 客戶端內(nèi)部使用的 Apache Common Pool2 開源框架線程池的基本原理刊驴,包括創(chuàng)建對象、銷毀對象寡润、空閑資源監(jiān)測任務(wù)機(jī)制做了具體分析捆憎。
由于線程池使用的配置參數(shù),通過工具或源碼分析 JedisPool 線程池里的參數(shù)合理性設(shè)置梭纹。
站在 Redis 服務(wù)端視角躲惰,分析了 Redis 服務(wù)器端的客戶端緩沖區(qū)參數(shù)和 timeout 參數(shù)設(shè)置是否合理,什么情況下會導(dǎo)致 Unexpected end of stream 異常的出現(xiàn)变抽。
通過本文了解到 Redis 客戶端產(chǎn)生的異常础拨,跟 Redis 客戶端和服務(wù)端都是有關(guān)系的,對于客戶端工具(框架)基本原理要有所了解绍载,才能更好的應(yīng)對各類異常诡宗,找到問題根源所在。
有時大部分應(yīng)用的性能問題都可以通過參數(shù)來調(diào)優(yōu)击儡,前提是你要對這些參數(shù)配置以及背后的原理深入分析僚焦,才能斗膽嘗試調(diào)優(yōu)。
本文僅提到了 Unexpected end of Stream 異常曙痘,除了該異常外芳悲,其他 Jedis 客戶端拋出的異常,本文的分析也是有幫助的边坤。
這里匯總了一些常見的 Jedis 異常:
1)blockWhenExhausted = true 當(dāng)?shù)却?maxWaitMillis 時間仍然無法獲取到連接名扛,會拋出:
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
2)blockWhenExhausted = false 當(dāng)無法獲得連接,會拋出
Caused by: java.util.NoSuchElementException: Pool exhausted
一般檢查Redis慢查詢阻塞是否存在茧痒;maxWaitMillis設(shè)置是否過短肮韧;
3)Redis 無法連接,連接時會被拒絕旺订,會拋出
Caused by: java.net.ConnectException: Connection refused
一般檢查 Redis 域名配置正確性弄企;排查該段時間網(wǎng)絡(luò)是否有問題。
4)客戶端讀寫超時区拳,會拋出
JedisConnectionException: java.net.SocketTimeoutException: Read timed out
5)連接超時拘领,會拋出
JedisConnectionException: java.net.SocketTimeoutException: connect timed out
4)、5)考慮讀寫超時設(shè)置的過短樱调;有慢查詢或者Redis發(fā)生阻塞约素;網(wǎng)絡(luò)不穩(wěn)定 方向去分析届良。
6)pipeline的錯誤使用,會拋出
JedisDataException: Please close pipeline or multi block before calling this method.
按照pipeline最佳實(shí)踐去使用圣猎,比如批量結(jié)果的解析士葫,建議使用pipeline.syncAndReturnAll()。
其他的異常送悔,你就見招拆招吧慢显。
文末了,碼字不易,如有疏漏箍铲,還請指正,希望對大家有所幫助,謝謝纱注。
參考資料:
https://yq.aliyun.com/articles/236384?spm=a2c4e.11155435.0.0.e21e2612uQAVoW#cc1
https://github.com/xetorthio/jedis/issues/932