一孝偎、背景
某一日收到上游調(diào)用方的反饋,提供的某一個(gè)Dubbo接口凉敲,每天在固定的時(shí)間點(diǎn)被短時(shí)間熔斷衣盾,拋出的異常信息為提供方dubbo線程池被耗盡。當(dāng)前dubbo接口日請(qǐng)求量18億次爷抓,報(bào)錯(cuò)請(qǐng)求94W/天势决,至此開始了優(yōu)化之旅。
二蓝撇、快速應(yīng)急
2.1 快速定位
首先進(jìn)行常規(guī)的系統(tǒng)信息監(jiān)控(機(jī)器果复、JVM內(nèi)存、GC渤昌、線程)据悔,發(fā)現(xiàn)雖稍有突刺,但都在合理范圍內(nèi)耘沼,且跟報(bào)錯(cuò)時(shí)間點(diǎn)對(duì)不上,先暫時(shí)忽略朱盐。
其次進(jìn)行流量分析群嗤,發(fā)現(xiàn)每天固定時(shí)間點(diǎn)會(huì)有流量突增的情況,流量突增的點(diǎn)跟報(bào)錯(cuò)的時(shí)間點(diǎn)也吻合兵琳,初步判斷為短時(shí)大流量導(dǎo)致狂秘。
流量趨勢(shì)
被降級(jí)量
接口99線
三、尋找性能瓶頸點(diǎn)
3.1 接口流程分析
3.1.1 流程圖
3.1.2 流程分析
收到請(qǐng)求后調(diào)用下游接口躯肌,使用hystrix熔斷器者春,熔斷時(shí)間為500MS;
根據(jù)下游接口返回的數(shù)據(jù)清女,進(jìn)行詳情數(shù)據(jù)的封裝钱烟,第一步先到本地緩存中獲取,如果本地緩存沒(méi)有嫡丙,則從Redis進(jìn)行回源拴袭,Redis中無(wú)則直接返回,異步線程從數(shù)據(jù)庫(kù)進(jìn)行回源曙博。
如果第一步調(diào)用下游接口異常拥刻,則進(jìn)行數(shù)據(jù)兜底,兜底流程為先到本地緩存中獲取父泳,如果本地緩存沒(méi)有般哼,則從Redis進(jìn)行回源吴汪,Redis中無(wú)則直接返回,異步線程從數(shù)據(jù)庫(kù)進(jìn)行回源蒸眠。
3.2 性能瓶頸點(diǎn)排查
3.2.1 下游接口服務(wù)耗時(shí)比較長(zhǎng)
調(diào)用鏈顯示漾橙,雖然下游接口的P99線在峰值流量時(shí)存在突刺,超出1S黔宛,但因?yàn)槿蹟喑瑫r(shí)的設(shè)置(熔斷時(shí)間500MS近刘,coreSize&masSize=50,下游接口平均耗時(shí)10MS以下)臀晃,判斷下游接口不是問(wèn)題的關(guān)鍵點(diǎn)觉渴,為進(jìn)一步排除干擾,在下游服務(wù)存在突刺時(shí)能快速失敗徽惋,調(diào)整熔斷時(shí)間為100MS案淋,dubbo超時(shí)時(shí)間100MS。
3.2.2 獲取詳情本地緩存無(wú)數(shù)據(jù)险绘,Redis回源
借助調(diào)用鏈平臺(tái)踢京,第一步分析Redis請(qǐng)求流量,以此來(lái)判斷本地緩存的命中率宦棺,發(fā)現(xiàn)Redis的流量是接口流量的2倍瓣距,從設(shè)計(jì)上來(lái)說(shuō)不應(yīng)該出現(xiàn)這個(gè)現(xiàn)象。開始代碼Review代咸,發(fā)現(xiàn)在有一處邏輯出現(xiàn)了問(wèn)題蹈丸。
沒(méi)有從本地緩存讀取,而是直接從Redis中獲取了數(shù)據(jù)呐芥,Redis最大響應(yīng)時(shí)間也確實(shí)發(fā)現(xiàn)了不合理的突刺逻杖,繼續(xù)分析發(fā)現(xiàn)Redis響應(yīng)時(shí)間和Dubbo99線突刺情況基本一致,感覺此時(shí)已經(jīng)找到了問(wèn)題的原因思瘟,心中暗喜荸百。
Redis請(qǐng)求流量
重試
服務(wù)接口請(qǐng)求流量
Dubbo99線
Redis最大響應(yīng)時(shí)間
3.2.3 獲取兜底數(shù)據(jù)本地緩存無(wú)數(shù)據(jù),Redis回源
正常
3.2.4 記錄請(qǐng)求結(jié)果入Redis
因?yàn)楫?dāng)前Redis做了資源隔離滨攻,且未在DB后臺(tái)查詢到慢日志够话,此時(shí)分析導(dǎo)致Redis變慢的原因有很多,不過(guò)其他的都被主觀忽略了铡买,注意力都在請(qǐng)求Redis流量翻倍的問(wèn)題上了更鲁,故優(yōu)先解決3.2.2中的問(wèn)題。
四奇钞、解決方案
4.1 3.3.2中定位的問(wèn)題上線
上線前Redis請(qǐng)求量
上線后Redis請(qǐng)求量
上線后Redis流量翻倍問(wèn)題得到解決澡为,Redis最大響應(yīng)時(shí)間突刺有所緩解,但依舊沒(méi)能徹底解決景埃,說(shuō)明大流量查詢不是最根本的原因媒至。
redis最大響應(yīng)時(shí)間(上線前)
redis最大響應(yīng)時(shí)間(上線后)
4.2 Redis擴(kuò)容
在Redis異常流量問(wèn)題解決后顶别,問(wèn)題并未得到徹底解決,此時(shí)能做的就是靜下心來(lái)拒啰,仔細(xì)去梳理導(dǎo)致Redis慢的原因驯绎,思路主要從以下三個(gè)方面:
出現(xiàn)了慢查詢
Redis服務(wù)出現(xiàn)性能瓶頸
客戶端配置不合理
基于以上思路,一個(gè)個(gè)的進(jìn)行排查谋旦;查詢Redis慢查詢?nèi)罩臼JВ窗l(fā)現(xiàn)慢查詢。
借用調(diào)用鏈平臺(tái)詳細(xì)分析慢的Redis命令册着,沒(méi)有了大流量導(dǎo)致的慢查詢的干擾拴孤,問(wèn)題定位流程很快,大量的耗時(shí)請(qǐng)求在setex方法上甲捏,偶爾出現(xiàn)查詢的慢請(qǐng)求也都是在setex方法之后演熟,根據(jù)Redis單線程的特性判斷setex是Redis99線突刺的元兇。找到具體語(yǔ)句司顿,定位到具體業(yè)務(wù)后芒粹,首先申請(qǐng)擴(kuò)容Redis,由6個(gè)master擴(kuò)到8個(gè)master大溜。
Redis擴(kuò)容前
Redis擴(kuò)容后
從結(jié)果上看化漆,擴(kuò)容基本上沒(méi)有效果,說(shuō)明redis服務(wù)本身不是性能瓶頸點(diǎn)钦奋,此時(shí)剩下的一個(gè)就是客戶端相關(guān)配置了获三。
4.3 客戶端參數(shù)優(yōu)化
4.3.1 連接池優(yōu)化
Redis擴(kuò)容沒(méi)有效果,針對(duì)客戶端可能出現(xiàn)的問(wèn)題锨苏,此時(shí)懷疑的點(diǎn)有兩個(gè)方向。
第一個(gè)是客戶端在處理Redis集群模式時(shí)棺聊,對(duì)連接的管理上存在BUG伞租,第二個(gè)是連接池參數(shù)設(shè)置不合理,此時(shí)源碼分析和連接池參數(shù)調(diào)整同步進(jìn)行限佩。
4.3.1.1 判斷客戶端連接管理上是否有BUG
在分析完葵诈,客戶端處理連接池的源碼后,沒(méi)有問(wèn)題祟同,跟預(yù)想一致作喘,按照槽位緩存連接池,第一個(gè)假設(shè)被排除晕城,源碼如下泞坦。
<pre class="public-DraftStyleDefault-pre" data-offset-key="42knj-0-0" style="margin: 1.4em 0px; padding: 0.88889em; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: auto; background: rgb(246, 246, 246); border-radius: 4px; color: rgb(18, 18, 18); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<pre class="Editable-styled" data-block="true" data-editor="ftb92" data-offset-key="42knj-0-0" style="margin: 0px; padding: 0px; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: initial; background: rgb(246, 246, 246); border-radius: 0px;">
1、setEx
public String setex(final byte[] key, final int seconds, final byte[] value) {
return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
@Override
public String execute(Jedis connection) {
return connection.setex(key, seconds, value);
}
}.runBinary(key);
}
2砖顷、runBinary
public T runBinary(byte[] key) {
if (key == null) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
}
return runWithRetries(key, this.maxAttempts, false, false);
}
3贰锁、runWithRetries
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
if (attempts <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
}
Jedis connection = null;
try {
if (asking) {
// TODO: Pipeline asking with the original command to make it
// faster....
connection = askConnection.get();
connection.asking();
// if asking success, reset asking flag
asking = false;
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
return execute(connection);
}
4赃梧、getConnectionFromSlot
public Jedis getConnectionFromSlot(int slot) {
JedisPool connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
// It can't guaranteed to get valid connection because of node
// assignment
return connectionPool.getResource();
} else {
renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state
connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
return connectionPool.getResource();
} else {
//no choice, fallback to new connection to random node
return getConnection();
}
}
}
</pre>
</pre>
4.3.1.2 分析連接池參數(shù)
通過(guò)跟中間件團(tuán)隊(duì)溝通,以及參考commons-pool2官方文檔修改如下豌熄;
參數(shù)調(diào)整后授嘀,1S以上的請(qǐng)求量得到減少,但還是存在锣险,上游反饋降級(jí)量由每天90萬(wàn)左右降到每天6W個(gè)(關(guān)于maxWaitMillis設(shè)置為200MS后為什么還會(huì)有超過(guò)200MS的請(qǐng)求蹄皱,下文有解釋)。
參數(shù)優(yōu)化后Reds最大響應(yīng)時(shí)間
參數(shù)優(yōu)化后接口報(bào)錯(cuò)量
4.3.2 持續(xù)優(yōu)化
優(yōu)化不能停止芯肤,如何把Redis的所有寫入請(qǐng)求降低到200MS以內(nèi)巷折,此時(shí)的優(yōu)化思路還是調(diào)整客戶端配置參數(shù),分析Jedis獲取連接相關(guān)源碼纷妆;
Jedis獲取連接源碼
<pre class="public-DraftStyleDefault-pre" data-offset-key="9b7rj-0-0" style="margin: 1.4em 0px; padding: 0.88889em; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: auto; background: rgb(246, 246, 246); border-radius: 4px; color: rgb(18, 18, 18); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<pre class="Editable-styled" data-block="true" data-editor="ftb92" data-offset-key="9b7rj-0-0" style="margin: 0px; padding: 0px; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: initial; background: rgb(246, 246, 246); border-radius: 0px;">
final AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
PooledObject<T> p = null;
// Get local copy of current config so it is consistent for entire
// method execution
final boolean blockWhenExhausted = getBlockWhenExhausted();
boolean create;
final long waitTime = System.currentTimeMillis();
while (p == null) {
create = false;
p = idleObjects.pollFirst();
if (p == null) {
p = create();
if (p != null) {
create = true;
}
}
if (blockWhenExhausted) {
if (p == null) {
if (borrowMaxWaitMillis < 0) {
p = idleObjects.takeFirst();
} else {
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
}
}
if (p == null) {
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
} else {
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
}
if (!p.allocate()) {
p = null;
}
if (p != null) {
try {
factory.activateObject(p);
} catch (final Exception e) {
try {
destroy(p);
} catch (final Exception e1) {
// Ignore - activation failure is more important
}
p = null;
if (create) {
final NoSuchElementException nsee = new NoSuchElementException(
"Unable to activate object");
nsee.initCause(e);
throw nsee;
}
}
if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
boolean validate = false;
Throwable validationThrowable = null;
try {
validate = factory.validateObject(p);
} catch (final Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
if (!validate) {
try {
destroy(p);
destroyedByBorrowValidationCount.incrementAndGet();
} catch (final Exception e) {
// Ignore - validation failure is more important
}
p = null;
if (create) {
final NoSuchElementException nsee = new NoSuchElementException(
"Unable to validate object");
nsee.initCause(validationThrowable);
throw nsee;
}
}
}
}
}
updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
return p.getObject();
</pre>
</pre>
獲取連接的大致流程如下:
是否有空閑連接盔几,有空閑連接就直接返回,沒(méi)有就創(chuàng)建掩幢;
創(chuàng)建時(shí)如果超出最大連接數(shù)逊拍,則判斷是否有其他線程在創(chuàng)建連接,如果沒(méi)則直接返回际邻,如果有則等待maxWaitMis時(shí)間(其他線程可能創(chuàng)建失斝旧ァ),如果未超出最大連接世曾,則執(zhí)行創(chuàng)建連接操作(此時(shí)獲取連接等待時(shí)間可能會(huì)大于maxWaitMs)缨恒。
如果創(chuàng)建不成功,則判斷是否是阻塞獲取連接轮听,如果不是則直接拋出異常骗露,連接池不夠用,如果是則判斷maxWaitMillis是否小于0血巍,如果小于0則阻塞等待萧锉,如果大于0則阻塞等待maxWaitMillis。
后續(xù)就是根據(jù)參數(shù)來(lái)判斷是否需要做連接check等述寡。
根據(jù)以上流程分析柿隙,maxWaitMills目前設(shè)置的為200,以上流程加起來(lái)最大阻塞時(shí)間為400MS鲫凶,大部分情況為200MS禀崖,不應(yīng)該出現(xiàn)超出400MS的突刺。
此時(shí)問(wèn)題可能出現(xiàn)在創(chuàng)建連接上螟炫,因?yàn)閯?chuàng)建連接比較耗時(shí)波附,且創(chuàng)建時(shí)間不定,重點(diǎn)分析是否有這個(gè)場(chǎng)景,通過(guò)DB后臺(tái)監(jiān)控Redis連接情況叶雹。
DB后臺(tái)監(jiān)控Redis服務(wù)連接
分析上圖發(fā)現(xiàn)财饥,確實(shí)在幾個(gè)時(shí)間點(diǎn)(9:00,12:00,19:00...)折晦,redis連接數(shù)存在上漲情況钥星,跟Redis突刺時(shí)間基本吻合。感覺(之前的各種嘗試后满着,已經(jīng)不敢用確定了)問(wèn)題到此定位清晰(在突增流量過(guò)來(lái)時(shí)谦炒,連接池可用連接滿足不了需求,會(huì)創(chuàng)建連接风喇,造成請(qǐng)求等待)宁改。
此時(shí)的想法是在服務(wù)啟動(dòng)時(shí)就進(jìn)行連接池的創(chuàng)建,盡量減少新連接的創(chuàng)建魂莫,修改連接池參數(shù)vivo.cache.depend.common.poolConfig.minIdle还蹲,結(jié)果竟然無(wú)效?耙考?谜喊?
啥都不說(shuō)了,開始擼源碼倦始,jedis底層使用的是commons-poll2來(lái)管理連接的斗遏,查看項(xiàng)目中使用的commons-pool2-2.6.2.jar部分源碼;
CommonPool2源碼
<pre class="public-DraftStyleDefault-pre" data-offset-key="c2oqp-0-0" style="margin: 1.4em 0px; padding: 0.88889em; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: auto; background: rgb(246, 246, 246); border-radius: 4px; color: rgb(18, 18, 18); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<pre class="Editable-styled" data-block="true" data-editor="ftb92" data-offset-key="c2oqp-0-0" style="margin: 0px; padding: 0px; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: initial; background: rgb(246, 246, 246); border-radius: 0px;">
public GenericObjectPool(final PooledObjectFactory<T> factory,
final GenericObjectPoolConfig<T> config) {
super(config, ONAME_BASE, config.getJmxNamePrefix());
if (factory == null) {
jmxUnregister(); // tidy up
throw new IllegalArgumentException("factory may not be null");
}
this.factory = factory;
idleObjects = new LinkedBlockingDeque<>(config.getFairness());
setConfig(config);
}
</pre>
</pre>
竟然發(fā)現(xiàn)沒(méi)有初始化連接的地方鞋邑,開始咨詢中間件團(tuán)隊(duì)诵次,中間件團(tuán)隊(duì)給出的源碼(commons-pool2-2.4.2.jar)如下,方法執(zhí)行后多了一次startEvictor方法的調(diào)用枚碗?
<pre class="public-DraftStyleDefault-pre" data-offset-key="f6uc7-0-0" style="margin: 1.4em 0px; padding: 0.88889em; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: auto; background: rgb(246, 246, 246); border-radius: 4px; color: rgb(18, 18, 18); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<pre class="Editable-styled" data-block="true" data-editor="ftb92" data-offset-key="f6uc7-0-0" style="margin: 0px; padding: 0px; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: initial; background: rgb(246, 246, 246); border-radius: 0px;">
1逾一、初始化連接池
public GenericObjectPool(PooledObjectFactory<T> factory,
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;
idleObjects = new LinkedBlockingDeque<PooledObject<T>>(config.getFairness());
setConfig(config);
startEvictor(getTimeBetweenEvictionRunsMillis());
}
</pre>
</pre>
為啥不一樣?肮雨?嬉荆?開始檢查Jar包,版本不一樣酷含,中間件給出的版本是在V2.4.2,項(xiàng)目實(shí)際使用的是V2.6.2汪茧,分析startEvictor有一步邏輯正是處理連接池預(yù)熱邏輯椅亚。
Jedis連接池預(yù)熱
<pre class="public-DraftStyleDefault-pre" data-offset-key="89itr-0-0" style="margin: 1.4em 0px; padding: 0.88889em; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: auto; background: rgb(246, 246, 246); border-radius: 4px; color: rgb(18, 18, 18); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">
<pre class="Editable-styled" data-block="true" data-editor="ftb92" data-offset-key="89itr-0-0" style="margin: 0px; padding: 0px; font-size: 0.9em; word-break: normal; overflow-wrap: normal; white-space: pre; overflow: initial; background: rgb(246, 246, 246); border-radius: 0px;">
1、final void startEvictor(long delay) {
synchronized (evictionLock) {
if (null != evictor) {
EvictionTimer.cancel(evictor);
evictor = null;
evictionIterator = null;
}
if (delay > 0) {
evictor = new Evictor();
EvictionTimer.schedule(evictor, delay, delay);
}
}
}
2舱污、class Evictor extends TimerTask {
/**
* Run pool maintenance. Evict objects qualifying for eviction and then
* ensure that the minimum number of idle instances are available.
* Since the Timer that invokes Evictors is shared for all Pools but
* pools may exist in different class loaders, the Evictor ensures that
* any actions taken are under the class loader of the factory
* associated with the pool.
*/
@Override
public void run() {
ClassLoader savedClassLoader =
Thread.currentThread().getContextClassLoader();
try {
if (factoryClassLoader != null) {
// Set the class loader for the factory
ClassLoader cl = factoryClassLoader.get();
if (cl == null) {
// The pool has been dereferenced and the class loader
// GC'd. Cancel this timer so the pool can be GC'd as
// well.
cancel();
return;
}
Thread.currentThread().setContextClassLoader(cl);
}
// Evict from the pool
try {
evict();
} catch(Exception e) {
swallowException(e);
} catch(OutOfMemoryError oome) {
// Log problem but give evictor thread a chance to continue
// in case error is recoverable
oome.printStackTrace(System.err);
}
// Re-create idle instances.
try {
ensureMinIdle();
} catch (Exception e) {
swallowException(e);
}
} finally {
// Restore the previous CCL
Thread.currentThread().setContextClassLoader(savedClassLoader);
}
}
}
3呀舔、 void ensureMinIdle() throws Exception {
ensureIdle(getMinIdle(), true);
}
4、 private void ensureIdle(int idleCount, boolean always) throws Exception {
if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
return;
}
while (idleObjects.size() < idleCount) {
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();
}
}
</pre>
</pre>
修改Jar版本,配置中心增加vivo.cache.depend.common.poolConfig.timeBetweenEvictionRunsMillis(檢查一次連接池中空閑的連接媚赖,把空閑時(shí)間超過(guò)minEvictableIdleTimeMillis毫秒的連接斷開,直到連接池中的連接數(shù)到minIdle為止)霜瘪。
vivo.cache.depend.common.poolConfig.minEvictableIdleTimeMillis(連接池中連接可空閑的時(shí)間,毫秒)兩個(gè)參數(shù),重啟服務(wù)后惧磺,連接池正常預(yù)熱颖对,最終從Redis層面上解決問(wèn)題。
優(yōu)化結(jié)果如下磨隘,性能問(wèn)題基本得到解決缤底;
Redis響應(yīng)時(shí)間(優(yōu)化前)
Redis響應(yīng)時(shí)間(優(yōu)化后)
接口99線(優(yōu)化前)
接口99線(優(yōu)化后)
五、總結(jié)
出現(xiàn)線上問(wèn)題時(shí)番捂,首先要考慮的還是快速恢復(fù)線上業(yè)務(wù)个唧,將業(yè)務(wù)的影響度降到最低,所以針對(duì)線上的業(yè)務(wù)设预,要提前做好限流徙歼、熔斷、降級(jí)等策略鳖枕,在線上出現(xiàn)問(wèn)題時(shí)能快速找到恢復(fù)方案魄梯。對(duì)公司各監(jiān)控平臺(tái)的熟練使用程度,決定了定位問(wèn)題的速度耕魄,每個(gè)開發(fā)都要把熟練使用監(jiān)控平臺(tái)(機(jī)器画恰、服務(wù)、接口吸奴、DB等)作為一個(gè)基本能力允扇。
Redis出現(xiàn)響應(yīng)慢時(shí),可以優(yōu)先從Redis集群服務(wù)端(機(jī)器負(fù)載则奥、服務(wù)是否有慢查詢)考润、業(yè)務(wù)代碼(是否有BUG)、客戶端(連接池配置是否合理)三個(gè)方面去排查读处,基本上能排查出大部分Redis慢響應(yīng)問(wèn)題糊治。
Redis連接池在系統(tǒng)冷啟動(dòng)時(shí),對(duì)連接池的預(yù)熱罚舱,不同commons-pool2的版本井辜,冷啟動(dòng)的策略也不同,但都需要配置minEvictableIdleTimeMillis參數(shù)才會(huì)生效管闷,可以看下common-pool2官方文檔粥脚,對(duì)常用參數(shù)都做到心中有數(shù),在問(wèn)題出現(xiàn)時(shí)能快速定位包个。
連接池默認(rèn)參數(shù)在解決大流量的業(yè)務(wù)上稍顯乏力刷允,需要針對(duì)大流量場(chǎng)景進(jìn)行調(diào)優(yōu)處理,如果業(yè)務(wù)上流量不是很大直接使用默認(rèn)參數(shù)即可。
具體問(wèn)題要具體分析树灶,不能解決問(wèn)題的時(shí)候要變通思路纤怒,通過(guò)各種方法去嘗試解決問(wèn)題。