如何改變Redis用不好的誤區

?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

王曉波:同程旅遊首席架構師,10餘年互聯網行業從業經驗,負責中間件、微服務、分佈式架構、運維、安全等方面的工作從實際案例看Redis的使用

在一個炎熱的夏天,引爆了埋藏已久的大炸彈。

首先是一個產品線開發人員搭建起了一套龐大的價格存儲系統,底層是關係型數據庫,只用來處理一些事務性的操作和存放一些基礎數據;

在關係型數據庫的上面還有一套MongoDB,因爲MongoDB的文檔型數據結構,讓他們用起來很順手,同時也可以支撐一定量的併發。

在大部分的情況下,一次大數據量的計算後結果可以重用但會出現細節數據的頻繁更新,所以他們又在MongoDB上搭建了一層Redis的緩存,這樣就形成了數據庫→MongoDB→Redis三級的方式,方案本身先不評價不是本文重,我們來看Redis這層的情況。

由於數據量巨大,所以需要200GB的Redis。並且在真實的調用過程中,Redis是請求量最大的點,當然如果Redis有故障時,也會有備用方案,從後面的MongoDB和數據庫中重新加載數據到Redis,就是這麼一套簡單的方案上線了。

當這個系統剛開始運行的時候,一切都還安好,只是運維同學有點傻眼了, 200GB的Redis單服務器去做,它的故障可能性太大了,所以大家建議將它分片,沒分不知道一分嚇一跳,各種類型用的太多了,特別是裏面還有一些當類似消息隊列使用的場景。

由於開發同學對Redis使用的注意點關注不夠,一味的濫用,一錘了事,所以讓事情變的困難了。有些僥倖不死的想法是會傳染,這時的每個人都心存僥倖,懶惰心裏,都想着:“這個應該沒事,以後再說吧,先做個主從,掛了就起從”,這種僥倖也是對Redis的虛僞的信心,無知者無畏。

可惜事情往往就是怕什麼來什麼,在大家快樂的放肆的使用時,系統中重要的節點MongoDB由於系統內核版本的BUG,造成整個Mongodb集羣掛了!(這裏不多說Mongodb的事情,這也是一個好玩的哭器)。

當然對天天與故障爲朋友的運維同學來說這個沒什麼,對整個系統來說問題也不大,因爲大部分請求調用都是在最上層的 Redis中完成的,只要做一定降級就行,等拉起了Mongodb集羣后自然就會好了。

但此時可別忘了那個Redis,是一個200G大的Redis,更是帶了個從機的Redis,所以這時的Redis是絕對不能出任何問題的,一旦有故障,所有請求會立即全部打向最低層的關係型數據庫,在如此大量的壓力下,數據庫瞬間就會癱瘓。

但是,怕什麼來什麼,還是出了狀況:主從Redis之間的網絡出現了一點小動盪,想想這麼大的一個東西在主從同步,一旦網絡動盪了一下下,會怎麼樣呢?主從同步失敗,同步失敗,就直接開啓全同步,於是200GB的Redis瞬間開始全同步,網卡瞬間打滿。

爲了保證Redis能夠繼續提供服務,運維同學,直接關掉從機,主從同步不存在了,流量也恢復正常。不過,主從的備份架構變成了單機Redis,心還是懸着的。俗話說,福無雙至,禍不單行。這Redis由於下層降級的原因併發操作量每秒增加到四萬多,AOF和RDB庫明顯扛不住。

同樣爲了保證能持續地提供服務,運維同學也關掉了AOF和RDB的數據持久化。連最後的保護也沒有了(其實這個保護本來也沒用,200GB的Redis恢復太大了)。

至此,這個Redis變成了完全的單機內存型,除了祈禱它不要掛,已經沒有任何方法了。懸着好久,直到修復MongoDB集羣才了事。如此的僥倖,沒出大事,但心裏會踏實嗎?不會。

在這個案例中主要的問題在於對Redis過度依賴,Redis看似爲系統帶來了簡單又方便的性能提升和穩定性,但在使用中缺乏對不同場影的數據的分離造成了一個邏輯上的單點問題。

當然這問題我們可以通過更合理的應用架構設計來解決,但是這樣解決不夠優雅也不夠徹底,也增加了應用層的架構設計的麻煩,Redis的問題就應該在基礎緩存層來解決,這樣即使還有類似的情況也沒有問題,因爲基礎緩存層已經能適應這樣的用法,也會讓應用層的設計更爲簡單(簡單其實一直是架構設計所追求的,Redis的大量隨意使用本身就是追求簡單的副產品,那我們爲不讓這簡單變爲真實呢)

再來看第二個案例。有個部門用自己現有Redis服務器做了一套日誌系統,將日誌數據先存儲到Redis裏面,再通過其他程序讀取數據並進行分析和計算,用來做數據報表。

當他們做完這個項目之後,這個日誌組件讓他們覺得用的還很過癮。他們都覺得這個做法不錯,可以輕鬆地記錄日誌,分析起來也挺快,還用什麼公司的分佈式日誌服務啊。

於是隨着時間的流逝,這個Redis上已經悄悄地掛載了數千個客戶端,每秒的併發量數萬,系統的單核CPU使用率也接近90%了,此時這個Redis已經開始不堪重負。

終於,壓死駱駝的最後一根稻草來了,有程序向這個日誌組件寫入了一條7MB的日誌(哈哈,這個容量可以寫一部小說了,這是什麼日誌啊),於是Redis堵死了,一旦堵死,數千個客戶端就全部無法連接,所有日誌記錄的操作全部失敗。

其實日誌記錄失敗本身應該不至於影響正常業務,但是由於這個日誌服務不是公司標準的分佈式日誌服務,所以關注的人很少,最開始寫它的開發同學也不知道會有這麼大的使用量,運維同學更不知有這個非法的日誌服務存在。

這個服務本身也沒有很好地設計容錯,所以在日誌記錄的地方就直接拋出異常,結果全公司相當一部分的業務系統都出現了故障,監控系統中“5XX”的錯誤直線上升。

一幫人欲哭無淚,頂着巨大的壓力排查問題,但是由於受災面實在太廣,排障的壓力是可以想像的。 這個案裏中看似是一個日誌服務沒做好或者是開發流程管理不到位。而且很多日誌服務也都用到了Redis做收集數據的緩衝,好像也沒什麼問題。

其實不然,像這樣大規模大流量的日誌系統從收集到分析要細細考慮的技術點是巨大的,而不只是簡單的寫入性能的問題。在這個案例中Redis給程序帶來的是超簡單的性能解決方案,但這個簡單是相對的,它是有場景限制的。在這裏這樣的簡單就是毒藥,無知的吃下是要害死自己的,這就像“一條在小河溝裏無所不能傲慢的小魚,那是因爲它沒見過大海,等到了大海……”。

在這個案例的另一問題:一個非法日誌服務的存在,表面上是管理問題,實質上還是技術問題,因爲Redis的使用無法像關係型數據庫那樣有DBA的監管,它的運維者無法管理和提前知道里面放的是什麼數據,開發者也無需任何申明就可以向Redis中寫入數據並使用,所以這裏我們發現Redis的使用沒這些場景的管理後在長期的使用中比較容易失控,我們需要一個對Redis使用可治理和管控的透明層。

兩個小例子中看到在Redis亂用的那個年代裏,使用他的兄弟們一定是痛的,承受了各種故障的狂轟濫炸:

• Redis被keys命令堵塞了;

• Keepalived切換虛IP失敗,虛IP被釋放了;

• 用Redis做計算了,Redis的CPU佔用率成了100%了;

• 主從同步失敗了;

• Redis客戶端連接數爆了;

• ……


如何改變Redis用不好的誤區

這樣的亂象一定是不可能繼續了,最少在同程這樣的使用方式不可以再繼續了,使用者也開始從喜歡到痛苦了。

怎麼辦?這是一個很沉重的事:“一個被人用亂的系統就像一桌燒壞的菜,讓你重新回爐,還讓人叫好,是很困難的”。關鍵是已經用的這樣了,總不可能讓所有系統都停下來,等待新系統上線並瞬間切換好吧?這是個什麼活:“高速公路上換輪胎”。

但問題出現了總是要解決的,想了再想,論了再論,總結以下幾點:

1)必須搭建完善的監控系統,在這之前要先預警,不能等到發生了,我們才發現問題;

(2)控制和引導Redis的使用,我們需要有自己研發的Redis客戶端,在使用時就開始控制和引導;

(3)Redis的部分角色要改,將Redis由storage角色降低爲cache角色;

(4)Redis的持久化方案要重新做,需要自己研發一個基於Redis協議的持久化方案讓使用者可以把Redis當DB用;

(5)Redis的高可用要按照場景分開,根據不同的場景決定採用不同的高可用方案。

留給開發同學的時間並不多,只有兩個月的時間來完成這些事情。這事其實還是很有挑戰的,考驗開發同學這個輪胎到底能不換下來的時候到來了。同學們開始研發我們自己的Redis緩存系統,下面我們來看一下這個代號爲鳳凰的緩存系統第一版方案:

首先是監控系統。原有的開源Redis監控從大面上講只一些監控工具,不能算作一個完整的監控系統。當然這個監控是全方位從客戶端開始一直到反回數據的全鏈路的監控。

其次是改造Redis客戶端。廣泛使用的Redis客戶端有的太簡單有的太重,總之不是我們想要東西,比如,.Net下的BookSleeve和servicestack.Redis(同程還有一點老的.Net開發的應用),前者已經好久沒人維護了,後者直接收費了。

好吧,我們就開發一個客戶端,然後督促全公司的研發用它來替換目前正在使用的客戶端。在這個客戶端裏面,我們植入了日誌記錄,記錄了代碼對Redis的所有操作事件,例如耗時、key、value大小、網絡斷開等,我們將這些有問題的事件在後臺進行收集,由一個收集程序進行分析和處理,同時取消了直接的IP端口連接方式,通過一個配置中心分配IP地址和端口。

當Redis發生問題並需要切換時,直接在配置中心修改,由配置中心推送新的配置到客戶端,這樣就免去了Redis切換時需要業務員修改配置文件的麻煩。

另外,把Redis的命令操作分拆成兩部分:安全的命令和不安全的命令。對於安全的命令可以直接使用,對於不安全的命令需要分析和審批後才能打開,這也是由配置中心控制的,這樣就解決了研發人員使用Redis時的規範問題,並且將Redis定位爲緩存角色,除非有特殊需求,否則一律以緩存角色對待。

最後,對Redis的部署方式也進行了修改,以前是Keepalived的方式,現在換成了主從+哨兵的模式。另外,我們自己實現了Redis的分片,如果業務需要申請大容量的Redis數據庫,就會把Redis拆分成多片,通過Hash算法均衡每片的大小,這樣的分片對應用層也是無感知的。

當然重客戶端方式不好,並且我們要做的是緩存不僅僅是單單的Redis,於是我們會做一個Redis的Proxy,提供統一的入口點,Proxy可以多份部署,客戶端無論連接的是哪個Proxy,都能取得完整的集羣數據,這樣就基本完成了按場景選擇不同的部署方式的問題。

這樣的一個Proxy也解決了多種開發語言的問題,例如,運維繫統是使用Python開發的,也需要用到Redis,就可以直接連Proxy,然後接入到統一的Redis體系中來。做客戶端也好,做Proxy也好,不只是爲代理請求而是爲了統一的治理Redis緩存的使用,不讓亂象的出現。

讓緩存有一個可管可控的場景下穩定的運維,讓開發者可以安全並肆無忌憚繼續亂用Redis,但這個“亂”是被虛擬化的亂,因爲它的底層是可以治理的。

?wx_fmt=png                    圖15-1 系統架構圖

當然以上這些改造都需要在不影響業務的情況下進行。實現這個其實還是有不小的挑戰,特別是分片,將一個Redis拆分成多個,還能讓客戶端正確找到所需要的key,這需要非常小心,因爲稍有不慎,內存的數據就全部消失了。在這段時間裏,我們開發了多種同步工具,幾乎把Redis的主從協議整個實現了一遍,終於可以將Redis平滑過渡到新的模式上了。

發佈了394 篇原創文章 · 獲贊 68 · 訪問量 82萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章