Redis時延問題分析及應對

Redis時延問題分析及應對

Redis的事件循環在一個線程中處理,作爲一個單線程程序,重要的是要保證事件處理的時延短,這樣,事件循環中的後續任務纔不會阻塞; 
當redis的數據量達到一定級別後(比如20G),阻塞操作對性能的影響尤爲嚴重; 
下面我們總結下在redis中有哪些耗時的場景及應對方法;

耗時長的命令造成阻塞

keys、sort等命令

keys命令用於查找所有符合給定模式 pattern 的 key,時間複雜度爲O(N), N 爲數據庫中 key 的數量。當數據庫中的個數達到千萬時,這個命令會造成讀寫線程阻塞數秒; 
類似的命令有sunion sort等操作; 
如果業務需求中一定要使用keys、sort等操作怎麼辦?

解決方案: 
image

在架構設計中,有“分流”一招,說的是將處理快的請求和處理慢的請求分離來開,否則,慢的影響到了快的,讓快的也快不起來;這在redis的設計中體現的非常明顯,redis的純內存操作,epoll非阻塞IO事件處理,這些快的放在一個線程中搞定,而持久化,AOF重寫、Master-slave同步數據這些耗時的操作就單開一個進程來處理,不要慢的影響到快的; 
同樣,既然需要使用keys這些耗時的操作,那麼我們就將它們剝離出去,比如單開一個redis slave結點,專門用於keys、sort等耗時的操作,這些查詢一般不會是線上的實時業務,查詢慢點就慢點,主要是能完成任務,而對於線上的耗時快的任務沒有影響;

smembers命令

smembers命令用於獲取集合全集,時間複雜度爲O(N),N爲集合中的數量; 
如果一個集合中保存了千萬量級的數據,一次取回也會造成事件處理線程的長時間阻塞;

解決方案: 
和sort,keys等命令不一樣,smembers可能是線上實時應用場景中使用頻率非常高的一個命令,這裏分流一招並不適合,我們更多的需要從設計層面來考慮; 
在設計時,我們可以控制集合的數量,將集合數一般保持在500個以內; 
比如原來使用一個鍵來存儲一年的記錄,數據量大,我們可以使用12個鍵來分別保存12個月的記錄,或者365個鍵來保存每一天的記錄,將集合的規模控制在可接受的範圍;

如果不容易將集合劃分爲多個子集合,而堅持用一個大集合來存儲,那麼在取集合的時候可以考慮使用SRANDMEMBER key [count];隨機返回集合中的指定數量,當然,如果要遍歷集合中的所有元素,這個命令就不適合了;

save命令

save命令使用事件處理線程進行數據的持久化;當數據量大的時候,會造成線程長時間阻塞(我們的生產上,reids內存中1個G保存需要12s左右),整個redis被block; 
save阻塞了事件處理的線程,我們甚至無法使用redis-cli查看當前的系統狀態,造成“何時保存結束,目前保存了多少”這樣的信息都無從得知;

解決方案: 
我沒有想到需要用到save命令的場景,任何時候需要持久化的時候使用bgsave都是合理的選擇(當然,這個命令也會帶來問題,後面聊到);

fork產生的阻塞

在redis需要執行耗時的操作時,會新建一個進程來做,比如數據持久化bgsave: 
開啓RDB持久化後,當達到持久化的閾值,redis會fork一個新的進程來做持久化,採用了操作系統的copy-on-wirte寫時複製策略,子進程與父進程共享Page。如果父進程的Page(每頁4K)有修改,父進程自己創建那個Page的副本,不會影響到子進程; 
fork新進程時,雖然可共享的數據內容不需要複製,但會複製之前進程空間的內存頁表,如果內存空間有40G(考慮每個頁表條目消耗 8 個字節),那麼頁表大小就有80M,這個複製是需要時間的,如果使用虛擬機,特別是Xen虛擬服務器,耗時會更長; 
在我們有的服務器結點上測試,35G的數據bgsave瞬間會阻塞200ms以上;

類似的,以下這些操作都有進程fork;

  • Master向slave首次同步數據:當master結點收到slave結點來的syn同步請求,會生成一個新的進程,將內存數據dump到文件上,然後再同步到slave結點中;
  • AOF日誌重寫:使用AOF持久化方式,做AOF文件重寫操作會創建新的進程做重寫;(重寫並不會去讀已有的文件,而是直接使用內存中的數據寫成歸檔日誌);

解決方案: 
爲了應對大內存頁表複製時帶來的影響,有些可用的措施:

  1. 控制每個redis實例的最大內存量; 
    不讓fork帶來的限制太多,可以從內存量上控制fork的時延; 
    一般建議不超過20G,可根據自己服務器的性能來確定(內存越大,持久化的時間越長,複製頁表的時間越長,對事件循環的阻塞就延長) 
    新浪微博給的建議是不超過20G,而我們虛機上的測試,要想保證應用毛刺不明顯,可能得在10G以下;

  2. 使用大內存頁,默認內存頁使用4KB,這樣,當使用40G的內存時,頁表就有80M;而將每個內存頁擴大到4M,頁表就只有80K;這樣複製頁表幾乎沒有阻塞,同時也會提高快速頁表緩衝TLB(translation lookaside buffer)的命中率;但大內存頁也有問題,在寫時複製時,只要一個頁快中任何一個元素被修改,這個頁塊都需要複製一份(COW機制的粒度是頁面),這樣在寫時複製期間,會耗用更多的內存空間;

  3. 使用物理機; 
    如果有的選,物理機當然是最佳方案,比上面都要省事; 
    當然,虛擬化實現也有多種,除了Xen系統外,現代的硬件大部分都可以快速的複製頁表; 
    但公司的虛擬化一般是成套上線的,不會因爲我們個別服務器的原因而變更,如果面對的只有Xen,只能想想如何用好它;

  4. 杜絕新進程的產生,不使用持久化,不在主結點上提供查詢;實現起來有以下方案: 
    1) 只用單機,不開持久化,不掛slave結點。這樣最簡單,不會有新進程的產生;但這樣的方案只適合緩存; 
    如何來做這個方案的高可用? 
    要做高可用,可以在寫redis的前端掛上一個消息隊列,在消息隊列中使用pub-sub來做分發,保證每個寫操作至少落到2個結點上;因爲所有結點的數據相同,只需要用一個結點做持久化,這個結點對外不提供查詢; 

    image 

    2) master-slave:在主結點上開持久化,主結點不對外提供查詢,查詢由slave結點提供,從結點不提供持久化;這樣,所有的fork耗時的操作都在主結點上,而查詢請求由slave結點提供; 
    這個方案的問題是主結點壞了之後如何處理? 
    簡單的實現方案是主不具有可替代性,壞了之後,redis集羣對外就只能提供讀,而無法更新;待主結點啓動後,再繼續更新操作;對於之前的更新操作,可以用MQ緩存起來,等主結點起來之後消化掉故障期間的寫請求; 

    image 

    如果使用官方的Sentinel將從升級爲主,整體實現就相對複雜了;需要更改可用從的ip配置,將其從可查詢結點中剔除,讓前端的查詢負載不再落在新主上;然後,才能放開sentinel的切換操作,這個前後關係需要保證;

持久化造成的阻塞

執行持久化(AOF / RDB snapshot)對系統性能有較大影響,特別是服務器結點上還有其它讀寫磁盤的操作時(比如,應用服務和redis服務部署在相同結點上,應用服務實時記錄進出報日誌);應儘可能避免在IO已經繁重的結點上開Redis持久化;

子進程持久化時,子進程的write和主進程的fsync衝突造成阻塞

在開啓了AOF持久化的結點上,當子進程執行AOF重寫或者RDB持久化時,出現了Redis查詢卡頓甚至長時間阻塞的問題, 此時, Redis無法提供任何讀寫操作;

原因分析: 
Redis 服務設置了 appendfsync everysec, 主進程每秒鐘便會調用 fsync(), 要求內核將數據”確實”寫到存儲硬件裏. 但由於服務器正在進行大量IO操作, 導致主進程 fsync()/操作被阻塞, 最終導致 Redis 主進程阻塞.

redis.conf中是這麼說的: 
When the AOF fsync policy is set to always or everysec, and a background 
saving process (a background save or AOF log background rewriting) is 
performing a lot of I/O against the disk, in some Linux configurations 
Redis may block too long on the fsync() call. Note that there is no fix for 
this currently, as even performing fsync in a different thread will block 
our synchronous write(2) call. 
當執行AOF重寫時會有大量IO,這在某些Linux配置下會造成主進程fsync阻塞;

解決方案: 
設置 no-appendfsync-on-rewrite yes, 在子進程執行AOF重寫時, 主進程不調用fsync()操作;注意, 即使進程不調用 fsync(), 系統內核也會根據自己的算法在適當的時機將數據寫到硬盤(Linux 默認最長不超過 30 秒). 
這個設置帶來的問題是當出現故障時,最長可能丟失超過30秒的數據,而不再是1秒;

子進程AOF重寫時,系統的sync造成主進程的write阻塞

我們來梳理下: 
1) 起因:有大量IO操作write(2) 但未主動調用同步操作 
2) 造成kernel buffer中有大量髒數據 
3) 系統同步時,sync的同步時間過長 
4) 造成redis的寫aof日誌write(2)操作阻塞; 
5) 造成單線程的redis的下一個事件無法處理,整個redis阻塞(redis的事件處理是在一個線程中進行,其中寫aof日誌的write(2)是同步阻塞模式調用,與網絡的非阻塞write(2)要區分開來)

產生1)的原因:這是redis2.6.12之前的問題,AOF rewrite時一直埋頭的調用write(2),由系統自己去觸發sync。 
另外的原因:系統IO繁忙,比如有別的應用在寫盤;

解決方案: 
控制系統sync調用的時間;需要同步的數據多時,耗時就長;縮小這個耗時,控制每次同步的數據量;通過配置按比例(vm.dirty_background_ratio)或按值(vm.dirty_bytes)設置sync的調用閾值;(一般設置爲32M同步一次) 
2.6.12以後,AOF rewrite 32M時會主動調用fdatasync;

另外,Redis當發現當前正在寫的文件有在執行fdatasync(2)時,就先不調用write(2),只存在cache裏,免得被block。但如果已經超過兩秒都還是這個樣子,則會強行執行write(2),即使redis會被block住。

AOF重寫完成後合併數據時造成的阻塞

在bgrewriteaof過程中,所有新來的寫入請求依然會被寫入舊的AOF文件,同時放到AOF buffer中,當rewrite完成後,會在主線程把這部分內容合併到臨時文件中之後才rename成新的AOF文件,所以rewrite過程中會不斷打印"Background AOF buffer size: 80 MB, Background AOF buffer size: 180 MB",要監控這部分的日誌。這個合併的過程是阻塞的,如果產生了280MB的buffer,在100MB/s的傳統硬盤上,Redis就要阻塞2.8秒;

解決方案: 
將硬盤設置的足夠大,將AOF重寫的閾值調高,保證高峯期間不會觸發重寫操作;在閒時使用crontab 調用AOF重寫命令;

轉自:https://www.cnblogs.com/me115/p/5032177.html

參考: 
http://www.oschina.net/translate/redis-latency-problems-troubleshooting 
https://github.com/springside/springside4/wiki/redis

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