記一次Redis數據庫配置導致的連接數泄露的問題

問題背景

去年聖誕節當天,突然收到一個我經手過的項目的告警郵件,錯誤消息顯示“Redis::CommandError: ERR max number of clients reached”
Redis 連接數告警

什麼情況?難道這個項目翻車了?第一反應是這臺服務器運行着自建的 Redis 數據庫,但是客戶端只有同個內網的一個 Ruby on Rails 的應用,怎麼會有連接數爆掉的可能?

理論連接數計算

老衲掐指一算:

  1. sidekiq 客戶端所需連接數: 對面 Rails 應用有 10 個 Unicorn 工作進程,每個unicorn進程初始化一個 sidekiq 客戶端,一個 sidekiq 客戶端默認連接池大小是 5,而且是懶惰策略,按需連接的,最大值是 10 x 5 = 50;
  2. 顯式 Redis 連接: 程序代碼裏有一個 $redis 全局變量,初始化了一個 redis 連接,10個工作進程,也就是 10 個連接;
  3. sidekiq 服務端所需連接數: sidekiq server 端 concurrency 配置是 10,那麼按照官方文檔,另有加上 2 個連接,也就是12個連接;
  4. Rails cache 所需連接數: 按照redis-store gem 源碼,默認連接池大小應該是 5,10個 unicorn 工作進程,按需連接,最大值是 10 x 5 = 50。

在不考慮其他可能還用到 Redis 連接的情況下,目前已知的最大 Redis 連接數需求是 122,這個數遠小於 Redis 理論最大連接數啊,而且當時顯示連接數到達上萬!而且這個項目已經很少訪問,壓力極其小,不大可能會達到理論所需連接數啊!

一定是有某種神祕力量在主導這一切!!!

監控與分析

以上理論最大連接數分析只是定性分析,只能大概說明有一些詭異的東西存在,而想真正確認問題根源,還得做定量分析,只有數據才能說明一切!

初步觀察:Redis 數據庫服務器端監控

事不宜遲,要採集數據,第一步就是加監控,所以當時就緊急寫了一個定時採集 Redis 客戶端數量(使用 redis 內建 CLIENT LIST 命令)的腳本,結合 crontab 定時運行,將結果寫入文件,作爲後續分析的基礎。
監控腳本

通過監控腳本,發現幾個有意思的現象:

  1. 從 Redis 數據庫服務端採集的數據看,一直只有來自一臺內網機器,也就是我前面說的 Rails 程序所在的服務器的連接,說明這個 Redis 數據庫不存在共享給其他應用的可能性;
  2. 經過3天左右的監控,即從12.25到12.28,連續3天,Redis 連接數一直穩步上升,平均每日增加 70-80。在典型的系統資源泄露類(比如內存泄露)問題的場景中,這樣的線條看起來特別熟悉,所以,真的是連接數泄露了?

連接數數量穩步攀升

進一步分析:Redis 數據庫服務器端與客戶端連接數對比分析

在有了上一步的發現之後,我繼續用系統命令 sudo netstat -apnt 檢查 6379 端口連接數發現,客戶端機器也才只有 42 個左右的連接到 redis 服務器端,結合最開始的理論連接數分析,這個數量是比較合理的。

但是!但是!反過來去服務端機器用同樣的命令檢查,在服務端視角,卻有多達300+個客戶端建立的連接,而且都是在 ESTABLISHED 狀態!這個數量和上面另一種監控方式得到的數量一致!
服務器端與客戶端 TCP 連接數不匹配

到底是什麼情況?還能有這種操作?
服務器端與客戶端誰真誰假

問題根源

至此,Redis 連接數泄露是板上釘釘的事情了,可是又是爲什麼呢?爲此,我在網上搜索了很多問答跟文章,後來總算找到了答案,果不其然,還是默認配置的問題。

Redis 默認配置

redis 爲了避免客戶端連接數過多,有一個timeout配置,意思是如果連接的空閒時間超過了timeout的值,則關閉連接。默認配置是0,意思是沒有超時限制,永遠不關閉連接。生產上顯然不會配置0……
redis timeout配置解釋

OMG!趕緊打開我們的 redis 的配置文件驗證是否如此,果不其然,redis一直保持着默認配置!
redis timeout 默認配置

至此,很好解釋爲什麼連接數會泄露了,因爲有很多空閒或者實際上客戶端已經斷開的連接,在服務器端一側仍然保持着。那什麼情況會導致這樣的情況發生呢?

我猜測:

  1. 網絡通信差: 按照 TCP 協議,客戶端斷開連接時,向服務器端發送 FIN 信號,但是服務端未接收到,客戶端超時後放棄等待,直接斷開,服務端由於通信故障,保持了 ESTABLISHED 狀態,不過由於兩端機器在同個內網,網絡質量沒有理由不行;
  2. 客戶端異常: 客戶端連接之後,由於代碼運行過程中產生異常,導致未正常釋放或者關閉連接,sidekiq 的worker很可能就有這類問題。這個的可能性非常大,畢竟我日常寫 bug (/ω╲)。

問題修復

找到問題根源之後,修復起來就簡直太簡單了。事實上,開發領域就是如此,絕大部分時間都花在了找 bug 上,而改掉bug,可能只需要一分鐘不到。

首先,修改了下 redis 數據庫配置:
redis 修改成建議配置

成功重啓 redis 之後,重新運行前面的監控腳本,以便觀察修復後情況,初步可以確認這下服務器端和客戶端的連接數一致了:

配置生效重啓後,多次重新檢查兩端看到的連接數,都一直保持一致了,說明服務端能正常釋放一些 idle 連接了

再又經過幾天的腳本自動採集數據後分析,系統又恢復平穩運行了,連接數一直穩定在理論最大連接數之下。
redis 連接數穩定,穩定在理論最大連接數之下

總結

這個問題的根源其實很小,但是排查過程還是花了挺多時間,主要是需要等待採集到足夠的數據後用於分析。其他心得體會:

  1. 保護“案發現場”很重要,要想挖掘問題根源,必須保持環境可重現,這次出現問題的時候雖然第一時間重啓了 redis 使服務恢復,但是由於沒有修改任何配置,所以使得後來的監控能夠發現問題根源;
  2. 使用開源軟件,必須對默認配置保持警惕,相信應該有人以前聽說過 redis 默認監聽0.0.0.0來源請求的安全漏洞;
  3. 這個項目由於開始較早,當時並沒有考慮使用 Redis 雲數據庫,自建數據庫有風險,需要慎重對待,儘可能的情況下,專業的事情,交給專業的人去做。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章