1. 程式人生 > >Redis服務端優化實踐:配置優化、主從切換、持久化

Redis服務端優化實踐:配置優化、主從切換、持久化

Redis是部門業務重要的核心業務元件,被廣泛應用在行情繫統、推送服務、資料中心、投顧、圈子、量化分析等平臺。在使用Redis的過程中遇到了很多問題,涉及到開發者使用API時的一些注意事項,以及如何通過優化服務端配置提高Redis的健壯性、容錯性。

本文通過案例分析的方式分享一下我們在Redis服務端配置優化、主從切換、持久化等方向的實踐。

注:Redis Server端版本2.8.19,客戶端使用Jedis2.6.0。

一、Server配置

llkeys-lru還是noeviction

Redis的服務端有這樣一個配置引數maxmemory-policy allkeys-lru,是說Redis的使用記憶體達到分配上限的時候預設使用lru的key淘汰策略(還有其它可選策略)。我們曾經遇到這樣一個問題,應用端因為一次不當的hgetall操作,導致Redis使用記憶體膨脹超過上限,觸發了key淘汰,結果重要資料被清空(幸好大部分業務都做了災備恢復機制),圖中可見使用記憶體上升後又迅速回落,是因為觸發了淘汰演算法。

Redis

這次事故之後我們將淘汰策略改為noeviction—禁止key淘汰。那麼,到底要不要調整淘汰策略呢?其實主要看業務場景,Redis是被當作允許資料淘汰的快取還是儲存核心資料的記憶體資料庫。

使用Redis的一個核心優勢就是Redis具有豐富的資料結構,當前的大部分業務也都是看中這一點才選擇Redis,某些複雜結構的資料集一旦被清空,沒有業務上的恢復機制是不可能自動重建的。如果你的Redis不是簡單的字串快取,那麼就需要慎重考慮是否禁用key淘汰了。

client-output-buffer-limit normal 0 0 0 ?

美團曾經因為工程師開啟monitor命令導致記憶體飆升引發事故,我們hgetall的例子與之同出一轍,原理都是因為Redis在處理客戶端請求的時候會為每個請求分配一個輸出緩衝區,如下圖:

Redis

預設這個緩衝區是無限分配的,而且同樣佔用Redis的記憶體空間,也就意味著一個大查詢過來使用記憶體就可能成倍飆升,多查幾次記憶體就爆了,如果不幸又沒有設定禁止key淘汰,那麼資料很可能就被清0了。Redis的API提供了類似hgetall、smembers、keys等很多O(N)級別指令,如果你的應用QPS很高,或直接面向終端使用者請求,那麼一定慎用O(N)級別指令;如果指令的目標資料集很大,那麼意味著要麼的請求很耗時會長期佔用cpu,要麼查詢的資料量很大導致Redis記憶體飆升。Redis單執行緒的設計註定不是為高頻大資料集查詢準備的。

我們對這個引數設定了client-output-buffer-limit normal 10mb 5mb 10的限制,單個請求10mb緩衝區的分配已經足夠高了,還可以更低,具體視應用而定,但絕不建議無限分配。

lua-time-limit

Redis支援自定義命令的一個優勢是因為支援Lua指令碼,我們部分業務使用了Lua指令碼,這些自定義指令的執行時間需要被測試、評估,在要求QPS比較高的應用中,一定不要有Lua指令碼長期佔用CPU資源。雖然lua-time-limit不會終止Lua指令碼,但會使Redis到達時限後開始響應客戶端請求返回Busy錯誤,這樣能在大量連線被掛起、超時時,避免不知道發生了什麼。Lua時限需要設定,但還是建議提前測試lua指令碼的效能。

rename-command

Redis提供了rename-command可以改寫某些危險命令,使其無法執行成功,這裡建議可以禁掉如下幾個命令:

  • rename-command FLUSHALL “” /*刪除所有現有的資料庫*/
  • rename-command FLUSHDB “” /*清空當前資料庫中的所有key*/
  • rename-command SHUTDOWN “” /*關閉 redis 伺服器(server)*/
  • rename-command KEYS “” /*複雜度O(N),遍歷所有key*/
  • rename-command MONITOR “” /*debug命令,檢視Redis正在執行的命令*/

公司曾有工程師誤執行過flushall導致資料清空,美團掉過monitor的坑,我們也出現過使用keys造成的CPU卡頓。這些命令其實不必也不應該出現在應用程式API呼叫中。寄希望於工程師不要誤呼叫,不如在server端直接禁掉。

slaveof

這個指令很特別,它指定了當前Redis例項是某個Master例項的slave。這個指令如果在配置檔案中寫死,那麼例項啟動後就只能是slave,除非有哨兵將它提升為master,或手動執行slaveof no one。這個指令是一個會被哨兵動態從配置檔案裡刪除或者新增的指令,它的存在與否最好交由哨兵決定。

我們之前的配置檔案是通過子檔案引入的這個指令,這樣會有一個問題,如果某個slave被哨兵選舉成為了master,它的slaveof指令要被移除,但子檔案中寫死的指令是無法移除的,一旦重啟這個master例項,它又成了slave。如果不注意這個子檔案的存在,問題還很不好排查,不知道發生了什麼。線上沒有遇到過問題,測試環境做過一次主從切換,操作過程中,重啟了下master,結果掉坑了。這個測試的例子後面還會講,因為它還涉及到哨兵及主從切換。

二、主從切換

Redis的使用一般有單點和叢集兩種方式,對QPS有很高的要求一般會搭建叢集,目前我們這邊一般業務對QPS沒有那麼高的需求,所以都是單點主從模式(叢集各節點也是主從模式)。

說到主從切換不得不先說哨兵,Master節點故障時勢必要求能及時地切換到slave節點,儘快恢復應用。那麼監測Master狀態,發起主從切換過程,最終將新選舉的master通知給客戶端應用的任務就是由哨兵完成的。哨兵一般需要部署一個叢集。

回到上面的例子,講這個例子是因為後面的調研及對配置的調整都是由這個例子引起的,故事是這樣的:

1)第一次主從切換,關閉r1,master順利切換到r2
2)啟動r1,主從同步後r1被誤關閉
3)第二次主從切換,關閉r2,切換失敗(r1被誤關閉,尷尬)
(這時候的配置是這樣的:
r1在之前第2步啟動後被哨兵打上了slaveof r2 6379標記;
r2主配置檔案中哨兵會移除slaveof標記,但子檔案中的標記無法移除;
因為切換失敗,哨兵的配置檔案中master的配置還是r2;)
4)切換失敗了,重啟r1,因為本來就要切到r1嘛,結果哨兵沒反應,應用端還是報錯,看日誌應用端還是在試圖連線r2。
5)沒辦法,重啟r2,結果應用端換了個報錯,slave模式不可寫。也就是r2連通了,但slave模式不可寫資料(slave模式可寫與否可以配置,我們這裡配置slave只讀)。
6)這時候用info命令檢視r2的角色的確是slave,檢視r1也是slave。

這裡就蒙圈了,r1和r2都起來了,卻沒有master,master哪裡去了,大概持續了10分鐘,奇蹟也沒有發生,對r2強制用slaveof no one命令提升為master,服務恢復。整個過程走下來有很多疑問,之前沒有了解過Redis主從切換的知識,帶著疑問開始在本地模擬測試,這裡主要有三個問題:

  1. 為什麼重啟後兩個例項都是slave
  2. 為什麼哨兵沒有從兩個例項中選擇一個成為master,並通知客戶端。
  3. 主從切換這麼重要的過程什麼情況下會失敗呢?

第一個問題上面已經講過了,重啟後兩個例項都是slave,這是因為第一次切換r1成為master,r1重啟會被哨兵打上slaveof標記,被誤關閉當然slaveof標記是不會清除的了。r2雖然是master,但重啟的時候子檔案中指定了slaveof,哨兵無法移除,所以重啟也就進入了slave模式。

第二個問題,哨兵為什麼沒有發揮作用?其實如果我們再等一段時間,奇蹟是可以發生的,等多久呢,總共需要等30分鐘(是不是有點太久了?)。Sentinel有一個重要的配置引數sentinel failover-timeout master 900000,這裡我們預設的配置是900000ms即15分鐘,如果sentinel操作過一次故障轉移但失敗了,那麼下次故障轉移的時間是2*15=30分鐘。上面的例子當關閉r2的時候哨兵已經做過一次切換了,但r1也掛了,所以這個failover是失敗的。需要等30分鐘才能做第二次,重啟r1和r2,雙slave困局也就沒人來破解了。由於等待時間太長了,不知道發生了什麼,讓人以為哨兵是不是掛了。官方預設配置是180000,我們因為歷史原因提高了這個時限,恢復預設配置足夠了。

第三個問題,什麼情況下主從切換會失敗?調研了部分資料,並本地模擬我們的當前配置搭建了Redis環境做了一系列測試,發現以下問題:

(1)超過一半以上的sentinel宕機,剩餘的sentinel無法進行failover

這種情況如果只有兩個sentinel,且其中一個sentinel和master例項同機部署,如果因為虛擬機器故障或網路問題master被認為宕機,同機的sentinel基本也就掛了,不能切換的概率100%。sentinel選舉leader需要超過一半的sentinel同意,少數的sentinel永遠無法選舉出leader,所以sentinel最好不要只設置兩個,而且sentinel最好不要與redis例項同物理機部署,否則redis例項宕機很大可能同機的sentinel也掛了,sentinel就失去了意義。

(2)大部分的sentinel投票給自己,沒有candidate獲取超過一半的票數,所以沒有選舉出leader,需要等待2*failover-timeout時間,重新發起切換。

這種情況在只有兩個sentinel時發生的相當頻繁,sentinel1和sentinel2同時觀察master客觀下線,同時發起投票,每個sentinel都選自己,無法選舉出leader。多個sentinel理論上也有可能出現選舉失敗,概率低一些。尤其第一次選舉失敗,下次選舉要等待2*failover-timeout,我們這裡是30min,會讓大多數人覺得不知道發生了什麼。這裡還是建議根據自己業務使用情況,儘可能縮短failover間隔。

(3)即使sentinel成功選舉出了leader,切換還是不一定成功,不是所有存活的slave都可以提升為master,需要經過篩選。

  • slave節點狀態處於S_DOWN, O_DOWN, DISCONNECTED的不能通過篩選
  • 最近一次ping應答時間不超過5倍ping的間隔(假如ping的間隔為1秒,則最近一次應答延遲不應超過5秒,redis sentinel預設為1秒),否則不能通過篩選
  • info_refresh應答不超過3倍info_refresh的間隔(原理同2,redis sentinel預設為10秒)
  • slave節點與master節點失去聯絡的時間不能超過指定閾值10*down-after-milliseconds(我們一般設定60s,10倍就是10分鐘),意思是說,與master同步太不及時的slave,不應該參與被選舉。
  • Slave priority不等於0(這個是在配置檔案中指定,預設配置為100)。

http://ks.netease.com/blog?id=7468 網易實踐者社群分享的這次事故叢集中12個節點7個主從切換失敗就是由於slave節點與master失連太久造成了沒有成功切換,當然這次事故還有其它原因:

1)master節點和同機部署的sentinel都出現了問題

2)3個sentinel掛掉了一個

如果3個sentinel獨立部署,這個故障很大程度能夠避免。

應急預案(本文以Jedis2.6.0客戶端為例):

由於主從切換可能失敗,具體原因具體分析:

  1. 如果是因為大部分sentinel都掛了,不足以選舉出leader,選擇一個存活的sentinel強制執行sentinel failover吧,如果有備選slave成功被提升為master,客戶端應用不需重啟。
  2. 如果是因為哨兵沒有選舉成功,看等待時間是否還能忍,能忍就等待下次failover,否則同1)步驟。
  3. 如果是因為沒有slave通過篩選,建議停止全部哨兵,將某個slave提升為master,修改哨兵配置指向這個新的master,重啟哨兵,重啟客戶端應用

(還可以再起一個Redis例項,新例項slaveof 新master, 手動在sentinel上做下failover,這樣就不用重啟客戶端應用了~)

哨兵部署建議:

  1. 按奇數個部署,至少要部署3個,哨兵之間、Redis例項之間物理機獨立。
  2. sentinel monitor master xxx.xxx.xxx.xxx xxxx 1

    哨兵的這個配置最好不要配置為1。quorum的值為1意味著只要一個sentinel發現master節點無響應就可以標記為客觀下線,從而發起主從切換,quorum最好設定超過sentinel個數的一半向上取整。

  3. entinel failover-timeout master 900000 //毫秒級

    條件允許的情況下儘可能縮短這個切換間隔吧。

三、持久化

Redis有兩種持久化方式,AOF和RDB,AOF持久化是指追加寫命令到aof檔案的方式,RDB是指定期儲存記憶體快照到rdb檔案的方式。

RDB雖然可以通過bgsave指令後臺儲存快照,但fork()子程序是有開銷的,在記憶體資料集較大的情況下會佔用很長的cpu時間,fork新程序時,雖然可共享的資料內容不需要複製,但會複製之前程序空間的記憶體頁表,如果記憶體空間有40G(考慮每個頁表條目消耗 8 個位元組),那麼頁表大小就有80M,這個複製是需要時間的,在有的伺服器結點上測試,35G的資料bgsave瞬間會阻塞200ms以上,一般建議Redis使用記憶體不超過20g。I/O消耗,我們線上是在Slave節點開啟rdb持久化,磁碟效能一般,1.2g的rdb檔案持久化一分鐘一次,一次大概耗時30s左右,所以rdb的頻率也不能太頻繁,需要根據情況做好配置。

AOF是追加寫命令到aof檔案的方式,優點是可以基本做到資料無損,缺點是檔案增長較快,需要間歇性bgrewrite,bgrewrite也是一個既耗cpu又耗磁碟IO的操作,單cpu利用率最高可達100%。bgrewrite期間可以設定將新的寫請求暫時快取,bgrewrite完成後同步寫盤,同步會暫時停止處理客戶端請求,如果bgrewrite時間較長,緩衝區積壓資料較多,核心阻塞時間會很長,所以如果必須要開啟aof,一般建議找幾個空閒時段設定指令碼來做bgrewrite。

AOF還有一個比較坑的地方是刷盤策略fsync的設定,這個設定一般有3種方式:always、everysec、no,如果設定為no,就將寫盤的時機交給作業系統,這在很大程度上犧牲了aof資料無損的優勢,如果設定為always就意味著每條命令都會同步刷盤,會造成頻繁I/O,所以一般建議是設定everysec,Redis會預設每隔一秒進行一次fsync呼叫,將緩衝區中的資料寫到磁碟。但是當這一次的fsync呼叫時長超過1秒時。Redis會採取延遲fsync的策略,再等一秒鐘。也就是在兩秒後再進行fsync,這一次的fsync就不管會執行多長時間都會進行。這時候由於在fsync時檔案描述符會被阻塞,所以當前的寫操作就會阻塞,因為是同步操作所以核心處理阻塞,開啟aof且要求Redis效能無損對磁碟有極高要求。下圖是我們一段時間內的磁碟監控截圖:

監控

這種間歇性的磁碟IO毛刺就會使fsync阻塞,fsync阻塞時一般會輸出如下日誌:

日誌

持久化為Redis提供了異常情況下的資料恢復機制,但開啟持久化是有代價的,哪一種持久化都可能造成CPU卡頓,影響對客戶端請求的處理。不開啟持久化又存在風險,如果一旦誤重啟master節點,或者試想這樣一種場景,主從切換失敗,很可能因為疏忽直接重啟master,這時沒有開啟持久化的master會把所有slave的資料清0。所以是否開啟持久化,怎樣開啟持久化是一個難題。和運維同事探討了一些方案,這裡總結一下供大家參考:

1、極端情況下可以容忍全量資料丟失,那麼建議master關閉持久化,slave關閉持久化;

2、極端情況下不能容忍全量資料丟失,但可以容忍部分資料丟失,如果記憶體資料集較小且不會增長建議master開啟rdb,slave開啟rdb;如果資料集很大,或不確定資料集增長趨勢,建議master關閉持久化,slave開啟rdb

開啟rdb需要cpu和磁碟效能保障。如果master關閉持久化,slave開啟rdb需要保證slave的rdb不會被master誤重啟所覆蓋,這裡提供幾種方案:

  • 重啟指令碼包一層命令先網路請求載入備機備份目錄下的rdb檔案後再執行start,可以防止誤重啟,但備機調整部署可能需要調整指令碼,主機開啟持久化也需要調整指令碼
  • 定時將rdb檔案通過網路io傳給master節點(檔案大比較耗時,檔案增長需要考慮定時指令碼執行間隔,否則會造成持續的網路io),而且也會有一定資料損失
  • 定時備份Slave的rdb到備份目錄,不做任何其他操作,誤重啟時人工拷貝rdb到master節點(會有一定資料損失)

3、最大限度需要資料無損,建議master開啟aof,slave開啟aof

開啟aof需要cpu和磁碟效能保障。開啟aof建議fsync同步刷盤使用everysec,自定義指令碼在應用空閒時定時做bgrewrite,bgrewrite期間增量資料做緩衝。

目前大部分業務都允許部分資料丟失,為使Redis效能最大化,關閉了Master持久化,slave開啟rdb,為防止誤重啟對rdb做了5分鐘一次備份,保留最近1小時的備份檔案,必要時人工copy到master資料目錄下恢復資料。後續硬體效能提升後,看情況再調整持久化機制。

作者介紹

閆東旭,網易樂得高階工程師。

原文來自微信公眾號:DBAplus社群