1. 程式人生 > >全球領先的redis客戶端:SFedis

全球領先的redis客戶端:SFedis

自帶 修改 標準 做了 red 崗位 and 穿透 監控

零、背景

  這個客戶端起源於我們一個系統的生產問題。

一、問題的發生

  在我們的生產環境上發生了兩次redis服務端連接數達到上限(我們配置的單節點連接數上限為8000)導致無法創建連接的情況。由於這個系統生產環境的redis集群的tps達到百萬級,所以發生了這個情況的後果是非常嚴重的,有的業務會發生緩存穿透的情況,有的業務會直接報錯。

二、問題分析

  在生產環境上每個redis節點的tps上限在50000左右,我們監控redis的slowlog的閥值設置為0.1ms,也就是說如果服務端慢到10000tps時就會觸發報警,但在問題發生當時並沒有報警。實際上這是我們的一個失誤:如果redis一個服務節點是獨享一個cpu核的,那麽按照redis的機制是可以推測出slowlog是不可能會有“慢”的結果的。那麽如果慢一定不是在redis本身的處理上,有可能是塞在epoll上或者網絡上。但我們並沒有發現有任何地方有異常(包括網絡)。

  我們並沒有查到故障發生在哪裏,但故障的確就發生了,這是很離奇的。

  最後我們只能進行了推測:正常情況下整個集群的速度是非常快的。監控設置的0.1ms的閥值雖然看起來是非常快(萬分之一秒),但和正常情況下的平均響應時長來說還是慢了5倍的差距。也就是說,我們檢測每一個地方都沒有看到問題,可能只是因為檢測的標準以及檢測工具的能力(精確度)的問題。比如說:平時單節點平均處理能力在0.02ms每個命令,但當慢(無論慢在哪裏)到0.05ms的時候我們是沒能監控出來的,而實際上這個時候問題已經發生了。假設網絡因未知原因卡了一秒鐘,那麽就會有幾十萬到一百萬個請求塞在網絡上,客戶端因請求還沒有返回,新的請求就會向連接池申請新的連接,如果服務端沒有保留足夠的buffer來處理瞬間多出來的請求,那麽很有可能在這個時候發生一個雪崩效應——連接數瞬間達到上限。

三、臨時解決

  當時在故障處理時,我們采取了比較粗爆但有效的辦法:減少客戶端的數量。我們停掉了相關服務的一半節點,使所有運行節點的線程池即使全部打滿也不會達到redis服務端的上限,這樣當業務消費一段時間後,請求降下來了,再啟動被關掉的服務。

  當天晚上我們對redis集群進行了擴容,保留了更大的buffer,使應對異常沖擊的能力提高一些。

  這些只是臨時的解決方案,治標不治本的。所以還是需要更進一步研究更好的解決方案。

  這裏需要說明一點:為什麽服務可以停掉一半?如果服務停掉了一半,前端的請求會不會把服務的cpu打滿,導致服務掛掉呢?

  這裏是因為:

  1.服務端對所有的rest/http接口以及rpc接口都做了隔離限流,每任何一個接口超過一定的並發之後,後面的請求就會馬上報錯,保證服務的安全。

  2.用戶端是移動App,在移動端我們對所有重要業務做了統一的重試機制,如果沒有傳上來的,可以在一定時間之後再次重試。

  所以這裏服務端減少服務能力的情況下,並不會導致嚴重的業務問題,但是會使業務數據上傳變慢一些。

四、原理分析

  當時我們的客戶端用的是jedis,連接的管理用的是jedis自帶的。

  因為redis服務端的每個節點的數據是不同的,所以在長時間的調用下,每個客戶端一定會訪問到每個服務端節點。這樣的話,服務端每個節點的連接數就並不取決於服務端集群的大小,而取決於客戶端集群的大小。

  如下圖所示:如果客戶端有2個,每個客戶端的連接池上限是40個連接,那麽無論服務端是多少個節點,每個節點的連接數量的上限應該是40*2=80個。

  技術分享

  那麽問題來了:服務端的每個節點處理能力是有限的,連接數過多是沒有意義的,如果每個服務端的連接上限是10000個,每個客戶端的連接池上限是100個,那麽在理論上要保證連接安全,客戶端的節點數上限是10000/100=100個。但如果我們需要更大的業務處理能力(業務應用集群的節點數需要超過100個)的情況下,怎麽辦呢?

五、一個想法

  從理論上說,1個連接是可以達到一個網卡的帶寬極限的,那麽是否有可能做到每個redis客戶端只有一個連接,卻可以達到原來n多個連接一樣的性能(甚至更好)呢?

六、研究業界現有方案

  帶著問題,我們用了兩個月時間來研究測試各種業界公認的成熟方案(除了當時正在用的jedis客戶端之外,還研究了twemproxy、Codis、redis 4.0 (cluster)、redisson),發現這些方案並沒有讓我們滿意。下面說一下我們為什麽不選擇這些方案:

  twemproxy:代理並不能完全解決連接數的問題,它只能讓連接數少一些,而且代理大約有20%的性能損耗。

  Codis:1.代理和twemproxy的差不多,也不能完全解決連接數的問題;2.Codis新版本沒有節點失效的檢測的能力;3.整個方案的部署比較麻煩。4.在增加節點時,集群會自動遷移數據(當然,這個不能說是缺點,但如果整個集群的內存達到幾個T的情況下,內存的數據遷移會有什麽後果不好預料(遷移數據導致網絡塞住怎麽辦?遷移數據時服務會中段多長時間?))。

  Redis cluster:1.必須做主備,當主備都掛了的情況下,不能自動摘除節點;2.在增加節點時,集群會自動遷移數據——這一點和Codis一樣——我們寧可緩存穿透,也不希望他遷移數據(如果實現了一致性hash,那麽會穿透的數據還是很少的——比如:如果我們服務已經有了100個節點,再加一個節點最多只會導致1%的數據失效);

  redisson:這個客戶端用了nio機制,在異步操作的情況下的確會大大減少連接數,並且異步的性能非常好(極端的情況下,有可能是jedis的十倍)。但在同步的情況下就沒那麽樂觀,還是需要多個連接才能勉強追得上jedis的速度。如果我們改用redisson的異步形式,則需要改業務代碼,這是很難接受的——不過這裏我認為是redisson的開發者們對代碼的優化沒有做到極致,因為在基礎原理上nio可以達到的程度絕對可以比現在的redisson更好。

  另外,如果采用短連接的形式的話,對性能的影響比較大,所以我們也不想犧牲長連接的優勢。

  既然找不到已經實現好的成熟方案,那麽我們是否可以自己實現一個呢?

七、自己開發

  目標很清晰:一個“新的jedis”,但每個客戶端在連接每個服務節點時只連一個連接,最重要的是性能絕不可以比jedis差。

  雖然目標很清晰,並且在基礎原理上是可以達到的,但具體的技術細節確並不容易。目標是我定的,但我給不出在技術細節上的實現方案,後來我們部門內的一個碼神想到了一個很好的實現方案。

  具體原理是這樣的:

  1.redis的通信協議是tcp,這就提供了異步請求的基礎——如果是同步的網絡請求,客戶端就需要等待服務端的響應,那麽在等待這段時間裏,帶寬是空著的,這樣要打滿帶寬就必然需要多個連接,所以,如果我們需要用一個連接打滿帶寬就必然需要用異步。

  2.redis的命令協議上是沒有在發送與接收之間建立對應關系的(沒有msg_id之類的屬性),這如果不停的發送與接收命令,應該如何告訴業務哪個接收到的數據屬於應用事例的哪個線程呢?這裏我們找到了一個很巧妙的對應關系:順序。redis服務端是單線程的,那麽服務端先接收到的命令必然先返回,同時,tcp協議又是保證順序的,這就決定了我們可以用“順序”做為“發”與“收”之間的對應協議。

  3.為了不修改業務,我們必須用“新的原理”來實現“老的接口”,老接口都是同步操作的,那麽這裏的阻塞動作就一定要在客戶端框架中來實現了。這裏就要用到Future了。

  技術分享

  最終的實現結果是:我們自己實現的新redis客戶端框架SFedis訪問每個服務節點只用1個連接,卻比業界廣泛使用的Jedis用多個連接還要快一點。

  我們現在還沒有實現異步接口,如果我們真的實現了異步接口,那麽估計比redisson還要快。

  另:在十一月的新書《決戰618》我看到書中有寫到京東也有用nio實現自己的redis客戶端來解決連接數的問題,不過書中只有一句話講這個,完全沒有任何細節。

八、結果展示

  我們有兩個服務共用一個redis集群,下圖是其中一個服務上線後的連接數監控圖:

  技術分享

  這裏可以看到:一個服務上線之後的幾天比上線前的幾天,redis連接數直接腰斬了。

  下圖是另一個服務也上線之後的連接數監控圖:

  技術分享

  可以看到在第二個服務上線之後,連接數已經完全不再波動了(這裏千萬別誤會:後面三天的線是平的,不代表沒有服務。服務是正常運行的,而且運行得很健康),這裏連接數停留在應用實例的個數上(58個)。

  這裏聲明一下:這個系統是有做灰度的,在生產上有多個環境在跑不同的版本,上面的兩個截圖是一個小環境上線前後的監控情況,所以節點數比較少,只有58個。而且這個小環境在性能上留的buffer是比較充足的,所以平時的redis連接數也不高。在大的生產環境上這個圖會顯得更猛一些。

九、開源計劃

  目前這個客戶端還沒有開源,但開源已在計劃之中。後續開源之後會公布出來。

十、人員招募

  我們團隊正在招人,崗位有:Android開發、Java後臺開發、架構師、測試。歡迎大家推薦或自薦!

  簡歷請發我郵箱:[email protected]

全球領先的redis客戶端:SFedis