Redis 橫向擴展案例
文/溫國兵
0x00 目錄
- 0x00 目錄
- 0x01 背景介紹
- 0x02 分析解決
- 2.1 初步分析
- 2.2 使用 Twemproxy 橫向擴展
- 2.3 問題解決
- 0x03 原理探討
- 0x04 案例小結
0x01 背景介紹
A 項目采集其它項目生成的數據,數據保存一定時間,并且不需要持久化。故 A 項目使用單點 Redis 做緩存。長期以來,該 Redis 實例在高峰期間的 QPS 高達 100K,甚至一度達到 120K。某天晚上,終于崩潰了。這也印證了「 墨菲定律 」,事情如果有變壞的可能,不管這種可能性有多小,它總會發生(Anything that can go wrong will go wrong)。所以,平時的運維過程,千萬不要抱有僥幸心理,有問題就第一時間反應,有隱患就及時處理。
0x02 分析解決
2.1 初步分析
單點 Redis 實例崩潰的現象就是新的連接不能建立,超時嚴重,數據不能及時讀取。查看系統日志,發現有大量形如「kernel: Possible SYN flooding on port xxxx. Sending cookies」的日志。我們很快排查了系統遭受攻擊的可能性。查看端口占用情況,確實是被 Redis 消耗了。分析監控數據和端口數情況,此時的 Redis 連接數達到了 7K,QPS 已經達到 100K。現在亟待解決的問題就是連接數高的問題。另外,針對 QPS 過高的問題,確認是否可以使用 Redis 管道技術。
經過和研發溝通,得知程序采用 Nginx Lua 實現。Lua 是一個簡潔、輕量、可擴展的腳本語言,也是號稱性能最高的腳本語言。使用 Nginx Lua,再加上 LuaRedisModule 模塊,就可以原生地和 Redis 建立請求,并解析響應。但真實的項目中采用的是 lua-resty-redis,這是一個為 ngx_lua 設計的,基于 cosocket API 的 Redis Lua 客戶端。
程序中使用了如下設置:
set_keepalive(5000, 20)
其中第一個參數表示 max_idle_timeout,第二個參數表示 pool_size。這個方法是為每個 Nginx 工作進程設置的。也就是說,最終建立的連接計算公式如下:
connectionNum(連接數) = machineNum(機器數) * nginxWokerProcess(每臺機器的 Nginx 工作進程數) * pool_size(連接池大小)
前端有四臺 Web 服務器,每臺機器 18 個 Nginx 工作進程,按照上面的設置,那和單點 Redis 建立的連接數為 4*18*20,也就是 1440。然而,真實的連接已經達到 7K,看來問題不在這里。
我們嘗試使用 Redis Pipeline,至于原因,且聽我慢慢道來。
Redis 是一種基于客戶端/服務端模型以及請求/響應協議的 TCP 服務。這意味著通常情況下一個請求會遵循以下步驟:
- 客戶端向服務端發送一個查詢請求,并監聽 Socket 返回,通常是以阻塞模式,等待服務端響應;
- 服務端處理命令,并將結果返回給客戶端。
Redis 管道可以在服務端未響應時,客戶端可以繼續向服務端發送請求,并最終一次性讀取所有服務端的響應。可以簡單地理解為批量操作,一次返回。
然而,真實場景中,絕大多多數的命令是 INCR,也就是 +1 的操作。這類操作使用管道的意義不是太大,于是放棄了。
2.2 使用 Twemproxy 橫向擴展
我們嘗試進行 Scale Out,增加 Redis 實例,并且使用 Twemproxy 代理,每臺 Web 服務器訪問本地的 Twemproxy。
在此不妨簡單介紹下 Twemproxy。為了滿足數據的日益增長和擴展性,數據存儲系統一般都需要進行一定的分片。分片主要存在三個位置,第一層,數據存儲系統本身支持;第 二層,服務器端和客戶端中間建代理支持;第三層,客戶端支持。Redis Cluster 屬于第一層,Twemproxy 屬于第二層,Memcached 屬于第三層。Twemproxy(又稱為 nutcracker)是一個輕量級的 Redis 和 Memcached 代理,主要用于分片。Twemproxy 由 推ter 開源出來的緩存服務器集群管理工具,主要用來彌補 Redis 和 Memcached 對集群 (Cluster) 管理的不足。Twemproxy 按照一定的路由規則,轉發給后臺的各個 Redis 實例,再原路返回。有了 Twemproxy,前端不再關心后端代理了多少 Redis 實例,而只需訪問 Twemproxy 即可,一方面簡化了開發難度,另一方面提高了性能。Twemproxy 支持大部分命令,但對多鍵命令的支持有限,并且會有 20% 左右的性能損失(推ter 官方測試結果)。簡單來說,Twemproxy 就是一個支持 Redis 協議,對前端透明,支持分片,性能優秀的代理。當然,目前 Redis 3.0 已經發布,不少廠商會選擇使用 Redis 3.0 代替 Twemproxy,這需要時間。
我們在獨立服務器新增了 4 個實例,并且 Web 服務器部署 Twemproxy,應用訪問本地的 Twemproxy。但實際的效果并不理想,Redis QPS 依然高,本地的 Twemproxy 居然讓 Web 服務器性能惡化。另外一個有趣的現象是,Twemproxy 代理的幾個 Redis 實例存在嚴重的數據傾斜。有些 Redis 實例 QPS 可以達到 80K,有些 Redis 只有 5K 左右。
那目前我們的問題主要集中在兩個問題上,第一,連接數過高;第二,數據傾斜。經過研發排查,找出一段令人匪夷所思的代碼。Nginx Lua 中的超時時間是 60s,這也解釋了為什么實際連接跟理論連接相差如此巨大。
2.3 問題解決
根據以上的分析,我們決定分兩步走。第一,更改超時時間;第二,解決數據傾斜的問題。
研發把超時改為 2s,并且根據實際情況更改了連接池,觀察效果。可以明顯地看到,連接數降到 1K 左右,機器 Socket 使用數下降顯著,連接沒有阻塞,業務沒有較大的波動。
解決了連接數的問題,我們接下來解決數據傾斜的問題。
如前所述,Scale Out 后,Redis 實例存在嚴重的數據傾斜。有些 Redis 實例 QPS 可以達到 80K,有些 Redis 只有 5K 左右。分析這個問題,這就要從業務數據形態入手。這個業務是統計業務,由大量的 INCR 操作,并且產生的 Key 較少。Twemproxy 根據配置的一致性 Hash 函數,對 Key 進行 Hash 校驗,再決定轉發到對應的 Redis 實例。
根據以上分析,產生的 Key 較少,也就是重復率較高,導致轉發的 Redis 實例就會集中。這也解釋了為什么會產生數據傾斜。針對這個問題,我們展開討論。最開始打算寫本地文件,然后定時寫入 Redis,這樣 Redis 的 QPS 會下降不少。但考慮到定時器實現較復雜,于是采取了拆分 Key 的辦法。舉個例子,比如之前是一分鐘一個 Key,那現在 1 分鐘產生 10+ Key,甚至更多,那這樣數據傾斜的問題自然會慢慢減緩,直至消除。
經過研發的艱苦奮斗,把 Key 拆分后,效果明顯。QPS 分攤到各個 Redis 實例,連接數下降,Web 服務器性能提高。
0x03 原理探討
在原理探討這一小結,筆者只針對 Twemproxy 一致性 Hash 函數進行淺薄地分析。
Twemproxy 提供取模,一致性哈希等手段進行數據分片,維護和后端 Server 的長連接,自動踢除 Server、恢復 Server,提供專門的狀態監控端口供外部工具獲取狀態監控信息。Twemproxy 使用的是單進程單線程來處理請求,只是另外起了一個線程來處理統計數據。
Twemproxy 的代碼組成如下:事件處理、多種 Hash 函數、協議、自定義的數據類型、網絡通信、信號處理、數據結構和算法、統計日志和工具、配置文件和主程序。
第二小結有提到 Twemproxy 的 一致性Hash 函數。一致性 Hash 函數有:one_at_a_time、md5、crc16、crc32、crc32a、fnv1_64、fnv1a_64、fnv1_32、 fnv1a_32、hsieh、murmur 和 jenkins。Key 的分發模式有:ketama、modula 和 random。線上業務配置的 Hash 函數是fnv1a_64,分發模式為ketama。
fnv1a_64 Hash 算法的實現,我們可以用如下 Python 代碼(來自 ning )模擬:
def hash_fnv1a_64(s): UINT32_MAX=2**32 FNV_64_INIT = 0xcbf29ce484222325 % UINT32_MAX FNV_64_PRIME = 0x100000001b3 % UINT32_MAX hval = FNV_64_INIT for c in s: hval = hval ^ ord(c) hval = (hval * FNV_64_PRIME) % UINT32_MAX return hval
Key 重復率越高,根據一致性函數處理后,轉發到相同機器的概率就會越高。
另外,ketama 分發模式的算法復雜度是 O(LogN),然而 modula 的算法復雜度是 O(1)。按照官方的示例,我們默認采用了 ketama。不過最好按照實際環境配置。
0x04 案例小結
此案例非常具有代表性。第一,排查定位問題的思路;第二,Redis 遇到瓶頸的解決思路;第三,Scale Out 的分析角度。遇到瓶頸問題,可以從如下幾個角度思考,第一,對代碼、服務器和相關服務進行優化;第二,具體產品的選型或者定制;第三,根據業務形態,對數 據產生、處理和消費流程進行梳理,梳理完成再決定或者優化架構形態;第四,進行擴展,根據業務場景決定 Scale Out 還是 Scale Up。
–EOF–
插圖來自:監控系統