Redis的KEYS命令引起RDS數據庫雪崩,RDS發生兩次宕機,造成幾百萬的資金損失

最近的互聯網線上事故發生比較頻繁,20180919順豐發生了一起線上刪庫事件,在這裏就不介紹了。

在這裏講述一下最近發生在我公司的事故,以及如何避免,並且如何處理優化。

間接原因還有很多,技術跟不上業務的發展,由每日百萬量到千萬級是一個大的跨進,公司對於系統優化的處理優先級不高,技術開發人手的短缺

第一次宕機
20180913某個點,公司某服務化項目的RDS實例連接飆升,CPU升到100%,拒絕了其他應用的所有請求服務
整個過程如下:

  1. 監控報警,顯示RDS的CPU使用率達到80%以上,DBA介入,準備KILL慢SQL

  2. 1分鐘內,沒有發現明顯阻塞的SQL,CPU持續上升到99%

3 .5分鐘內,大量應用報警,並且拒絕服務,RDS的監控顯示出現大量慢SQL,聯繫服務器數據庫提供商進行協助

  1. 8分鐘內,進行數據庫主備切換(業務會受損,但是也沒辦法,沒有定位到問題)

  2. 9分鐘內,部分業務恢復,但是一些業務訂單的回調消息堆積超過20w,備庫的CPU使用率也持續上升

  3. 15分鐘內,備庫CPU使用率超過97%,業務再次中斷,進行切回主庫,並進行限流

  4. 20分鐘內,關閉一些次要應用的流量入口

  5. 25分鐘內,主庫CPU使用率恢復正常

  6. 30分鐘內,逐步開啓關閉的限流應用

  7. 35分鐘內,所有應用恢復正常

  8. 接下來就是與服務器數據庫提供商成立應急小組緊急優化可能出現的慢SQL,雖然說可能解決了一些慢SQL,但此次並沒有定位到具體的問題,也就爲幾天後再次發生宕機事件埋下了伏筆

事故影響
某服務化項目服務不可用幾十分鐘,造成訂單數減少幾十萬筆,損失百萬資金

原因分析
當時是沒有定位到具體的原因的,但是下面的原因也是一部分可能引起宕機的情況。
某服務化項目的業務增速非常快,在高峯期,數據庫QPS突破35000,系統處於高負荷狀態。
在高峯期如果同時執行幾個全表掃描的SQL,會造成數據庫壓力急劇上升,應用超時增多,前端應用超時,用戶重試,流量飆升,形成了雪崩效應。
主要原因在與一些老項目的SQL查詢性能較差,並且使用的主庫,對數據庫影響較大。數據庫QPS太高,但是緩存方案因爲人手原因一直沒有落地,慢SQL的問題處理優先級應該提升

改進方案

  1. 針對每個應用建一個數據庫賬號,嚴格按照規範使用

  2. 緩存優化方案即時落地,慢SQL問題優先處理,集中處理目前已經發現的慢SQL(查詢時間超過1S)

  3. 升級數據庫配置

  4. 遷移非核心業務到新的RDS實例中去

第二次宕機
由於上一次的宕機原因未找到,所以此次的宕機是可以預見的
20180919,還是一樣的"配方",還是原來的"味道"。同一個RDS,CPU飆升至100%,接下來就是拒絕服務,宕機。當然,有了第一次的經驗,直接主從切換,在幾十秒左右就恢復了所有業務,但還是嚴重影響了公司的業務和形象

原因分析
恢復業務後,公司緊急召開了緊急事故研究會議,當然,我的級別是參與不了的。公司的高管,高層技術架構、DBA、各個項目的主負責人一起進行了會議。
在此次會議中,經過查看各個項目的日誌,後臺的監控數據,發現在那臺RDS數據庫CPU飆升時,有一臺Redis數據庫內存將近100%,然後急劇下降。聯繫第一次的宕機情況,也是類似的。
接下來就是聯繫服務器數據庫提供商,將那臺Redis最近一週的命令全部調用出來,最後發現,在那個時間點運行了一條keys ...命令。公司的一個工程師執行keys模糊的匹配命令是爲了清理沒用的鍵,但是沒有考慮到keys *進行模糊匹配引發Redis鎖,造成Redis鎖住,CPU飆升,引起了所有調用鏈路的超時並且卡住,等Redis鎖的那幾秒結束,所有的請求流量全部請求到RDS數據庫中,使數據庫產生了雪崩,使數據庫宕機。

改進方案

  1. 所有線上操作,全部要經過運維通過後方可執行,運維部門逐步快速收回各項權限

  2. 新增Redis實例,進行分離

  3. 如果有使用類似keys正則命令需求,使用scan命令代替

總結
該事件中出現的兩次事故,完全是由於人爲操作引起的,如果那位工程師,看過Redis的開發規範,會發現是建議禁用keys命令的。另外,有線上的命令操作,一定要經過運維評估後方可進行操作,估計那個工程師是老員工吧,有權限,然後直接就進行操作了
另外,公司的業務發展確實很快,技術跟不上,這是非常非常危險的,極大的增加了宕機的概率
在業務量不大的情況下,那位工程師的操作是完全沒什麼問題的,畢竟併發也不大,但是現在,隨着公司的發展,業務量的成倍成倍增加,技術的擴展卻沒有隨着增長那麼快
公司的技術人手不足也是一方面,絕大多數人都是邊維護老項目邊做新功能,但是對於項目的重構優化,人手卻少了很多,項目優化的優先級不高,這也是很大的一個原因,極有可能出現類似的情況,新服務化構建迫在眉睫

最後的最後,線上操作的任何一條命令,再小心也不爲過
因爲由於你的一個符號而引起的事故可能是你所承擔不起的

Redis開發建議
最後附上Redis的一些開發規範和建議

1.冷熱數據分離,不要將所有數據全部都放到Redis中
雖然Redis支持持久化,但是Redis的數據存儲全部都是在內存中的,成本昂貴。建議根據業務只將高頻熱數據存儲到Redis中【QPS大於5000】,對於低頻冷數據可以使用MySQL/ElasticSearch/MongoDB等基於磁盤的存儲方式,不僅節省內存成本,而且數據量小在操作時速度更快、效率更高!

2.不同的業務數據要分開存儲
不要將不相關的業務數據都放到一個Redis實例中,建議新業務申請新的單獨實例。因爲Redis爲單線程處理,獨立存儲會減少不同業務相互操作的影響,提高請求響應速度;同時也避免單個實例內存數據量膨脹過大,在出現異常情況時可以更快恢復服務! 在實際的使用過程中,redis最大的瓶頸一般是CPU,由於它是單線程作業所以很容易跑滿一個邏輯CPU,可以使用redis代理或者是分佈式方案來提升redis的CPU使用率。

3.存儲的Key一定要設置超時時間
如果應用將Redis定位爲緩存Cache使用,對於存放的Key一定要設置超時時間!因爲若不設置,這些Key會一直佔用內存不釋放,造成極大的浪費,而且隨着時間的推移會導致內存佔用越來越大,直到達到服務器內存上限!另外Key的超時長短要根據業務綜合評估,而不是越長越好!

4.對於必須要存儲的大文本數據一定要壓縮後存儲
對於大文本【+超過500字節】寫入到Redis時,一定要壓縮後存儲!大文本數據存入Redis,除了帶來極大的內存佔用外,在訪問量高時,很容易就會將網卡流量佔滿,進而造成整個服務器上的所有服務不可用,並引發雪崩效應,造成各個系統癱瘓!

5.線上Redis禁止使用Keys正則匹配操作
Redis是單線程處理,在線上KEY數量較多時,操作效率極低【時間複雜度爲O(N)】,該命令一旦執行會嚴重阻塞線上其它命令的正常請求,而且在高QPS情況下會直接造成Redis服務崩潰!如果有類似需求,請使用scan命令代替!

6.可靠的消息隊列服務
Redis List經常被用於消息隊列服務。假設消費者程序在從隊列中取出消息後立刻崩潰,但由於該消息已經被取出且沒有被正常處理,那麼可以認爲該消息已經丟失,由此可能會導致業務數據丟失,或業務狀態不一致等現象發生。
爲了避免這種情況,Redis提供了RPOPLPUSH命令,消費者程序會原子性的從主消息隊列中取出消息並將其插入到備份隊列中,直到消費者程序完成正常的處理邏輯後再將該消息從備份隊列中刪除。同時還可以提供一個守護進程,當發現備份隊列中的消息過期時,可以重新將其再放回到主消息隊列中,以便其它的消費者程序繼續處理。

7.謹慎全量操作Hash、Set等集合結構
在使用HASH結構存儲對象屬性時,開始只有有限的十幾個field,往往使用HGETALL獲取所有成員,效率也很高,但是隨着業務發展,會將field擴張到上百個甚至幾百個,此時還使用HGETALL會出現效率急劇下降、網卡頻繁打滿等問題【時間複雜度O(N)】,此時建議根據業務拆分爲多個Hash結構;或者如果大部分都是獲取所有屬性的操作,可以將所有屬性序列化爲一個STRING類型存儲!同樣在使用SMEMBERS操作SET結構類型時也是相同的情況!

8.根據業務場景合理使用不同的數據結構類型
目前Redis支持的數據庫結構類型較多:字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(Sorted Set), Bitmap, HyperLogLog和地理空間索引(geospatial)等,需要根據業務場景選擇合適的類型。
常見的如:String可以用作普通的K-V、計數類;Hash可以用作對象如商品、經紀人等,包含較多屬性的信息;List可以用作消息隊列、粉絲/關注列表等;Set可以用於推薦;Sorted Set可以用於排行榜等!

9.命名規範
雖然說Redis支持多個數據庫(默認32個,可以配置更多),但是除了默認的0號庫以外,其它的都需要通過一個額外請求才能使用。所以用前綴作爲命名空間可能會更明智一點。
另外,在使用前綴作爲命名空間區隔不同key的時候,最好在程序中使用全局配置來實現,直接在代碼裏寫前綴的做法要嚴格避免,這樣可維護性實在太差了。
如:系統名:業務名:業務數據:其他
但是注意,key的名稱不要過長,儘量清晰明瞭,容易理解,需要自己衡量

10.線上禁止使用monitor命令
禁止生產環境使用monitor命令,monitor命令在高併發條件下,會存在內存暴增和影響Redis性能的隱患

11.禁止大string
核心集羣禁用1mb的string大key(雖然redis支持512MB大小的string),如果1mb的key每秒重複寫入10次,就會導致寫入網絡IO達10MB;

12.redis容量
單實例的內存大小不建議過大,建議在10~20GB以內。
redis實例包含的鍵個數建議控制在1kw內,單實例鍵個數過大,可能導致過期鍵的回收不及時。

13 可靠性
需要定時監控redis的健康情況:使用各種redis健康監控工具,實在不行可以定時返回redis 的 info信息。
客戶端連接儘量使用連接池(長鏈接和自動重連)

歡迎Java工程師朋友們加入Java進階高級架構羣:855355016

本羣提供免費的學習指導 架構資料 以及解答

不懂得問題都可以在本羣提出來

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章