摘要: 使用 Redis 的開發者必看,吸取教訓啊!
Fundebug經授權轉載,版權歸原作者所有。
最近的網際網路線上事故發生比較頻繁,2018 年 9 月 19 號順豐發生了一起線上刪庫事件,在這裡就不介紹了。
在這裡講述一下最近發生在我公司的事故,以及如何避免,並且如何處理優化。
間接原因還有很多,技術跟不上業務的發展,由每日百萬量到千萬級是一個大的跨進,公司對於系統優化的處理優先順序不高,技術開發人手的短缺
第一次當機
2018 年 9 月 13 號某個點,公司某服務化專案的 RDS 例項連線飆升,CPU 升到 100%,拒絕了其他應用的所有請求服務
整個過程如下:
- 監控報警,顯示 RDS 的 CPU 使用率達到 80%以上,DBA 介入,準備 KILL 慢 SQL
- 1 分鐘內,沒有發現明顯阻塞的 SQL,CPU 持續上升到 99%
- 5 分鐘內,大量應用報警,並且拒絕服務,RDS 的監控顯示出現大量慢 SQL,聯絡伺服器資料庫提供商進行協助
- 8 分鐘內,進行資料庫主備切換(業務會受損,但是也沒辦法,沒有定位到問題)
- 9 分鐘內,部分業務恢復,但是一些業務訂單的回撥訊息堆積超過 20w,備庫的 CPU 使用率也持續上升
- 15 分鐘內,備庫 CPU 使用率超過 97%,業務再次中斷,進行切回主庫,並進行限流
- 20 分鐘內,關閉一些次要應用的流量入口
- 25 分鐘內,主庫 CPU 使用率恢復正常
- 30 分鐘內,逐步開啟關閉的限流應用
- 35 分鐘內,所有應用恢復正常
- 接下來就是與伺服器資料庫提供商成立應急小組緊急優化可能出現的慢 SQL,雖然說可能解決了一些慢 SQL,但此次並沒有定位到具體的問題,也就為幾天後再次發生當機事件埋下了伏筆
事故影響
某服務化專案服務不可用幾十分鐘,造成訂單數減少幾十萬筆,損失百萬資金。
原因分析
當時是沒有定位到具體的原因的,但是下面的原因也是一部分可能引起當機的情況。
某服務化專案的業務增速非常快,在高峰期,資料庫 QPS 突破 35000,系統處於高負荷狀態。
在高峰期如果同時執行幾個全表掃描的 SQL,會造成資料庫壓力急劇上升,應用超時增多,前端應用超時,使用者重試,流量飆升,形成了雪崩效應。
主要原因在與一些老專案的 SQL 查詢效能較差,並且使用的主庫,對資料庫影響較大。資料庫 QPS 太高,但是快取方案因為人手原因一直沒有落地,慢 SQL 的問題處理優先順序應該提升
改進方案
- 針對每個應用建一個資料庫賬號,嚴格按照規範使用
- 快取優化方案即時落地,慢 SQL 問題優先處理,集中處理目前已經發現的慢 SQL(查詢時間超過 1S)
- 升級資料庫配置
- 遷移非核心業務到新的 RDS 例項中去
第二次當機
由於上一次的當機原因未找到,所以此次的當機是可以預見的。
2018 年 9 月 19 號,還是一樣的"配方",還是原來的"味道"。同一個 RDS,CPU 飆升至 100%,接下來就是拒絕服務,當機。當然,有了第一次的經驗,直接主從切換,在幾十秒左右就恢復了所有業務,但還是嚴重影響了公司的業務和形象。
原因分析
恢復業務後,公司緊急召開了緊急事故研究會議,當然,我的級別是參與不了的。公司的高管,高層技術架構、DBA、各個專案的主負責人一起進行了會議。
在此次會議中,經過檢視各個專案的日誌,後臺的監控資料,發現在那臺 RDS 資料庫 CPU 飆升時,有一臺 Redis 資料庫記憶體將近 100%,然後急劇下降。聯絡第一次的當機情況,也是類似的。
接下來就是聯絡伺服器資料庫提供商,將那臺 Redis 最近一週的命令全部呼叫出來,最後發現,在那個時間點執行了一條keys *...*
命令。公司的一個工程師執行 keys 模糊的匹配命令是為了清理沒用的鍵,但是沒有考慮到keys *
進行模糊匹配引發 Redis 鎖,造成 Redis 鎖住,CPU 飆升,引起了所有呼叫鏈路的超時並且卡住,等 Redis 鎖的那幾秒結束,所有的請求流量全部請求到 RDS 資料庫中,使資料庫產生了雪崩,使資料庫當機。
改進方案
- 所有線上操作,全部要經過運維通過後方可執行,運維部門逐步快速收回各項許可權
- 新增 Redis 例項,進行分離
- 如果有使用類似 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 資訊。客戶端連線儘量使用連線池(長連結和自動重連)。