0x00 起源
專案的一些微服務整合了 Spring Data Redis
,而底層的 Redis 客戶端是 lettuce
,這也是預設的客戶端。微服務在某些環境中執行很正常,但在另一些環境中執行就會間歇性的發生 RedisCommandTimeoutException
:有時長時間沒人使用(當然也不操作 Redis 了),例如一個晚上沒人作業系統,第二天早上使用時就會發生這個異常。而且發生該異常之後,訪問 Redis 就會一直拋這個異常,但過了一段時間後,又正常了。或者立即重啟微服務,也會正常了。
- lettuce 版本:5.3.0
- Redis 版本:官方 docker 映象, 5.0,預設配置
- Spring boot 版本:2.1.x
經過日誌排查(lettuce 的日誌級別需要開啟 DEBUG
或 TRACE
),發生RedisCommandTimeoutException
的原因時lettuce
的 Connection 已經斷了,發生異常後大約 15 分鐘,lettuce
的 ConnectionWatchdog
會進行自動重連。
那麼為何 lettuce 的 Connection 為什麼會斷呢?而 ConnectionWatchdog
為什麼沒有立即重連呢?又怎麼解決這些問題呢?這些問題如果不弄清楚不解決,會嚴重影響系統的可用性,總不能讓使用者等十幾分鍾再用吧,也不能總重啟應用吧。
網上也搜到了類似的問題,看來還是挺多人遇到相同的問題的。但大部分都沒說清楚這個現象的原因,也沒說真正的解決方法。網上幾乎全部的解決方法都是將lettuce
換成了 jedis
,迴避了這個問題。
0x01 本質
換成jedis
固然可以解決問題,但既然 lettuce
能成為Spring
預設的客戶端,還是有先進的地方的。而且遇到問題不搞清楚,心裡也癢癢的。下面會闡述這些問題的來龍去脈。
1.1 為什麼 Redis 連線會斷
其實這個問題並不是很重要,因為Socket
連線斷已經是事實,而且在分散式環境中,網路分割槽是必然的。在網路環境,Redis 伺服器主動斷掉連線是很正常的,lettuce 的作者也提及 lettuce 一天發生一兩次重連是很正常的。
那麼哪些情況會導致連線斷呢:
- Linux 核心的 keepalive 功能可能會一直收不到客戶端的回應;
- 收到與該連線相關的 ICMP 錯誤資訊;
- 其他網路鏈路問題等等;
如果要需要真正查明原因,需要 tcp dump 進行抓包,但意義不大,除非斷線的概率大,像一天一兩次或者幾天才一次不必花這麼大力氣去查這個。而最主要的問題是 lettuce 客戶端能否及時檢測到連線已斷,並儘快重連。
1.2 為何 lettuce 沒有立刻重連
lettuce
的重連機制這裡進行贅述,有興趣的同學可以參考 Redis客戶端Lettuce原始碼【四】 這篇文件或者自行閱讀 lettuce
中ConnectionWatchdog
的原始碼。
根據ConnectionWatchdog
重連的機制(收到netty
的ChannelInactived
事件後啟動重連的執行緒不斷進行連線)可以確定,連線是由 Redis 服務端斷開的,因為如果是客戶端主動斷開連線,那麼一定能收到ChannelInactived
,因此,之所以lettuce
要等 15 分鐘後才重連,是因為沒收到ChanelInactived
事件。
那麼為什麼客戶端沒有到ChannelInactived
事件呢?很多情況都會,例如:
- 客戶端沒收到服務端 FIN 包;
- 網路鏈路斷了,例如拔網線,斷電等等;
在我們這個情況,應該是沒收到服務端的 FIN 包。
好了,我們再來看另一個問題:日誌顯示發生RedisCommandTimeoutException
後,15 分鐘後收到ChannelInactived
事件。那麼,為什麼會大約是 15 分鐘而不是別的時間呢?
其實,這是與 Linux 底層Socket
的實現有關--這就是超時重傳機制。也就是/proc/sys/net/ipv4/tcp_retries2
引數,關於重傳機制,可以看這篇文章:
Linux TCP_RTO_MIN, TCP_RTO_MAX and the tcp_retries2 sysctl
根據重傳機制,發生RedisCommandTimeoutException
的命令會重傳 tcp_retries2
這麼多次,剛剛好是 15 分鐘左右。
小結:
問題的原因已經清楚了,這裡需要對 lettuce
的重連機制、netty
的工作原理、Linux socket
實現原理有一定的瞭解。既然問題的原因找到了,如何解決呢?顯然無論是網上說的替換Jedis
客戶端,還是重啟應用、還是等 15 分鐘,都不是好辦法。
0x02 解決方案
既然找到了問題原因所在,那麼可以根據這些原因來解決。主要有三種解決的方案:
2.1 設定 Linux 的 TCP_RETRIES2 引數
針對等待 15 分鐘,那麼就可以猜想是不是可以設定 Linux 的 TCP_RETRIES2 引數小點來縮短等待時間呢?答案是肯定的;這個引數 Linux 的預設值是 15,而有些應用(如 Oracle)要求設定為 3。
其實,一般情況下,tcp
資料包超時了,重發 3 次都不成功,重發再多幾次也是枉然的。
但是這個方案有個缺點:
如果修改了這個引數,也會影響到其他應用,因為這個是全域性的引數。那麼能否單獨針對某個應用程式設定 Socket Option
呢?很遺憾的是,筆者在 netty
裡並沒找到該選項的設定,無論是EpollChannelOption
還是 JDK 的ExtendedSocketOptions
。
所幸的是:
netty
提供另一個引數的設定:TCP_USER_TIMEOUT
,這個引數就是為了針對單獨設定某個應用程式的超時重傳的設定。下面一小節講述如何使用。
2.2 設定 Socket Option 的 TCP_USER_TIMEOUT 引數
在Spring Boot
的auto-configuration
中,ClientResources
的初始化是預設的 ClientResources
,因此,我們可以自定義一個 ClientResources
。
@Bean
public ClientResources clientResources(){
return ClientResources clientResources = ClientResources.builder()
.nettyCustomizer(new NettyCustomizer() {
@Override
public void afterBootstrapInitialized(Bootstrap bootstrap) {
bootstrap.option(EpollChannelOption.TCP_USER_TIMEOUT, 10);
}
})
.build();
}
2.3 定製 lettuce:增加心跳機制
上面兩個方案,縮短了等待的時長,都是依賴作業系統底層的通知。如果不想依賴底層作業系統的通知,唯一的辦法就是自己在應用層增加心跳機制。
如上述的方案,lettuce
提供了NettyCustomizer
進行擴充套件,熟悉netty
的同學,應該聽說過netty
所提供的心跳機制--IdleStateHandler
,結合這兩者,就很容易在初始化netty
時增加心跳機制:
@Bean
public ClientResources clientResources(){
NettyCustomizer nettyCustomizer = new NettyCustomizer() {
@Override
public void afterChannelInitialized(Channel channel) {
channel.pipeline().addLast(
new IdleStateHandler(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds));
channel.pipeline().addLast(new ChannelDuplexHandler() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
ctx.disconnect();
}
}
});
}
@Override
public void afterBootstrapInitialized(Bootstrap bootstrap) {
}
};
return ClientResources.builder().nettyCustomizer(nettyCustomizer ).build();
}
這裡由客戶端自己做心跳檢測,一旦發現Channel
死了,主動關閉ctx.close()
,那麼ChannelInactived
事件一定會被觸發了。但是這個方案有個缺點,增加了客戶端的壓力。
0x03 總結
lettuce
是一個優秀的開源軟體,設計和程式碼都很優美。通過這次的問題排查和解決問題,加深了自己對netty
,Linux Socket
機制、TCP/IP 協議的理解。
0x04 參考
4.1 Redis客戶端Lettuce原始碼【三】
4.2 Redis客戶端Lettuce原始碼【四】
4.3 Linux TCP_RTO_MIN, TCP_RTO_MAX and the tcp_retries2 sysctl
4.4 https://github.com/lettuce-io/lettuce-core/issues/762