Redis的事件循環在一個線程中處理,做爲一個單線程程序,重要的是要保證事件處理的時延短,這樣,事件循環中的後續任務纔不會阻塞;
當redis的數據量達到必定級別後(好比20G),阻塞操做對性能的影響尤其嚴重;
下面咱們總結下在redis中有哪些耗時的場景及應對方法;前端
keys命令用於查找全部符合給定模式 pattern 的 key,時間複雜度爲O(N), N 爲數據庫中 key 的數量。當數據庫中的個數達到千萬時,這個命令會形成讀寫線程阻塞數秒;
相似的命令有sunion sort等操做;
若是業務需求中必定要使用keys、sort等操做怎麼辦?git
解決方案:
github
在架構設計中,有「分流」一招,說的是將處理快的請求和處理慢的請求分離來開,不然,慢的影響到了快的,讓快的也快不起來;這在redis的設計中體現的很是明顯,redis的純內存操做,epoll非阻塞IO事件處理,這些快的放在一個線程中搞定,而持久化,AOF重寫、Master-slave同步數據這些耗時的操做就單開一個進程來處理,不要慢的影響到快的;
一樣,既然須要使用keys這些耗時的操做,那麼咱們就將它們剝離出去,好比單開一個redis slave結點,專門用於keys、sort等耗時的操做,這些查詢通常不會是線上的實時業務,查詢慢點就慢點,主要是能完成任務,而對於線上的耗時快的任務沒有影響;redis
smembers命令用於獲取集合全集,時間複雜度爲O(N),N爲集合中的數量;
若是一個集合中保存了千萬量級的數據,一次取回也會形成事件處理線程的長時間阻塞;算法
解決方案:
和sort,keys等命令不同,smembers多是線上實時應用場景中使用頻率很是高的一個命令,這裏分流一招並不適合,咱們更多的須要從設計層面來考慮;
在設計時,咱們能夠控制集合的數量,將集合數通常保持在500個之內;
好比原來使用一個鍵來存儲一年的記錄,數據量大,咱們可使用12個鍵來分別保存12個月的記錄,或者365個鍵來保存每一天的記錄,將集合的規模控制在可接受的範圍;spring
若是不容易將集合劃分爲多個子集合,而堅持用一個大集合來存儲,那麼在取集合的時候能夠考慮使用SRANDMEMBER key [count];隨機返回集合中的指定數量,固然,若是要遍歷集合中的全部元素,這個命令就不適合了;數據庫
save命令使用事件處理線程進行數據的持久化;當數據量大的時候,會形成線程長時間阻塞(咱們的生產上,reids內存中1個G保存須要12s左右),整個redis被block;
save阻塞了事件處理的線程,咱們甚至沒法使用redis-cli查看當前的系統狀態,形成「什麼時候保存結束,目前保存了多少」這樣的信息都無從得知;緩存
解決方案:
我沒有想到須要用到save命令的場景,任什麼時候候須要持久化的時候使用bgsave都是合理的選擇(固然,這個命令也會帶來問題,後面聊到);服務器
在redis須要執行耗時的操做時,會新建一個進程來作,好比數據持久化bgsave:
開啓RDB持久化後,當達到持久化的閾值,redis會fork一個新的進程來作持久化,採用了操做系統的copy-on-wirte寫時複製策略,子進程與父進程共享Page。若是父進程的Page(每頁4K)有修改,父進程本身建立那個Page的副本,不會影響到子進程;
fork新進程時,雖然可共享的數據內容不須要複製,但會複製以前進程空間的內存頁表,若是內存空間有40G(考慮每一個頁表條目消耗 8 個字節),那麼頁表大小就有80M,這個複製是須要時間的,若是使用虛擬機,特別是Xen虛擬服務器,耗時會更長;
在咱們有的服務器結點上測試,35G的數據bgsave瞬間會阻塞200ms以上;網絡
相似的,如下這些操做都有進程fork;
解決方案:
爲了應對大內存頁表複製時帶來的影響,有些可用的措施:
控制每一個redis實例的最大內存量;
不讓fork帶來的限制太多,能夠從內存量上控制fork的時延;
通常建議不超過20G,可根據本身服務器的性能來肯定(內存越大,持久化的時間越長,複製頁表的時間越長,對事件循環的阻塞就延長)
新浪微博給的建議是不超過20G,而咱們虛機上的測試,要想保證應用毛刺不明顯,可能得在10G如下;
使用大內存頁,默認內存頁使用4KB,這樣,當使用40G的內存時,頁表就有80M;而將每一個內存頁擴大到4M,頁表就只有80K;這樣複製頁表幾乎沒有阻塞,同時也會提升快速頁表緩衝TLB(translation lookaside buffer)的命中率;但大內存頁也有問題,在寫時複製時,只要一個頁快中任何一個元素被修改,這個頁塊都須要複製一份(COW機制的粒度是頁面),這樣在寫時複製期間,會耗用更多的內存空間;
使用物理機;
若是有的選,物理機固然是最佳方案,比上面都要省事;
固然,虛擬化實現也有多種,除了Xen系統外,現代的硬件大部分均可以快速的複製頁表;
但公司的虛擬化通常是成套上線的,不會由於咱們個別服務器的緣由而變動,若是面對的只有Xen,只能想一想如何用好它;
杜絕新進程的產生,不使用持久化,不在主結點上提供查詢;實現起來有如下方案:
1) 只用單機,不開持久化,不掛slave結點。這樣最簡單,不會有新進程的產生;但這樣的方案只適合緩存;
如何來作這個方案的高可用?
要作高可用,能夠在寫redis的前端掛上一個消息隊列,在消息隊列中使用pub-sub來作分發,保證每一個寫操做至少落到2個結點上;由於全部結點的數據相同,只須要用一個結點作持久化,這個結點對外不提供查詢;
2) master-slave:在主結點上開持久化,主結點不對外提供查詢,查詢由slave結點提供,從結點不提供持久化;這樣,全部的fork耗時的操做都在主結點上,而查詢請求由slave結點提供;
這個方案的問題是主結點壞了以後如何處理?
簡單的實現方案是主不具備可替代性,壞了以後,redis集羣對外就只能提供讀,而沒法更新;待主結點啓動後,再繼續更新操做;對於以前的更新操做,能夠用MQ緩存起來,等主結點起來以後消化掉故障期間的寫請求;
若是使用官方的Sentinel將從升級爲主,總體實現就相對複雜了;須要更改可用從的ip配置,將其從可查詢結點中剔除,讓前端的查詢負載再也不落在新主上;而後,才能放開sentinel的切換操做,這個先後關係須要保證;
執行持久化(AOF / RDB snapshot)對系統性能有較大影響,特別是服務器結點上還有其它讀寫磁盤的操做時(好比,應用服務和redis服務部署在相同結點上,應用服務實時記錄進出報日誌);應儘量避免在IO已經繁重的結點上開Redis持久化;
在開啓了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秒;
咱們來梳理下:
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住。
在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重寫命令;
參考:
http://www.oschina.net/translate/redis-latency-problems-troubleshooting
https://github.com/springside/springside4/wiki/redis
Posted by: 大CC | 10DEC,2015
博客:blog.me115.com [訂閱]
Github:大CC