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

問題背景

去年聖誕節當天,忽然收到一個我經手過的項目的告警郵件,錯誤消息顯示「Redis::CommandError: ERR max number of clients reached」
Redis 鏈接數告警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 理論最大鏈接數啊,並且當時顯示鏈接數到達上萬!並且這個項目已經不多訪問,壓力極其小,不大可能會達到理論所需鏈接數啊!服務器

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

監控與分析

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

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

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

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

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

鏈接數數量穩步攀升

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

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

可是!可是!反過來去服務端機器用一樣的命令檢查,在服務端視角,卻有多達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 雲數據庫,自建數據庫有風險,須要慎重對待,儘量的狀況下,專業的事情,交給專業的人去作。
相關文章
相關標籤/搜索