問題背景
某在線教育平臺(tái)在一個(gè)工作日上午 10:00(業(yè)務(wù)高峰時(shí)段)收到大量線上報(bào)警:
用戶端:頁面加載失敗、提交作業(yè)超時(shí)
服務(wù)端:Java 應(yīng)用頻繁報(bào)出redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
Redis 監(jiān)控:connected_clients達(dá)到上限maxclients=10000,rejected_connections開始出現(xiàn)
更嚴(yán)重的是,Redis 連接數(shù)打滿后,連鎖導(dǎo)致依賴 Redis 的認(rèn)證服務(wù)、會(huì)話服務(wù)、緩存服務(wù)全部不可用,流量進(jìn)一步轉(zhuǎn)移到剩余的正常服務(wù),引發(fā)了小范圍的業(yè)務(wù)雪崩。
一、Redis 連接機(jī)制快速理解
1.1 Redis 如何處理連接
Redis 是單線程事件循環(huán)模型(6.0 之后網(wǎng)絡(luò) IO 可多線程,但核心處理仍是單線程)。每個(gè)客戶端連接占用一個(gè)文件描述符(fd),Redis 通過epoll或kqueue處理事件。
關(guān)鍵配置項(xiàng):
maxclients 10000 # 最大連接數(shù)(默認(rèn) 10000,Redis 2.4+) timeout 0 # 連接空閑超時(shí)(0 表示永不超時(shí)) tcp-keepalive 300 # TCP keepalive 間隔
1.2 連接數(shù)打滿的影響
當(dāng)連接數(shù)達(dá)到maxclients后,Redis 不再接受新連接,并直接在日志中輸出:
# Redis 9001 refused connection (maxclients) -ERR max number of clients reached
此時(shí)所有依賴 Redis 的服務(wù)都會(huì)受到影響:
新請(qǐng)求無法獲取 Redis 連接 → 業(yè)務(wù)線程阻塞
阻塞線程不斷重試 → 線程池打滿 → Web 容器無可用線程
上游服務(wù)的健康檢查失敗 → 從注冊中心摘除節(jié)點(diǎn)
流量轉(zhuǎn)移到剩余節(jié)點(diǎn) → 剩余節(jié)點(diǎn)的 Redis 連接也暴增 → 級(jí)聯(lián)故障
二、排查過程
2.1 直接檢查 Redis 連接狀況
# 登錄 Redis 查看連接數(shù) $ redis-cli -h-p 6379 -a INFO clients # Clients connected_clients:10000 client_longest_output_list:0 client_biggest_input_buf:0 blocked_clients:0 cluster_connections:0 maxclients:10000
connected_clients=10000且maxclients=10000,連接數(shù)已打滿。
2.2 查看連接分布
# 列出所有客戶端連接 $ redis-cli CLIENT LIST id=12345 addr=10.0.1.12:54321 fd=23 name= age=12345 idle=456 flags=N ... id=12346 addr=10.0.1.13:54322 fd=24 name= age=12300 idle=12 flags=N ... ...
輸出列含義:
addr:客戶端 IP 和端口
age:連接存在時(shí)間(秒)
idle:連接空閑時(shí)間(秒)
flags:N 表示普通客戶端,M 表示主從,S 表示從庫
按 IP 統(tǒng)計(jì)連接數(shù):
# 統(tǒng)計(jì)各客戶端 IP 的連接數(shù)
$ redis-cli CLIENT LIST | awk'{print $2}'| cut -d= -f2 | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
3000 10.0.1.12
2800 10.0.1.14
1500 10.0.1.13
1200 10.0.1.15
...
2.3 查找僵尸連接
# 查找空閑時(shí)間大于 300 秒的連接
$ redis-cli CLIENT LIST | awk -F'[ =]''{for(i=1;i<=NF;i++) if($i=="idle") print $(i+1), $0}'?| awk?'$1 > 300'| head -20
# 更簡潔的方式
$ redis-cli CLIENT LIST | grep -E"idle=[0-9]{4,}"
發(fā)現(xiàn)大量連接的idle值在 600~3600 秒之間,說明這些連接長時(shí)間空閑但沒有被回收。timeout配置為 0(永不超時(shí))。
注:Redis 7.0+ 引入CLIENT NO-TOUCH和CLIENT NO-EVICT指令,但timeout仍是控制僵尸連接的主要參數(shù)。
2.4 確認(rèn)系統(tǒng)級(jí)限制
Redis 的maxclients受系統(tǒng)文件描述符限制和內(nèi)核參數(shù)限制:
# Redis 進(jìn)程的 fd 限制 $ cat /proc/$(pidof redis-server)/limits | grep"Max open files" Max open files 10024 10024 files # 系統(tǒng)級(jí) fd 限制 $ cat /proc/sys/fs/file-max 100000 # 當(dāng)前 fd 使用量 $ cat /proc/sys/fs/file-nr 30000 0 100000
Redis 進(jìn)程的Max open files為 10024,與 maxclients 10000 非常接近(Redis 本身還需要占用少量 fd 用于監(jiān)聽端口、AOF 寫入等)。
2.5 應(yīng)用層排查
檢查應(yīng)用側(cè)的 Redis 連接池配置:
3000 個(gè)應(yīng)用實(shí)例(部署了 15 個(gè)節(jié)點(diǎn) × 每個(gè)節(jié)點(diǎn) maxTotal=200) × 連接復(fù)用不足 → 實(shí)際連接數(shù)遠(yuǎn)超預(yù)期。
三、根因分析
經(jīng)過以上排查,確定本次事故由三個(gè)因素疊加導(dǎo)致:
3.1 直接原因:應(yīng)用發(fā)布后連接數(shù)暴增
當(dāng)天凌晨發(fā)布新版本后,應(yīng)用啟動(dòng)時(shí)連接池的minIdle=10導(dǎo)致每個(gè)節(jié)點(diǎn)預(yù)先建立 10 條連接。發(fā)布方式是滾動(dòng)更新,舊節(jié)點(diǎn)和新節(jié)點(diǎn)短暫共存,連接數(shù)翻倍。部分舊節(jié)點(diǎn)上的連接在優(yōu)雅關(guān)閉時(shí)未正確釋放,變成僵尸連接。
3.2 放大因素:timeout = 0
Redis 配置timeout=0(永不主動(dòng)斷開空閑連接)。大量僵尸連接(idle > 600s)未被回收,持續(xù)占用連接槽位。等到上午業(yè)務(wù)高峰到來時(shí),新連接請(qǐng)求直接打到上限。
3.3 雪崩鏈路
Redis 連接滿(maxclients=10000) → 新請(qǐng)求無法獲取連接 → JedisConnectionException → 應(yīng)用層無熔斷降級(jí) → 線程被重試阻塞 → Web 容器線程池打滿 → 健康檢查接口超時(shí) → 注冊中心摘除節(jié)點(diǎn) → 流量轉(zhuǎn)移到剩余節(jié)點(diǎn) → 剩余節(jié)點(diǎn) Redis 連接數(shù)飆升 → 也打滿 → 服務(wù)全面不可用
四、快速止血方案
4.1 方案 A:臨時(shí)增大 maxclients(推薦首選)
# 臨時(shí)修改(重啟后失效) $ redis-cli CONFIG SET maxclients 20000
注意:增大 maxclients 的同時(shí)必須增大系統(tǒng)的 fd 限制:
# 臨時(shí)修改 Redis 進(jìn)程的 fd 限制 $ prlimit --pid $(pidof redis-server) --nofile=30000 # 或修改系統(tǒng)級(jí)限制 $ulimit-n 65535
永久修改/etc/security/limits.conf:
# 添加以下行 root soft nofile 65535 root hard nofile 65535 redis soft nofile 65535 redis hard nofile 65535
Redis 配置文件中的maxclients也要同步修改:
# /etc/redis/redis.conf maxclients 20000
4.2 方案 B:啟用 timeout 自動(dòng)清理僵尸連接
# 設(shè)置空閑超時(shí) 60 秒(立即生效) $ redis-cli CONFIG SET timeout 60
60 秒后,所有空閑超過 60 秒的連接會(huì)被 Redis 自動(dòng)關(guān)閉。這會(huì)觸發(fā)以下效果:
# 觀察連接數(shù)變化 $ watch -n 5'redis-cli INFO clients | grep connected_clients'
timeout值應(yīng)根據(jù)業(yè)務(wù)心跳間隔設(shè)置。一般建議:
Web 應(yīng)用 + 連接池:timeout=60~300
長連接訂閱/推送:timeout=600~3600
僅做緩存:timeout=60
4.3 方案 C:批量清理異常連接
如果上述方案來不及等待,直接按條件 kill 連接:
# 按類型 kill(kill 所有普通客戶端連接,保留主從復(fù)制連接) $ redis-cli CLIENT KILL TYPE normal # 按 IP 段 kill(如果某臺(tái)應(yīng)用服務(wù)器連接異常) $ redis-cli CLIENT KILL addr 10.0.1.12:0 # 跳過 skipme(是否 kill 當(dāng)前連接,默認(rèn) yes) $ redis-cli CLIENT KILL addr 10.0.1.12:0 skipme no
4.4 方案 D:重啟應(yīng)用(讓連接池重建)
作為兜底方案,重啟應(yīng)用讓連接池重新初始化:
# 重啟應(yīng)用服務(wù)(確保是滾動(dòng)重啟) $ systemctl restart app-service
重啟后連接數(shù)會(huì)在短時(shí)間內(nèi)回歸正常水平(minIdle 重新建立)。
五、長期治理方案
5.1 連接池最佳實(shí)踐
生產(chǎn)環(huán)境合理的連接池配置:
配置原則:
maxTotal 不要過大:單個(gè)應(yīng)用節(jié)點(diǎn)對(duì)單個(gè) Redis 實(shí)例的 maxTotal 建議 20~100,視并發(fā)度而定。連接數(shù)不是越多越好,Redis 處理 10000 個(gè)空閑連接和 500 個(gè)活躍連接的開銷完全不同。
設(shè)置 maxWaitMillis:避免線程無限等待連接,建議 1000~3000ms。
開啟 testWhileIdle:定時(shí)檢測空閑連接是否可用,避免連接被 Redis 側(cè)關(guān)閉后應(yīng)用仍在使用。
代碼中正確歸還連接:使用 try-with-resources(Jedis 3.x+ 支持)或 finally 塊確保 close。
// Jedis 3.x 推薦用法
try(Jedis jedis = jedisPool.getResource()) {
jedis.set("key","value");
}// 自動(dòng)歸還連接,無需顯式 close
// Jedis 2.x 必須顯式 close
Jedis jedis =null;
try{
jedis = jedisPool.getResource();
jedis.set("key","value");
}finally{
if(jedis !=null) {
jedis.close(); // 歸還到連接池而非真正關(guān)閉
}
}
5.2 使用連接代理收斂架構(gòu)
如果有大量客戶端直連 Redis,考慮引入連接代理層:
[應(yīng)用實(shí)例] x 50個(gè) → [Twemproxy / Predixy] → [Redis 主從]
優(yōu)勢:
連接數(shù)從 M × N 降為 M + N(M 是應(yīng)用節(jié)點(diǎn),N 是 Redis 節(jié)點(diǎn))
代理層可以緩沖突發(fā)連接請(qǐng)求
支持讀寫分離和故障轉(zhuǎn)移
缺點(diǎn):
增加一層網(wǎng)絡(luò)跳轉(zhuǎn),延遲增加 0.1~0.5ms
代理本身可能成為瓶頸
5.3 連接限額和防火墻防護(hù)
# 限制單臺(tái)應(yīng)用服務(wù)器的 Redis 連接數(shù)(iptables) $ iptables -A INPUT -p tcp --dport 6379 -m connlimit --connlimit-above 50 -j REJECT
5.4 熔斷降級(jí)和重連退避
應(yīng)用側(cè)必須實(shí)現(xiàn)熔斷機(jī)制:
// 使用 Resilience4j CircuitBreaker
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50% 失敗率觸發(fā)熔斷
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔斷后等 30 秒
.slidingWindowSize(10)
.build();
// 或簡單的退避策略
intbaseDelay =100; // 基礎(chǔ)等待 100ms
for(inti =0; i < maxRetries; i++) {
? ??try?{
? ? ? ??return?jedisPool.getResource();
? ? }?catch?(Exception e) {
? ? ? ? Thread.sleep(baseDelay * (long)Math.pow(2, i)); ?// 指數(shù)退避
? ? }
}
5.5 監(jiān)控和告警
必須監(jiān)控的 Redis 連接指標(biāo):
# Prometheus redis_exporter 已暴露的關(guān)鍵指標(biāo) redis_connected_clients redis_config_maxclients redis_rejected_connections_total
告警閾值建議:
| 指標(biāo) | 告警閾值 | 嚴(yán)重級(jí)別 |
|---|---|---|
| connected_clients/maxclients | > 80% | Warning |
| connected_clients/maxclients | > 90% | Critical |
| rejected_connections > 0 | 立即 | P0 Emergency |
六、生產(chǎn)環(huán)境注意事項(xiàng)
CONFIG SET maxclients需要聯(lián)動(dòng)調(diào)整多個(gè)參數(shù):
缺了任何一個(gè),maxclients設(shè)置不生效。
Redis 配置maxclients
系統(tǒng)的ulimit -n
內(nèi)核的fs.file-max
/etc/security/limits.conf
不要在高峰期修改 timeout:如果timeout從 0 改為一個(gè)很小的值(如 30),會(huì)瞬間斷開大量連接,導(dǎo)致客戶端連接池出現(xiàn)批量重建連接的場景,可能觸發(fā) CPU 暴漲和網(wǎng)絡(luò)抖動(dòng)。
CLIENT KILL 需要注意 skipme:如果當(dāng)前連接也在 kill 范圍內(nèi),會(huì)導(dǎo)致當(dāng)前命令執(zhí)行中斷。默認(rèn) skipme=yes 不 kill 當(dāng)前連接。
云 Redis 服務(wù)的 maxclients 限制:各云廠商 Redis 實(shí)例的maxclients可能與實(shí)例規(guī)格綁定(如 2GB 實(shí)例 = maxclients 10000),不能無限上調(diào)。提工單前先確認(rèn)產(chǎn)品文檔。
連接池泄漏排查:如果頻繁出現(xiàn)連接打滿但CLIENT LIST看不出來異常,檢查應(yīng)用代碼中是否將 Jedis 實(shí)例作為成員變量而非局部變量使用,或者異常分支未執(zhí)行 close。
七、總結(jié)
Redis 連接數(shù)打滿導(dǎo)致業(yè)務(wù)雪崩的排查和治理可以分為三層:
第一層:快速止血
CONFIG SET maxclients 20000 # 增大上限 CONFIG SET timeout 60 # 清理僵尸連接 CLIENT KILL TYPE normal # 批量殺異常連接
第二層:排查根因
檢查連接分布 → 分析空閑時(shí)間 → 核對(duì)連接池配置 → 確認(rèn)系統(tǒng)限制
第三層:架構(gòu)治理
連接池規(guī)范化 → 熔斷降級(jí) → 代理收斂 → 監(jiān)控告警 → 應(yīng)急預(yù)案
本次故障的核心教訓(xùn)是:
timeout=0在生產(chǎn)環(huán)境是高風(fēng)險(xiǎn)配置,必須設(shè)置合理的空閑超時(shí)
連接池的 minIdle/maxTotal 要根據(jù)實(shí)際并發(fā)度計(jì)算,而非隨意配置
服務(wù)必須有熔斷降級(jí)機(jī)制,否則單一組件故障會(huì)級(jí)聯(lián)擴(kuò)散
發(fā)布過程中要監(jiān)控連接數(shù)變化,發(fā)現(xiàn)異常立即回滾或調(diào)整
-
網(wǎng)絡(luò)
+關(guān)注
關(guān)注
14文章
8341瀏覽量
95618 -
線程
+關(guān)注
關(guān)注
0文章
511瀏覽量
20877 -
Redis
+關(guān)注
關(guān)注
0文章
395瀏覽量
12260
原文標(biāo)題:一次 Redis 連接數(shù)打滿導(dǎo)致業(yè)務(wù)雪崩的排查記錄
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
2011全球移動(dòng)連接數(shù)將增至56億
ESP32-C3超過了最大站點(diǎn)連接數(shù)如何解決?
請(qǐng)問ESP32藍(lán)牙連接數(shù)如何設(shè)置?
企業(yè)打開Redis的正確方式,來自阿里云云數(shù)據(jù)庫團(tuán)隊(duì)的解讀
請(qǐng)問如何讓一個(gè)數(shù)從零開始遞增每遞增一次間隔1s記錄一次記錄完成再進(jìn)行遞增依次循環(huán)
防火墻的并發(fā)連接數(shù)
[Ganglia監(jiān)控?cái)U(kuò)展]監(jiān)控nginx的連接數(shù)
一次Redis連接數(shù)打滿導(dǎo)致業(yè)務(wù)雪崩的排查記錄
評(píng)論