為什麼 Lettuce 會帶來更長的故障時間?

帶你聊技術發表於2023-09-28

為什麼 Lettuce 會帶來更長的故障時間?

來源:阿里技術


這是2023年的第69篇文章

( 本文閱讀時間:15分鐘 )



本文詳述了阿里雲資料庫 Tair/Redis 將使用長連線客戶端在非預期故障當機切換場景下的恢復時間從最初的 900s 降到 120s 再到 30s的最佳化過程,涉及產品最佳化,開源產品問題修復等諸多方面。



01



背景

Lettuce1 是一款優秀的 Redis2 Java 客戶端,支援同步、非同步、流式等程式設計介面,深受使用者喜歡。2020 年開始,隨著其使用者量增大,很多使用者反饋其使用 Lettuce 客戶端時,在某些 Redis 故障當機情況下,Lettuce 會持續超時長達 15 分鐘,導致業務不可用。


阿里雲資料庫工程師也收到了客戶反饋,於是我們開始深入調查並持續跟蹤解決這個問題。終於,在最近 9 月份,這個問題得到了有效解決。下面我們以 Redis 的標準版架構來描述此問題(注意,即使在非雲環境,此問題仍舊存在)。


為什麼 Lettuce 會帶來更長的故障時間?

(圖 1. Redis 標準版雙副本切換流程)


  1. Redis 標準版架構中,開源 SDK 透過域名解析獲取到 VIP 地址,建連到 Ali-LB,再到 Redis Master(圖中 1’ 和 1 連線對應)。

  2. 當 Master 由於非預期故障直接當機,有機率不會產生 RST 

  3. HA 元件探測 Master 當機,呼叫 Ali-LB switch_rs 介面,將後端的連線從 Master 切換到 Replica。

  4. 切換完成後 Ali-LB 並不會主動釋放前端舊的客戶端連線,對於客戶端發到 Ali-LB 的包,由於後端不可用,預設丟棄,因此客戶端將持續超時。此時如果有新的連線建立(例如 4’,會建連到新的 Master)是不存在問題的,但 Lettuce 客戶端在超時情況下不會重新建立連線,因此舊連線存在問題。

  5. 直到到達 Ali-LB 的 est_timeout(預設 900s)之後,Ali-LB 會回覆 RST 斷開連線,之後客戶端恢復。

注:對於部分網路卡當機、網路分割槽故障等情況,機率性不會產生 RST。大多數的當機,作業系統會在退出前給客戶端傳送 RST,因此這個問題不是切換或者當機就必現;在正常切換情況下由於 Master 可服務,在第 3 步,HA 元件會主動傳送 client kill 命令給舊的 Master,從而讓客戶端發起一次重連恢復。



02



問題分析

  1. 首先這是一個 Lettuce 客戶端設計缺陷,原因見後文和其餘客戶端對比分析。

  2. 其次這是一個 Ali-LB 的不成熟的機制(切換之後保持靜默狀態,不關閉自己與 Client 的連線),因此所有使用 Ali-LB 的資料庫產品都會遇到,包括 RDS MySQL 等。

  3. 由於 900s 不可用對 Tair 來說影響太大,比如使用者 1 萬 QPS,那麼 900s 就涉及約千萬 QPS,因此我們率先來推動這個問題的解決。

2.1 為什麼 Jedis 和 Redisson 客戶端沒有問題?

Jedis 是連線池模式,底層超時之後,會銷燬當前連線,下一次重新建連,就會連線到新的切換節點上去並恢復。


Jedis連線池模式













try {    jedis = jedisPool.getResource(); // 查詢前獲取一個連線    // jedis.xxx // 執行操作查詢} catch (Exception e) {    e.printStackTrace(); // 超時,命令錯誤等情況} finally {    if (jedis != null) {        // 這裡的 close,如果連線正常,就返回連線池        // 如果連線異常,則會銷燬這條連線        jedis.close();    }}


Redisson 本身支援了間隔發 ping 給服務端判活,如果不通則發起重連。


Redisson 的 PingConnectionInterval 引數

// PingConnectionInterval: 間隔多少 ms 給服務端發 PING 包,在本連線上,如果不通則重連,預設 30000config.useSingleServer().setAddress(uri).setPingConnectionInterval(1000);RedissonClient connect = Redisson.create(config);2.2 能否透過配置 TCP 的 KeepAlive 來保活?

結論是不行,因為 TCP Retransmission Package 的優先順序高於 KeepAlive,即如果是一個活躍連線,當此問題出現時候,會先開始 TCP Retran,具體取決於 tcp_retries23 引數(預設 15 次,需要 924.6 s)。


為什麼 Lettuce 會帶來更長的故障時間?

(圖2. 活躍連線黑洞問題流程圖)


  • T1:Client 傳送 set key value 給 Ali-LB

  • T2:Ali-LB  回覆 ok

  • T3:Client 傳送 get key 給 Ali-LB,但是此時後端發生切換,之後 Ali-LB 沒有任何 Response,客戶端表現超時

  • T4:開始第一次 tcp retran

  • T5:開始第二次 tcp retran

  • T6:此時還在 tcp retran,但是因為到達 Ali-LB est_timeout 時間,因此 Ali-LB 回覆了 RST 回來,客戶端就會恢復了。那如果 Ali-LB 一直不回覆 RST,重傳結束之後,TCP 也是會主動斷開重連的,也可以恢復。


所以說,如果客戶端側想解決這個問題,依靠 TCP KeepAlive 是無法完成的,也可以參考知乎此問題《TCP中已有SO_KEEPALIVE選項,為什麼還要在應用層加入心跳包機制》4 ,而 Lettuce 在 6.1.05 版本開始支援了設定 KeepAlive 的選項,但如此前分析,這並不能解決活躍連線的問題。因此我們給 Lettuce 提了一個詳細的 issue6 ,來描述問題、復現方法、原因,可能的修復方法,作者也認同了問題。



03



問題解決

3.1 緊急止血

  1. 由於沒有別的有效方法,只能先將 est_timeout 調整到 120s (不能再小,否則會斷開正常靜默連線),這意味著使用者最多受損 135s(120s + 15s 探測,注意:不可用之後還要探測完才能發起切換)

  2. 官網文件不推薦使用者使用 Lettuce。

為什麼 Lettuce 會帶來更長的故障時間?

3.2 客戶端側修復

嘗試一:為 Lettuce 新增 PingConnectionInterval

上述分析我們提到,如果客戶端側想解決這個問題,需要實現應用層的判活機制,簡而言之就是客戶端會在和服務端的連線上間接的插入判活資料包,注意,這裡使用的連線必須是客戶端和服務端已有連線,而不能是一個單獨的新連線,否則會誤判,因為問題是針對連線維度的黑洞,如果使用新連線判斷,那麼服務端會返回正常的結果。


提交了 commit7 之後,作者對這個方案並不是非常認同,他認為:

  • 這個修復方法比較複雜。

  • 由於 Lettuce 支援 Command Listener,他認為使用者可以在 Command 超時之後自己關閉連線。

  • Redis 本身存在一些 Block 的命令,例如 xread,brpop,此時連線是被 hang 住的,探活無法進行。


交流下來,我們拒絕因為修改複雜就讓使用者透過 Command Listener 的方法來自己關閉連線,這意味著每個使用者為了安全使用 Lettuce 都要改程式碼,成本將會非常高,但是 block 的命令透過此方案無法解決的問題也確實存在,因此暫時被擱置。


嘗試二:使用 TCP_USER_TIMEOUT

TCP USER TIMEOUT 是RFC 54288 規定的 TCP option,用來擴充套件 TCP RFC 7939 協議中本身的 "User Timeout" 引數(原協議不允許配置引數大小)。其用來控制已經傳送,但是尚未被 ACK的資料包的存活時間,超過這個時間則會強制關閉連線。用它可以解決上述 KeepAlive 無法解決的 Retran 優先順序高的問題,下面是 KeepAlive 和 Retran 以及 TCP USER TIMEOUT 一起工作的情況。


為什麼 Lettuce 會帶來更長的故障時間?


確認 TCP_USER_TIMEOUT 可以解決此問題後,和作者再次溝通,作者也同意了此修復訪問,我們提交了 PR10,並最終被合併,之後也驗證了修復的效果,符合預期。使用下述版本可以解決黑洞問題,但需要依賴netty-transport-native-epoll:4.1.65.Final:linux-x86_64,在 EPOLL 可用時,用下面程式碼開啟,tcpUserTimeout 可結合業務具體情況配置,建議 30s。


開啟 TCP_USER_TIMEOUT


bootstrap.option(EpollChannelOption.TCP_USER_TIMEOUT, tcpUserTimeout);

Lettuce 修復版本的SNAPSHOT版本<dependency>    <groupId>io.lettuce</groupId>    <artifactId>lettuce-core</artifactId>    <version>6.3.0.BUILD-SNAPSHOT</version></dependency><dependency>    <groupId>io.netty</groupId>    <artifactId>netty-transport-native-epoll</artifactId>    <version>4.1.65.Final</version>    <classifier>linux-x86_64</classifier></dependency>3.3 Ali-LB 的修復方案

Ali-LB 側針對此問題,推出了 Connection Draining 功能,Connection Draing 意為連線排空,為了做優雅關閉使用。


優雅關閉意味著通常後端伺服器可用,如下圖一個 Ali-LB 後面掛有 4 個 Server,執行縮容操作移除 Server4,對於即將要發給這個 Server 的 Request 4 和 6(同連線上),在 draining 配置的時間內(0-900s),Server4 還是會對 Request 做出響應,等到 draining 時間到達之後才斷開連線,注意:draining 之後,新的連結就不會再排程給 Server4 了,因此後續的7,8,9等請求都不會再發給 Server4 了,這也是能排空的前提。


因此一旦開啟 draining,則在最遲到達 draining 時間之後,客戶端就會收到 Ali-LB 的 RST 了。


為什麼 Lettuce 會帶來更長的故障時間?

(圖 3. Connection Draining 示意圖)


對比 est_timeout 機制,Connection Draining 的優勢是減少了誤判,盡最大能力交付。

為什麼 Lettuce 會帶來更長的故障時間?

(表1. est_timeout 對比 connection draining)


Ali-LB 團隊上線 Connection Draining 之後,我們配合驗證,可以將故障時間從 120s 縮短至 30s 內,符合 Redis 產品的 SLA,目前已經全網釋出完成,這也解決其餘 Redis 收斂連線 SDK,和整個資料庫產品的連線黑洞問題。



04



總結

本文詳述了 Lettuce 客戶端黑洞問題的原理和解決方案:

  1. 從客戶端側:可以升級 Lettuce 最新的 6.3.0 版本,並開啟 TCP_USER_TIMEOUT 引數。在阿里雲上,無需修改程式碼,Ali-LB 的 Connection Draining 將會主動避免此問題,(無需使用者升級,阿里雲會主動逐步變更)

  2. 一個應用廣泛的軟體包的惡性 Bug 傷害巨大。比如這次 Lettuce,本來屬於 Spring Boot 中最常用的 Redis SDK,由於作者的矯情也好,較真也好(見6導致數年中諸多雲上使用者出現大量惡性故障,我們在推動中既看到如 Azure、AWS 和華為在諮詢和推動,也看到無數期待 Fix 的開發者。Redis 和 Tair 也要加大在社群 SDK 的投入,尤其是自研,自主自控的 SDK 尤為重要。

此問題從發現,到修復歷時約 2 年,終於被解決,道阻且長,行則將至!

參考閱讀

[01] 

[02] 

[03] tcp_retries2

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

[04] 《TCP中已有SO_KEEPALIVE選項,為什麼還要在應用層加入心跳包機制?》

[05] /issues/1437

[06] /issues/2082

[07] 

[08] TCP_USER_TIMEOUT

[09] 

[10] /pull/2499


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2986573/,如需轉載,請註明出處,否則將追究法律責任。

相關文章