記一次 Redis 連接問題排查
問題發(fā)現(xiàn)
客戶端:業(yè)務應用使用 lettuce 客戶端
服務端:Redis server 部署架構(gòu)采用 1 主 + 1 從 + 3 哨兵
Redis 和業(yè)務應用部署在同一個 K8s 集群中,Redis Server 暴露了一個 redis-service,指向到 master 節(jié)點,業(yè)務應用通過 redis-service 連接 Redis。
某個時刻起,開始發(fā)現(xiàn)業(yè)務報錯,稍加定位,發(fā)現(xiàn)是 Redis 訪問出了問題,搜索業(yè)務應用日志,發(fā)現(xiàn)關(guān)鍵信息:
這是一個 Redis 訪問的報錯,看起來跟 Redis 的讀寫配置有關(guān)。
問題定位
首先排查下業(yè)務應用和 Redis 的連接情況
其中 172.24.7.34 是業(yè)務 pod 的 ip,10.96.113.219 是 redis 的 K8s service ip,連接是 ESTABLISHED 狀態(tài),說明連接沒有斷。
繼續(xù)排查 Redis 的 pod 是否正常:
無論是讀寫節(jié)點還是哨兵節(jié)點,都沒有重啟過。
既然報了只讀節(jié)點的異常,索性看下 redis 節(jié)點的讀寫角色情況。
可以看到此時 redis-shareredis-0(172.24.1.95)是 slave 節(jié)點,redis-shareredis-1(172.24.1.96)是 master 節(jié)點。
排查到這里,猜測是業(yè)務 pod 實際通過 K8s service 連到了 slave 節(jié)點。進入 slave 確認這一信息,發(fā)現(xiàn)果然如此,并且 master 節(jié)點并沒有檢查到有該業(yè)務 pod 的連接
懷疑是某個時刻開始,master 和 slave 角色發(fā)生了互換,而主從切換過程中由于 pod 沒有重啟,長連接會一直保留著,此時即使 Redis service 的 endpoint 被修正,也不會影響到已有的連接。
為了驗證上述猜想,著手排查 Redis server 節(jié)點和 sentinel 節(jié)點。
查看 Redis 哨兵日志:
可以看到在 2023/2/14 14:53 (時區(qū)+8)時發(fā)生了主從切換。
嘗試排查主從切換的原因,進到 redis-0 查看日志:
從日志分析是主從同步時出現(xiàn)了網(wǎng)絡分區(qū),導致哨兵進行重新選主,但為什么出現(xiàn)網(wǎng)絡分區(qū),就無從得知了,K8s 中兩個 pod 之間的通信都能出現(xiàn) Connection lost 的確挺詭異的。
到這里,問題的根源基本定位清楚了。
問題復盤
無論 Redis 的主從切換是故意的還是不小心,都應當被當做是一個常態(tài),程序需要兼容這類場景。反映出兩個問題:
- 問題一,Redis 使用了哨兵機制,程序應當首選通過哨兵連接 Redis
- 問題二,Lettuce 客戶端沒有自動斷開錯誤的連接
那么改進思路自然是有兩種,一是改用哨兵連接 Redis,二是替換掉 Lettuce。對于本文遇到的問題,方案一可能可以,但不能確保沒有其他極端情況導致其他連接問題,所以我實際采用的是方案二,使用 Jedis 替換掉 Lettuce。
項目一開始采用 Lettuce,主要是因為 spring-boot-data-redis 默認采用了 Lettuce 的實現(xiàn),盡管我一開始已經(jīng)留意到搜索引擎中諸多關(guān)于 Lettuce 的問題,但實際測試發(fā)現(xiàn),高版本 Lettuce 基本均已修復了這些問題,忽略了特殊場景下其可能存在的風險。簡單對比下 Jedis 和 Lettuce:
- Lettuce:
- Lettuce 客戶端沒有連接?;钐綔y,錯誤連接存在連接池中會造成請求超時報錯。
- Lettuce 客戶端未實現(xiàn) testOnBorrow 等連接池檢測方法,無法在使用連接之前進行連接校驗。
- Jedis:
Jedis 客戶端實現(xiàn)了 testOnBorrow、testWhileIdle、testOnReturn 等連接池校驗配置。
開啟 testOnBorrow 在每次借用連接前都會進行連接校驗,可靠性最高,但是會影響性能(每次 Redis 請求前會進行探測)。
testWhileIdle 可以在連接空閑時進行連接檢測,合理配置閾值可以及時剔除連接池中的異常連接,防止使用異常連接造成業(yè)務報錯。
在空閑連接檢測之前,連接出現(xiàn)問題,可能會造成使用該連接的業(yè)務報錯,此處可以通過參數(shù)控制檢測間隔(timeBetweenEvictionRunsMillis)。
因此,Jedis 客戶端在面對連接異常,網(wǎng)絡抖動等場景下的異常處理和檢測能力明顯強于 Lettuce,可靠性更強。
參數(shù) | 配置介紹 | 配置建議 |
maxTotal | 最大連接,單位:個 | 根據(jù)Web容器的Http線程數(shù)來進行配置,估算單個Http請求中可能會并行進行的Redis調(diào)用次數(shù),例如:Tomcat中的Connector內(nèi)的maxConnections配置為150,每個Http請求可能會并行執(zhí)行2個Redis請求,在此之上進行部分預留,則建議配置至少為:150 x 2 + 100= 400限制條件:單個Redis實例的最大連接數(shù)。maxTotal和客戶端節(jié)點數(shù)(CCE容器或業(yè)務VM數(shù)量)數(shù)值的乘積要小于單個Redis實例的最大連接數(shù)。例如:Redis主備實例配置maxClients為10000,單個客戶端maxTotal配置為500,則最大客戶端節(jié)點數(shù)量為20個。 |
maxIdle | 最大空閑連接,單位:個 | 建議配置為maxTotal一致。 |
minIdle | 最小空閑連接,單位:個 | 一般來說建議配置為maxTotal的X分之一,例如此處常規(guī)配置建議為:100。對于性能敏感的場景,防止經(jīng)常連接數(shù)量抖動造成影響,也可以配置為與maxIdle一致,例如:400。 |
maxWaitMillis | 最大獲取連接等待時間,單位:毫秒 | 獲取連接時最大的連接池等待時間,根據(jù)單次業(yè)務最長容忍的失敗時間減去執(zhí)行命令的超時時間得到建議值。例如:Http最大容忍超時時間為15s,Redis請求的timeout設置為10s,則此處可以配置為5s。 |
timeout | 命令執(zhí)行超時時間,單位:毫秒 | 單次執(zhí)行Redis命令最大可容忍的超時時間,根據(jù)業(yè)務程序的邏輯進行選擇,一般來說處于對網(wǎng)絡容錯等考慮至少建議配置為210ms以上。特殊的探測邏輯或者環(huán)境異常檢測等,可以適當調(diào)整達到秒級。 |
minEvictableIdleTimeMillis | 空閑連接逐出時間,大于該值的空閑連接一直未被使用則會被釋放,單位:毫秒 | 如果希望系統(tǒng)不會經(jīng)常對連接進行斷鏈重建,此處可以配置一個較大值(xx分鐘),或者此處配置為-1并且搭配空閑連接檢測進行定期檢測。 |
timeBetweenEvictionRunsMillis | 空閑連接探測時間間隔,單位:毫秒 | 根據(jù)系統(tǒng)的空閑連接數(shù)量進行估算,例如系統(tǒng)的空閑連接探測時間配置為30s,則代表每隔30s會對連接進行探測,如果30s內(nèi)發(fā)生異常的連接,經(jīng)過探測后會進行連接排除。根據(jù)連接數(shù)的多少進行配置,如果連接數(shù)太大,配置時間太短,會造成請求資源浪費。對于幾百級別的連接,常規(guī)來說建議配置為30s,可以根據(jù)系統(tǒng)需要進行動態(tài)調(diào)整。 |
testOnBorrow | 向資源池借用連接時是否做連接有效性檢測(ping),檢測到的無效連接將會被移除。 | 對于業(yè)務連接極端敏感的,并且性能可以接受的情況下,可以配置為True,一般來說建議配置為False,啟用連接空閑檢測。 |
testWhileIdle | 是否在空閑資源監(jiān)測時通過ping命令監(jiān)測連接有效性,無效連接將被銷毀。 | True |
testOnReturn | 向資源池歸還連接時是否做連接有效性檢測(ping),檢測到無效連接將會被移除。 | False |
maxAttempts | 在JedisCluster模式下,您可以配置maxAttempts參數(shù)來定義失敗時的重試次數(shù)。 | 建議配置3-5之間,默認配置為5。根據(jù)業(yè)務接口最大超時時間和單次請求的timeout綜合配置,最大配置不建議超過10,否則會造成單次請求處理時間過長,接口請求阻塞。 |
再次回到本次案例,如果使用了 Jedis,并且配置了合理的連接池策略,可能仍然會存在問題,因為 Jedis 底層檢測連接是否可用,使用的是 ping 命令,當連接到只讀節(jié)點,ping 命令仍然可以工作,所以實際上連接檢查機制并不能解決本案例的問題。
但 Jedis 提供了一個 minEvictableIdleTimeMillis 參數(shù),該參數(shù)表示一個連接至少停留在 idle 狀態(tài)的最短時間,然后才能被 idle object evitor 掃描并驅(qū)逐,該參數(shù)會受到 minIdle 的影響,驅(qū)逐到 minIdle 的數(shù)量。也就意味著:默認配置 minEvictableIdleTimeMillis=60s,minIdle=0 下,連接在空閑時間達到 60s 時,將會被釋放。由于實際的業(yè)務場景 Redis 讀寫空閑達到 60s 的場景是很常見的,所以該方案勉強可以達到在主從切換之后,在較短時間內(nèi)恢復。但如果 minIdle > 0,這些連接依舊會有問題。而 Lettuce 默認配置下,連接會一直存在。
出于一些不可描述的原因,我無法將應用連接 Redis 的模式切換成哨兵模式,所以最終采取了切換到 Jedis 客戶端,并且配置 minIdle=0、minEvictableIdleTimeMillis=60s 的方案。
問題總結(jié)
當使用域名/K8s Service 連接 Redis 集群時,需要考慮主從切換時可能存在的問題。Redis 通常使用長連接通信,主從切換時如果連接不斷開,會導致無法進行寫入操作??梢栽诳蛻舳恕⒎斩藘蓚€層面規(guī)避這一問題,以下是一些行之有效的方案:
- 客戶端連接哨兵集群,哨兵會感知到主從切換,并推送給客戶端這一變化
- 客戶端配置 minIdle=0,及時斷開空閑的連接,可以一定程度規(guī)避連接已經(jīng)不可用但健康檢測又檢查不出來的場景。(即本文的場景)
- 服務端主從切換時斷開所有已有的連接,依靠客戶端的健康檢測以及重連等機制,確保連接到正確的節(jié)點。
Redis 客戶端推薦使用 Jedis 客戶端,其在面對連接異常,網(wǎng)絡抖動等場景下的異常處理和檢測能力明顯強于 Lettuce。