redis時延問題分析及應(yīng)對
Redis的事件循環(huán)在一個線程中處理,作為一個單線程程序,重要的是要保證事件處理的時延短,這樣,事件循環(huán)中的后續(xù)任務(wù)才不會阻塞;
當(dāng)redis的數(shù)據(jù)量達到一定級別后(比如20G),阻塞操作對性能的影響尤為嚴重;
下面我們總結(jié)下在redis中有哪些耗時的場景及應(yīng)對方法;
耗時長的命令造成阻塞
keys、sort等命令
keys命令用于查找所有符合給定模式 pattern 的 key,時間復(fù)雜度為O(N), N 為數(shù)據(jù)庫中 key 的數(shù)量。當(dāng)數(shù)據(jù)庫中的個數(shù)達到千萬時,這個命令會造成讀寫線程阻塞數(shù)秒;
類似的命令有sunion sort等操作;
如果業(yè)務(wù)需求中一定要使用keys、sort等操作怎么辦?
解決方案:
在架構(gòu)設(shè)計中,有“分流”一招,說的是將處理快的請求和處理慢的請求分離來開,否則,慢的影響到了快的,讓快的也快不起來;這在redis的設(shè)計中體現(xiàn)的非常明顯,redis的純內(nèi)存操作,epoll非阻塞IO事件處理,這些快的放在一個線程中搞定,而持久化,AOF重寫、Master-slave同步數(shù)據(jù)這些耗時的操作就單開一個進程來處理,不要慢的影響到快的;
同樣,既然需要使用keys這些耗時的操作,那么我們就將它們剝離出去,比如單開一個redis slave結(jié)點,專門用于keys、sort等耗時的操作,這些查詢一般不會是線上的實時業(yè)務(wù),查詢慢點就慢點,主要是能完成任務(wù),而對于線上的耗時快的任務(wù)沒有影響;
smembers命令
smembers命令用于獲取集合全集,時間復(fù)雜度為O(N),N為集合中的數(shù)量;
如果一個集合中保存了千萬量級的數(shù)據(jù),一次取回也會造成事件處理線程的長時間阻塞;
解決方案:
和sort,keys等命令不一樣,smembers可能是線上實時應(yīng)用場景中使用頻率非常高的一個命令,這里分流一招并不適合,我們更多的需要從設(shè)計層面來考慮;
在設(shè)計時,我們可以控制集合的數(shù)量,將集合數(shù)一般保持在500個以內(nèi);
比如原來使用一個鍵來存儲一年的記錄,數(shù)據(jù)量大,我們可以使用12個鍵來分別保存12個月的記錄,或者365個鍵來保存每一天的記錄,將集合的規(guī)模控制在可接受的范圍;
如果不容易將集合劃分為多個子集合,而堅持用一個大集合來存儲,那么在取集合的時候可以考慮使用SRANDMEMBER key [count];隨機返回集合中的指定數(shù)量,當(dāng)然,如果要遍歷集合中的所有元素,這個命令就不適合了;
save命令
save命令使用事件處理線程進行數(shù)據(jù)的持久化;當(dāng)數(shù)據(jù)量大的時候,會造成線程長時間阻塞(我們的生產(chǎn)上,reids內(nèi)存中1個G保存需要12s左右),整個redis被block;
save阻塞了事件處理的線程,我們甚至無法使用redis-cli查看當(dāng)前的系統(tǒng)狀態(tài),造成“何時保存結(jié)束,目前保存了多少”這樣的信息都無從得知;
解決方案:
我沒有想到需要用到save命令的場景,任何時候需要持久化的時候使用bgsave都是合理的選擇(當(dāng)然,這個命令也會帶來問題,后面聊到);
fork產(chǎn)生的阻塞
在redis需要執(zhí)行耗時的操作時,會新建一個進程來做,比如數(shù)據(jù)持久化bgsave:
開啟RDB持久化后,當(dāng)達到持久化的閾值,redis會fork一個新的進程來做持久化,采用了操作系統(tǒng)的copy-on-wirte寫時復(fù)制策略,子進程與父進程共享Page。如果父進程的Page(每頁4K)有修改,父進程自己創(chuàng)建那個Page的副本,不會影響到子進程;
fork新進程時,雖然可共享的數(shù)據(jù)內(nèi)容不需要復(fù)制,但會復(fù)制之前進程空間的內(nèi)存頁表,如果內(nèi)存空間有40G(考慮每個頁表條目消耗 8 個字節(jié)),那么頁表大小就有80M,這個復(fù)制是需要時間的,如果使用虛擬機,特別是Xen虛擬服務(wù)器,耗時會更長;
在我們有的服務(wù)器結(jié)點上測試,35G的數(shù)據(jù)bgsave瞬間會阻塞200ms以上;
類似的,以下這些操作都有進程fork;
- Master向slave首次同步數(shù)據(jù):當(dāng)master結(jié)點收到slave結(jié)點來的syn同步請求,會生成一個新的進程,將內(nèi)存數(shù)據(jù)dump到文件上,然后再同步到slave結(jié)點中;
- AOF日志重寫:使用AOF持久化方式,做AOF文件重寫操作會創(chuàng)建新的進程做重寫;(重寫并不會去讀已有的文件,而是直接使用內(nèi)存中的數(shù)據(jù)寫成歸檔日志);
解決方案:
為了應(yīng)對大內(nèi)存頁表復(fù)制時帶來的影響,有些可用的措施:
- 控制每個redis實例的最大內(nèi)存量;
- 不讓fork帶來的限制太多,可以從內(nèi)存量上控制fork的時延;
- 一般建議不超過20G,可根據(jù)自己服務(wù)器的性能來確定(內(nèi)存越大,持久化的時間越長,復(fù)制頁表的時間越長,對事件循環(huán)的阻塞就延長)
- 新浪微博給的建議是不超過20G,而我們虛機上的測試,要想保證應(yīng)用毛刺不明顯,可能得在10G以下;
- 使用大內(nèi)存頁,默認內(nèi)存頁使用4KB,這樣,當(dāng)使用40G的內(nèi)存時,頁表就有80M;而將每個內(nèi)存頁擴大到4M,頁表就只有80K;這樣復(fù)制頁表幾乎沒有阻塞,同時也會提高快速頁表緩沖TLB(translation lookaside buffer)的命中率;但大內(nèi)存頁也有問題,在寫時復(fù)制時,只要一個頁快中任何一個元素被修改,這個頁塊都需要復(fù)制一份(COW機制的粒度是頁面),這樣在寫時復(fù)制期間,會耗用更多的內(nèi)存空間;
- 使用物理機;
- 如果有的選,物理機當(dāng)然是最佳方案,比上面都要省事;
- 當(dāng)然,虛擬化實現(xiàn)也有多種,除了Xen系統(tǒng)外,現(xiàn)代的硬件大部分都可以快速的復(fù)制頁表;
- 但公司的虛擬化一般是成套上線的,不會因為我們個別服務(wù)器的原因而變更,如果面對的只有Xen,只能想想如何用好它;
- 杜絕新進程的產(chǎn)生,不使用持久化,不在主結(jié)點上提供查詢;實現(xiàn)起來有以下方案:
- 1) 只用單機,不開持久化,不掛slave結(jié)點。這樣最簡單,不會有新進程的產(chǎn)生;但這樣的方案只適合緩存;
- 如何來做這個方案的高可用?
- 要做高可用,可以在寫redis的前端掛上一個消息隊列,在消息隊列中使用pub-sub來做分發(fā),保證每個寫操作至少落到2個結(jié)點上;因為所有結(jié)點的數(shù)據(jù)相同,只需要用一個結(jié)點做持久化,這個結(jié)點對外不提供查詢;
- 2) master-slave:在主結(jié)點上開持久化,主結(jié)點不對外提供查詢,查詢由slave結(jié)點提供,從結(jié)點不提供持久化;這樣,所有的fork耗時的操作都在主結(jié)點上,而查詢請求由slave結(jié)點提供;
- 這個方案的問題是主結(jié)點壞了之后如何處理?
- 簡單的實現(xiàn)方案是主不具有可替代性,壞了之后,redis集群對外就只能提供讀,而無法更新;待主結(jié)點啟動后,再繼續(xù)更新操作;對于之前的更新操作,可以用MQ緩存起來,等主結(jié)點起來之后消化掉故障期間的寫請求;
- 如果使用官方的Sentinel將從升級為主,整體實現(xiàn)就相對復(fù)雜了;需要更改可用從的ip配置,將其從可查詢結(jié)點中剔除,讓前端的查詢負載不再落在新主上;然后,才能放開sentinel的切換操作,這個前后關(guān)系需要保證;
持久化造成的阻塞
執(zhí)行持久化(AOF / RDB snapshot)對系統(tǒng)性能有較大影響,特別是服務(wù)器結(jié)點上還有其它讀寫磁盤的操作時(比如,應(yīng)用服務(wù)和redis服務(wù)部署在相同結(jié)點上,應(yīng)用服務(wù)實時記錄進出報日志);應(yīng)盡可能避免在IO已經(jīng)繁重的結(jié)點上開Redis持久化;
子進程持久化時,子進程的write和主進程的fsync沖突造成阻塞
在開啟了AOF持久化的結(jié)點上,當(dāng)子進程執(zhí)行AOF重寫或者RDB持久化時,出現(xiàn)了Redis查詢卡頓甚至長時間阻塞的問題, 此時, Redis無法提供任何讀寫操作;
原因分析:
Redis 服務(wù)設(shè)置了 Appendfsync everysec, 主進程每秒鐘便會調(diào)用 fsync(), 要求內(nèi)核將數(shù)據(jù)”確實”寫到存儲硬件里. 但由于服務(wù)器正在進行大量IO操作, 導(dǎo)致主進程 fsync()/操作被阻塞, 最終導(dǎo)致 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.
當(dāng)執(zhí)行AOF重寫時會有大量IO,這在某些Linux配置下會造成主進程fsync阻塞;
解決方案:
設(shè)置 no-appendfsync-on-rewrite yes, 在子進程執(zhí)行AOF重寫時, 主進程不調(diào)用fsync()操作;注意, 即使進程不調(diào)用 fsync(), 系統(tǒng)內(nèi)核也會根據(jù)自己的算法在適當(dāng)?shù)臅r機將數(shù)據(jù)寫到硬盤(Linux 默認最長不超過 30 秒).
這個設(shè)置帶來的問題是當(dāng)出現(xiàn)故障時,最長可能丟失超過30秒的數(shù)據(jù),而不再是1秒;
子進程AOF重寫時,系統(tǒng)的sync造成主進程的write阻塞
我們來梳理下:
1) 起因:有大量IO操作write(2) 但未主動調(diào)用同步操作
2) 造成kernel buffer中有大量臟數(shù)據(jù)
3) 系統(tǒng)同步時,sync的同步時間過長
4) 造成redis的寫aof日志write(2)操作阻塞;
5) 造成單線程的redis的下一個事件無法處理,整個redis阻塞(redis的事件處理是在一個線程中進行,其中寫aof日志的write(2)是同步阻塞模式調(diào)用,與網(wǎng)絡(luò)的非阻塞write(2)要區(qū)分開來)
產(chǎn)生1)的原因:這是redis2.6.12之前的問題,AOF rewrite時一直埋頭的調(diào)用write(2),由系統(tǒng)自己去觸發(fā)sync。
另外的原因:系統(tǒng)IO繁忙,比如有別的應(yīng)用在寫盤;
解決方案:
控制系統(tǒng)sync調(diào)用的時間;需要同步的數(shù)據(jù)多時,耗時就長;縮小這個耗時,控制每次同步的數(shù)據(jù)量;通過配置按比例(vm.dirty_background_ratio)或按值(vm.dirty_bytes)設(shè)置sync的調(diào)用閾值;(一般設(shè)置為32M同步一次)
2.6.12以后,AOF rewrite 32M時會主動調(diào)用fdatasync;
另外,Redis當(dāng)發(fā)現(xiàn)當(dāng)前正在寫的文件有在執(zhí)行fdatasync(2)時,就先不調(diào)用write(2),只存在cache里,免得被block。但如果已經(jīng)超過兩秒都還是這個樣子,則會強行執(zhí)行write(2),即使redis會被block住。
AOF重寫完成后合并數(shù)據(jù)時造成的阻塞
在bgrewriteaof過程中,所有新來的寫入請求依然會被寫入舊的AOF文件,同時放到AOF buffer中,當(dāng)rewrite完成后,會在主線程把這部分內(nèi)容合并到臨時文件中之后才rename成新的AOF文件,所以rewrite過程中會不斷打印"Background AOF buffer size: 80 MB, Background AOF buffer size: 180 MB",要監(jiān)控這部分的日志。這個合并的過程是阻塞的,如果產(chǎn)生了280MB的buffer,在100MB/s的傳統(tǒng)硬盤上,Redis就要阻塞2.8秒;
解決方案:
將硬盤設(shè)置的足夠大,將AOF重寫的閾值調(diào)高,保證高峰期間不會觸發(fā)重寫操作;在閑時使用crontab 調(diào)用AOF重寫命令;
參考:
http://www.oschina.net/translate/redis-latency-problems-troubleshooting
https://github.com/springside/springside4/wiki/redis
原文地址:https://www.cnblogs.com/me115/p/5032177.html
JAVA編程技術(shù)樂園:一個分享編程知識。跟著老司機一起學(xué)習(xí)干貨技術(shù)知識,每天進步一點點,讓小的積累,帶來大的改變!
歡迎關(guān)注!持續(xù)推送有趣有料的技術(shù)文章~






