Redis 2.4:後臺線程如何解決aof缺陷?【轉】

轉自:http://tech.it168.com/a2011/1219/1290/000001290827.shtml

Redis終於在2.4版本里引入了除主線程之外的後臺線程,這個事情由來已久.早在2010年2月就有人提出aof的缺陷,提及的問題主要有:

  ① 主線程aof的每次fsync(everysecond模式)在高併發下時常出現100ms的延時,這源於fsync必不可少的磁盤操作,即便已經優化多次請求的離散小io轉化成一次大的連續io(sina的同學也反映過這個問題).

  ② 主線程裏backgroundRewriteDoneHandler函數在處理bgrewriteaof後臺進程退出的時候存在一個rename new-aof-file old-aof-file,然後再close old-aof-file的操作, close是一個unlink的操作(最後的引用計數), unlink消耗的時間取決於文件的大小,是個容易阻塞的系統調用.

  ③ 當發生bgsave或者bgrewriteaof的時候主線程和子進程同時寫入不同的文件,這改變了原有連續寫模式,不同寫入點造成了磁盤磁頭的尋道時間加長(其實一個臺物理機多實例也有這個問題, 要避免同一時間點做bgrewriteaof), 這又加長了fsync時間.

  經過漫長的設計和交流,antirez終於在2.4版裏給出了實現, 這個設計保持了Redis原有的keep it simple的風格,實現的特別簡單且有效果,實現的主要原理就是把fsync和close操作都移動到background來執行.

  後臺線程

  2.4.1版本引入新的文件bio.c,這個文件包含了後臺線程的業務邏輯.如圖.

  

  bioInit在Redis啓動的時候被調用,默認啓動2個後臺線程(如圖中的thread1,2),其一負責fsync fd的任務(解決缺陷1),其二負責close fd的任務(解決缺陷2).這兩個線程條件等待各自獨立的2個鏈表(close job,fsync job)上,看是否有新任務的加入,有則進行fsync或者close.

  解決問題1

  主線程僅僅把aofbuf的數據刷新到aof文件裏,然後通過bioCreateBackgroundJob函數往這隊列裏插入fsync job,於是原有主線程的fsync工作被轉移到後臺線程來做,這樣主線程阻塞問題就異步的解決了.

  但這又引發了一個問題,主線程對同一個fd如果有write操作,後臺線程同時在fsync,這兩個線程會互相影響. antirez爲此做了一定研究,並給出了簡單的解決方案.

  爲了避免線程的互相影響,主線程每次write之前都要檢測一下後臺線程任務隊列裏是否有fsync操作,如果有則延遲這次aofbuf的flush,延遲flush這個功能,當然會增大丟數據的可能,我們來看看實現.

aof.c
=======
78 void flushAppendOnlyFile(int force) {
     .......
84     if (server.appendfsync == APPENDFSYNC_EVERYSEC)
85         sync_in_progress = bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC) != 0;
86
87     if (server.appendfsync == APPENDFSYNC_EVERYSEC && !force) {
88         /* With this append fsync policy we do background fsyncing.
89          * If the fsync is still in progress we can try to delay
90          * the write for a couple of seconds.
*/
91         if (sync_in_progress) {
92             if (server.aof_flush_postponed_start == 0) {
93                 /* No previous write postponinig, remember that we are
94                  * postponing the flush and return.
*/
95                 server.aof_flush_postponed_start = server.unixtime;
96                 return;
97             } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
98                 /* We were already waiting for fsync to finish, but for less
99                  * than two seconds this is still ok. Postpone again.
*/
100                 return;
101             }
102             /* Otherwise fall trough, and go write since we can't wait
103              * over two seconds.
*/
104             redisLog(REDIS_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting        for fsync to complete, this may slow down Redis.");
105         }
106     }

   我們來解讀一下這段代碼, force這個參數如果爲1,則爲強制flush,爲0否則允許延遲flush.

  85行:這段就是判斷後臺線程是否有fsync任務,如果存在則會出現主線程write,後臺線程fsync的併發行爲.sync_in_process就表示存在衝突的可能性,則開始延遲flush.

  92行:如果當前未發生延遲,現在開始延遲flush,記錄一下時間就立即返回,這就發生了延遲flush,aofbuf裏的信息未被刷出去.

  97行:當再次進入該函數之後,如果距離開始延遲時間仍然小於2s,則允許繼續延遲.

  104行:距離開始延遲事件已經超過2s了,必須強制flush了,否則丟數據可能超過2s.

  解決了衝突之後就是加入後臺任務了,以前是fsync現在改成了加入隊列

aof.c
========
151     } else if ((server.appendfsync == APPENDFSYNC_EVERYSEC &&
152                 server.unixtime > server.lastfsync)) {
153         if (!sync_in_progress) aof_background_fsync(server.appendfd);
154         server.lastfsync = server.unixtime;
155     }

   好了缺陷1解決了.

解決缺陷2

  backgroundRewriteDoneHandler裏同樣的把close old-aof-file的工作交給backgroud thread來執行.

aof.c
=========
856 /* Asynchronously close the overwritten AOF. */
857 if (oldfd != -1) bioCreateBackgroundJob(REDIS_BIO_CLOSE_FILE,(void*)(long)oldfd,NULL

   這樣關閉old-aof-file的工作被移交到後臺任務執行,不再阻塞主線程了,不過沒那麼簡單,如下的特殊場景需要額外處理.

aof enabled
bgrewriteaof start
aof disbled
bgrewriteaof stop
bgrewriteaof handler

   在bgrewriteaof觸發之後,關閉了aof功能,這樣由於server.appendfd對應old-aof-file文件未被打開, 一旦rename new-aof old-aof, 則會觸發一個unlink old-aof-file的行爲, 而不是上面說的close才觸發unlink行爲.爲了跳過這種狀況,如果發現aof被關閉,通過打開old-aof-file文件增加引用計數的方法解決這個問題.

aof.c
==========
810         if (server.appendfd == -1) {
811             /* AOF disabled */
812
813              /* Don't care if this fails: oldfd will be -1 and we handle that.
814               * One notable case of -1 return is if the old file does
815               * not exist.
*/
816              oldfd = open(server.appendfilename,O_RDONLY|O_NONBLOCK);
817         } else {
818             /* AOF enabled */
819             oldfd = -1; /* We'll set this to the current AOF filedes later. */
820         }

   816行:如果處於aof關閉狀態,則打開old-aof-file.

  819行:aof已經是激活狀態,不做任何操作.

  這樣rename就不再引發unlink old-aof-file, 不會再阻塞主線程.

824         if (rename(tmpfile,server.appendfilename) == -1) {

   處理完rename之後就要來處理old-aof-file了.如果aof是非激活狀態,對於new-aof-file文件,我們關閉他即可不需要其它操作,這個close不會引發阻塞,因爲這個文件的已經在生成new-aof-file文件的時候做過fsync了.

  如果aof是激活狀態, fsync行爲遞給後臺去執行,這塊的行爲和缺陷1一樣.

aof.c
===========
840             if (server.appendfsync == APPENDFSYNC_ALWAYS)
841                 aof_fsync(newfd);
842             else if (server.appendfsync == APPENDFSYNC_EVERYSEC)
843                 aof_background_fsync(newfd);

   解決缺陷3

  引入了延遲bgrewriteaof來避免與bgsave同時寫文件,而server.no_appendfsync_on_rewrite參數的設置又避免了bgrewriteaof時主線程出現fsync.

  測試2.4.1的性能確實較之前版有較大的提升,以後會給出測試數據.


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