網站首頁 編程語言 正文
背景
系統原來是使用(HAProxy+Sentinel+redis)做的高可用方案,該方案HAProxy無法感知到主從切換,導致寫操作失敗。具體可參考之前的文章。
為了解決這個問題,拿掉了HAProxy,然后使用Jedis的Sentinel模式。
# 代碼有刪減
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(sentinel.getMaster(), sentinelHostAndPorts);
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisSentinelConfiguration, jedisPoolConfig);
jedisConnectionFactory.afterPropertiesSet();
切換新方案上線后,白天業務高峰期,redis報錯:
org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: open to many files.
定位思路
1.解決open to many files
open to many files一般是超過了系統打開文件數限制。linux一切皆文件,包括網絡連接。因此猜測是網絡高峰期網絡請求太多導致。
解決方法
通過ulimit命令查看當前限制,并通過lsof -p pid查看進程當前已打開文件數。發現系統限制為4096,當前已打開文件數已達到這個限制。
因此先使用ulimit提高這個限制。
結果
當提高系統文件限制數量后,觀測打開文件數仍在不斷增長。對比以往業務量,這個現象明顯有異常,因此分析連接的分布,發現大部分都是redis的連接。
2.定位為什么redis的連接數會不斷增長
對比之前版本,本版本只是修改的redis連接方式,運維這邊懷疑是哨兵模式本身有問題。個人覺得redis哨兵模式幾年前就發布了,要是有BUG,早報到開源社區修復了。之前分析哨兵模式源碼時,主從切換jedis會清理連接池,而且redis本身沒有事務(具備完整的acid特性,此處表達不夠準確),訪問完后就會釋放連接,本身不應該有問題。
連接數不斷增長有可能的原因應該就2點:
1.redis連接有泄漏,未回收?????????????????????? --測試環境沒有復現問題,此原因存疑。
2.現網就是有這么大訪問量,導致redis連接需求有這么高。????????--由于系統配置原因,無法使用jstack查看線程堆棧來確認這一點。(因為redis沒有事務,執行完查詢后,連接直接釋放,所以一個線程理論上最多只會持有一個redis連接,所以可以通過線程堆棧來確認)
嘗試一:確認redis連接釋放是否存在失敗的可能
經過源碼排查(源碼分析見另外一篇文章),不太有可能。
- 如果釋放失敗,會拋異常打日志。
- 如上jedis這個包已經在全球現網經受了考驗,不太可能有這方面的bug。
嘗試二:系統原班開發人員提供了一個疑點
執行multi命令后,未執行exec()方法。
stringRedisTemplate.multi();
stringRedisTemplate.boundHashOps(hkey).put(vkey, vval);
stringRedisTemplate.expireAt(hkey, TimeUtil.startDate(incre));
multi指令是為redis事務準備的,雖然無法進行回退,但是他能保證指令的原子性(redis是單線程的,通過multi包裝的指令,redis一次性全部執行完,中間不會插入其他連接發過來的指令)。
為了保證指令的原子性,redis連接肯定是需要被綁定的(執行multi后,當前連接需要跟線程綁定,這樣在執行exec前都需要從ThreadLocal去獲取同一個連接),不然上面3行代碼都是用不同的連接,是無法實現multi指令的。
初步看上述代碼嫌疑很大。
結果,驗證失敗
本地按照現網的模式,寫了一個測試代碼,并使用redis客戶端,通過info clients發現連接數并不如預期會不斷增長。
String prefix = "test";
for (int i = 0; i < 100; i++) {
String key = prefix + String.valueOf(i);
new Thread(() -> {
while (true) {
stringRedisTemplate.multi();
stringRedisTemplate.boundHashOps(key).put("a", "vval");
stringRedisTemplate.expireAt(key, TimeUtil.startDate(1));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
通過閱讀源碼發現,redistemplate只有啟用enableTransactionSupport后,才會開啟事務功能。
此配置未開啟的情況下,即使是執行multi方法,執行完后釋放連接的時候,會立即執行exec方法,進行自動提交。
大家可以執行跟蹤muti方法的執行,主方法在Redistemplate#execute(RedisCallback action, boolean exposeConnection, boolean pipeline)
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
RedisConnectionFactory factory = getRequiredConnectionFactory();
RedisConnection conn = null;
try {
if (enableTransactionSupport) {
# 如果開啟事務,會對將Redis連接綁定到線程的上下文,方便后面執行獲取到的是同一個連接。
# 同時會對RedisConnection進行包裝,因為需要攔截close方法,防止連接被回收到連接池
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
# 走這個分支
conn = RedisConnectionUtils.getConnection(factory);
}
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
T result = action.doInRedis(connToExpose);
return postProcessResult(result, connToUse, existingConnection);
} finally {
# 執行完命令后,需要釋放連接。這就是為啥之前強調一個線程最多只用使用一個redis連接。因為一般的指令執行完后,連接會立即釋放
# 線程里面的執行都是串行執行的,所以不可能一個線程同時執行2個Redis方法,來持有2條redis連接
RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
}
}
到此已經沒有辦法分析下去了。
嘗試三:只能通過dump線程及內存去分析當前線程情況及內存對象。
之前現網jdk的命令執行不成功,因此準備在測試環境搗鼓一下,順便看看測試環境是不是確實沒問題。
準備工作:
- 使用命令dump內存,jmap -dump:format=b,file=xxx.bin pid
- 使用visualvm導入內存文件
通過分析dump文件發現,redis連接確實存在泄漏。
如圖:Redis的連接狀態是ALLOCATED,但是除了連接池,卻沒有其他對象持有它。這個說明之前確實有使用到這個連接,而且使用這個連接的對象已經被回收了。
所以有2個方面的原因:
- 要么回收連接的時候,拋了異常導致狀態沒有修改成功。 --貌似不太可能,全球大量使用的庫不可能有這種低級錯誤,而且自身排查源碼也沒發現什么疑點。
- 代碼中是否用到了某個redis特性,開啟了事務,同時未提交事務,導致連接未釋放
- 有地方沒有釋放資源 --需要全面排查代碼
對于第二點,如果開啟了事務,會綁定連接到線程上下文的。可以追蹤到TransactionSynchronizationManager#resources字段。被綁定的連接都是存在這個里面,因此想通過dump出來的內存,去查找這種對象的信息。遺憾的是,visualvm查找起來太麻煩,遂放棄。
于是找原班人馬分析一下用到的redis高階功能,這樣可以快速縮小范圍。
根據提供的范圍,快速鎖定了下面的代碼。因為游標類似滑動窗口,一般都會保持與服務端的連接,這樣可以實現分批次找服務端查詢數據,防止單次查詢數據量過大,內存溢出。這里代碼又沒有顯示的關閉游標,泄漏很可能是這里導致。 (ps:要是還不是這個原因,真滴就腦殼疼了,已經快沒招了,只能頭鐵去驗證第二點了)
public Map<String, String> hscan(final String key, int count) {
ScanOptions scanOptions = ScanOptions.scanOptions().count(count).match("xxxx").build();
Cursor<Map.Entry<Object, Object>> cursor = stringRedisTemplate.opsForHash().scan(key, scanOptions);
while (cursor.hasNext()){
Map.Entry<Object,Object> entry = cursor.next();
result.put(entry.getKey().toString(),entry.getValue().toString());
}
return result;
}
根據scan方法定位到RedisTemplate的executeWithStickyConnection方法,很驚喜的發現這里沒有關閉連接。
@Override
public <T extends Closeable> T executeWithStickyConnection(RedisCallback<T> callback) {
Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
Assert.notNull(callback, "Callback object must not be null");
RedisConnectionFactory factory = getRequiredConnectionFactory();
RedisConnection connection = preProcessConnection(RedisConnectionUtils.doGetConnection(factory, true, false, false),
false);
return callback.doInRedis(connection);
}
理論上上面的分析已經能夠解釋Redis連接泄漏的問題了,剩的就是本地復現。
答疑
為啥切換之前沒有這個問題,切換之前,沒有啟用連接池。當Curse對象被GC回收后,RedisConection也會被GC被回收,從而釋放連接。
切換后,RedisConection除了被Curse持有,還會被連接池持有。Curse回收后,RedisConection仍然在內存中,然后狀態還是ALLOCATED無法被復用,這樣新連接會不斷的被創建,最終達到ulimit的限制。
原文鏈接:https://blog.csdn.net/shuxiaohua/article/details/122127056
相關推薦
- 2022-08-21 C語言實現隊列的示例詳解_C 語言
- 2022-03-18 docker?創建容器時指定容器ip的實現示例_docker
- 2023-04-07 C語言中的編碼小技巧_C 語言
- 2022-03-14 Feign客戶端消費服務超時:com.netflix.hystrix.exception.Hystr
- 2023-01-30 Numpy?np.array()函數使用方法指南_python
- 2023-03-03 詳解Flask框架中Flask-Login模塊的使用_python
- 2023-03-22 Redis慢查詢日志及慢查詢分析詳解_Redis
- 2023-02-07 go?reflect要不要傳指針原理詳解_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支