Redis的快照為什么不會(huì)阻塞其他請(qǐng)求

今天就跟大家聊聊有關(guān)redis的快照為什么不會(huì)阻塞其他請(qǐng)求,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

創(chuàng)新互聯(lián)建站是網(wǎng)站建設(shè)技術(shù)企業(yè),為成都企業(yè)提供專業(yè)的成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì),網(wǎng)站設(shè)計(jì),網(wǎng)站制作,網(wǎng)站改版等技術(shù)服務(wù)。擁有10年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制適合企業(yè)的網(wǎng)站。10年品質(zhì),值得信賴!

為什么這么設(shè)計(jì)(Why's THE Design)是一系列關(guān)于計(jì)算機(jī)領(lǐng)域中程序設(shè)計(jì)決策的文章,我們?cè)谶@個(gè)系列的每一篇文章中都會(huì)提出一個(gè)具體的問題并從不同的角度討論這種設(shè)計(jì)的優(yōu)缺點(diǎn)、對(duì)具體實(shí)現(xiàn)造成的影響。如果你有想要了解的問題,可以在文章下面留言。

雖然我們經(jīng)常將 Redis 看做一個(gè)純內(nèi)存的鍵值存儲(chǔ)系統(tǒng),但是我們也會(huì)用到它的持久化功能,RDB 和 AOF 就是 Redis 為我們提供的兩種持久化工具,其中 RDB 就是 Redis 的數(shù)據(jù)快照,我們?cè)谶@篇文章想要分析 Redis 為什么在對(duì)數(shù)據(jù)進(jìn)行快照持久化時(shí)會(huì)需要使用子進(jìn)程,而不是將內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)直接導(dǎo)出到磁盤上進(jìn)行存儲(chǔ)。

概述

在具體分析今天的問題之前,我們首先需要了解 Redis 的持久化存儲(chǔ)機(jī)制 RDB 究竟是什么,RDB 會(huì)每隔一段時(shí)間中對(duì) Redis 服務(wù)中當(dāng)下的數(shù)據(jù)集進(jìn)行快照,除了 Redis 的配置文件可以對(duì)快照的間隔進(jìn)行設(shè)置之外,Redis 客戶端還同時(shí)提供兩個(gè)命令來生成 RDB 存儲(chǔ)文件,也就是 SAVE 和 BGSAVE,通過命令的名字我們就能猜出這兩個(gè)命令的區(qū)別。

Redis的快照為什么不會(huì)阻塞其他請(qǐng)求

其中 SAVE 命令在執(zhí)行時(shí)會(huì)直接阻塞當(dāng)前的線程,由于 Redis 是 單線程 的,所以 SAVE 命令會(huì)直接阻塞來自客戶端的所有其他請(qǐng)求,這在很多時(shí)候?qū)τ谛枰峁┹^強(qiáng)可用性保證的 Redis 服務(wù)都是無法接受的。

我們往往需要 BGSAVE 命令在后臺(tái)生成 Redis 全部數(shù)據(jù)對(duì)應(yīng)的 RDB 文件,當(dāng)我們使用 BGSAVE 命令時(shí),Redis 會(huì)立刻 fork 出一個(gè)子進(jìn)程,子進(jìn)程會(huì)執(zhí)行『將內(nèi)存中的數(shù)據(jù)以 RDB 格式保存到磁盤中』這一過程,而 Redis 服務(wù)在 BGSAVE 工作期間仍然可以處理來自客戶端的請(qǐng)求。

rdbSaveBackground 就是用來處理在后臺(tái)將數(shù)據(jù)保存到磁盤上的函數(shù):

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {      pid_t childpid;      if (hasActiveChildProcess()) return C_ERR;      ...      if ((childpid = redisFork()) == 0) {          int retval;          /* Child */          redisSetProcTitle("redis-rdb-bgsave");          retval = rdbSave(filename,rsi);          if (retval == C_OK) {              sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB");          }          exitFromChild((retval == C_OK) ? 0 : 1);      } else {          /* Parent */          ...      }      ...  }

Redis 服務(wù)器會(huì)在觸發(fā) BGSAVE 時(shí)調(diào)用 redisFork 函數(shù)來創(chuàng)建子進(jìn)程并調(diào)用 rdbSave 在子進(jìn)程中對(duì)數(shù)據(jù)進(jìn)行持久化,我們?cè)谶@里雖然省略了函數(shù)中的一些內(nèi)容,但是整體的結(jié)構(gòu)還是非常清晰的,感興趣的讀者可以在點(diǎn)擊上面的鏈接了解整個(gè)函數(shù)的實(shí)現(xiàn)。

使用 fork 的目的最終一定是為了不阻塞主進(jìn)程來提升 Redis 服務(wù)的可用性,但是到了這里我們其實(shí)能夠發(fā)現(xiàn)兩個(gè)問題:

  1.  為什么 fork 之后的子進(jìn)程能夠獲取父進(jìn)程內(nèi)存中的數(shù)據(jù)?

  2.  fork 函數(shù)是否會(huì)帶來額外的性能開銷,這些開銷我們?cè)趺礃硬趴梢员苊猓?/p>

既然 Redis 選擇使用了 fork 的方式來解決快照持久化的問題,那就說明這兩個(gè)問題已經(jīng)有了答案,首先 fork 之后的子進(jìn)程是可以獲取父進(jìn)程內(nèi)存中的數(shù)據(jù)的,而 fork 帶來的額外性能開銷相比阻塞主線程也一定是可以接受的,只有同時(shí)具備這兩點(diǎn),Redis 最終才會(huì)選擇這樣的方案。

設(shè)計(jì)

為了分析上一節(jié)提出的兩個(gè)問題,我們?cè)谶@里需要了解以下的這些內(nèi)容,這些內(nèi)容是 Redis 服務(wù)器使用 fork 函數(shù)的前提條件,也是最終促使它選擇這種實(shí)現(xiàn)方式的關(guān)鍵:

  1.  通過 fork 生成的父子進(jìn)程會(huì)共享包括內(nèi)存空間在內(nèi)的資源;

  2.  fork 函數(shù)并不會(huì)帶來明顯的性能開銷,尤其是對(duì)內(nèi)存進(jìn)行大量的拷貝,它能通過寫時(shí)拷貝將拷貝內(nèi)存這一工作推遲到真正需要的時(shí)候;

子進(jìn)程

在計(jì)算機(jī)編程領(lǐng)域,尤其是 Unix 和類 Unix 系統(tǒng)中,fork 都是一個(gè)進(jìn)程用于創(chuàng)建自己拷貝的操作,它往往都是被操作系統(tǒng)內(nèi)核實(shí)現(xiàn)的系統(tǒng)調(diào)用,也是操作系統(tǒng)在 *nix 系統(tǒng)中創(chuàng)建新進(jìn)程的主要方法。

Redis的快照為什么不會(huì)阻塞其他請(qǐng)求

當(dāng)程序調(diào)用了 fork 方法之后,我們就可以通過 fork 的返回值確定父子進(jìn)程,以此來執(zhí)行不同的操作:

  •  fork 函數(shù)返回 0 時(shí),意味著當(dāng)前進(jìn)程是子進(jìn)程;

  •  fork 函數(shù)返回非 0 時(shí),意味著當(dāng)前進(jìn)程是父進(jìn)程,返回值是子進(jìn)程的 pid; 

int main() {      if (fork() == 0) {          // child process      } else {          // parent process      }  }

在 fork 的 手冊(cè) 中,我們會(huì)發(fā)現(xiàn)調(diào)用 fork 后的父子進(jìn)程會(huì)運(yùn)行在不同的內(nèi)存空間中,當(dāng) fork 發(fā)生時(shí)兩者的內(nèi)存空間有著完全相同的內(nèi)容,對(duì)內(nèi)存的寫入和修改、文件的映射都是獨(dú)立的,兩個(gè)進(jìn)程不會(huì)相互影響。

The child process and the parent process run in separate memory spaces.  At the time of fork() both memory spaces have the same content.  Memory writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect other.

除此之外,子進(jìn)程幾乎是父進(jìn)程的完整副本(Exact duplicate),然而這兩個(gè)進(jìn)程在以下的一些方面會(huì)有較小的區(qū)別:

  •  子進(jìn)程用于獨(dú)立且唯一的進(jìn)程 ID;

  •  子進(jìn)程的父進(jìn)程 ID 與父進(jìn)程 ID 完全相同;

  •  子進(jìn)程不會(huì)繼承父進(jìn)程的內(nèi)存鎖;

  •  子進(jìn)程會(huì)重新設(shè)置進(jìn)程資源利用率和 CPU 計(jì)時(shí)器;

  •  ...

最關(guān)鍵的點(diǎn)在于父子進(jìn)程的內(nèi)存在 fork 時(shí)是完全相同的,在 fork 之后進(jìn)行寫入和修改也不會(huì)相互影響,這其實(shí)就完美的解決了快照這個(gè)場(chǎng)景的問題 —— 只需要某個(gè)時(shí)間點(diǎn)下內(nèi)存中的數(shù)據(jù),而父進(jìn)程可以繼續(xù)對(duì)自己的內(nèi)存進(jìn)行修改,這既不會(huì)被阻塞,也不會(huì)影響生成的快照。

寫時(shí)拷貝

既然父進(jìn)程和子進(jìn)程擁有完全相同的內(nèi)存空間并且兩者對(duì)內(nèi)存的寫入都不會(huì)相互影響,那么是否意味著子進(jìn)程在 fork 時(shí)需要對(duì)父進(jìn)程的內(nèi)存進(jìn)行全量的拷貝呢?假設(shè)子進(jìn)程需要對(duì)父進(jìn)程的內(nèi)存進(jìn)行拷貝,這對(duì)于 Redis 服務(wù)來說基本都是災(zāi)難性的,尤其是在以下的兩個(gè)場(chǎng)景中:

  1.  內(nèi)存中存儲(chǔ)大量的數(shù)據(jù),fork 時(shí)拷貝內(nèi)存空間會(huì)消耗大量的時(shí)間和資源,會(huì)導(dǎo)致程序一段時(shí)間的不可用;

  2.  Redis 占用了 10G 的內(nèi)存,而物理機(jī)或者虛擬機(jī)的資源上限只有 16G,在這時(shí)我們就無法對(duì) Redis 中的數(shù)據(jù)進(jìn)行持久化,也就是說 Redis 對(duì)機(jī)器上內(nèi)存資源的最大利用率不能超過 50%;

如果無法解決上面的兩個(gè)問題,使用 fork 來生成內(nèi)存鏡像的方式也無法真正落地,不是一個(gè)工程中真正可以使用的方法。

就算脫離了 Redis 的場(chǎng)景,fork 時(shí)全量拷貝內(nèi)存也是難以接受的,假設(shè)我們需要在命令行中執(zhí)行一個(gè)命令,我們需要先通過 fork 創(chuàng)建一個(gè)新的進(jìn)程再通過 exec 來執(zhí)行程序,fork 拷貝的大量?jī)?nèi)存空間對(duì)于子進(jìn)程來說可能完全沒有任何作用的,但是卻引入了巨大的額外開銷。

寫時(shí)拷貝(Copy-on-Write)的出現(xiàn)就是為了解決這一問題,就像我們?cè)谶@一節(jié)開頭介紹的,寫時(shí)拷貝的主要作用就是將拷貝推遲到寫操作真正發(fā)生時(shí),這也就避免了大量無意義的拷貝操作。在一些早期的 *nix 系統(tǒng)上,系統(tǒng)調(diào)用 fork 確實(shí)會(huì)立刻對(duì)父進(jìn)程的內(nèi)存空間進(jìn)行復(fù)制,但是在今天的多數(shù)系統(tǒng)中,fork 并不會(huì)立刻觸發(fā)這一過程:

Redis的快照為什么不會(huì)阻塞其他請(qǐng)求

在 fork 函數(shù)調(diào)用時(shí),父進(jìn)程和子進(jìn)程會(huì)被 Kernel 分配到不同的虛擬內(nèi)存空間中,所以在兩個(gè)進(jìn)程看來它們?cè)L問的是不同的內(nèi)存:

  •  在真正訪問虛擬內(nèi)存空間時(shí),Kernel 會(huì)將虛擬內(nèi)存映射到物理內(nèi)存上,所以父子進(jìn)程共享了物理上的內(nèi)存空間;

  •  當(dāng)父進(jìn)程或者子進(jìn)程對(duì)共享的內(nèi)存進(jìn)行修改時(shí),共享的內(nèi)存才會(huì)以頁為單位進(jìn)行拷貝,父進(jìn)程會(huì)保留原有的物理空間,而子進(jìn)程會(huì)使用拷貝后的新物理空間;

在 Redis 服務(wù)中,子進(jìn)程只會(huì)讀取共享內(nèi)存中的數(shù)據(jù),它并不會(huì)執(zhí)行任何寫操作,只有父進(jìn)程會(huì)在寫入時(shí)才會(huì)觸發(fā)這一機(jī)制,而對(duì)于大多數(shù)的 Redis 服務(wù)或者數(shù)據(jù)庫(kù),寫請(qǐng)求往往都是遠(yuǎn)小于讀請(qǐng)求的,所以使用 fork 加上寫時(shí)拷貝這一機(jī)制能夠帶來非常好的性能,也讓 BGSAVE 這一操作的實(shí)現(xiàn)變得非常簡(jiǎn)單。

總結(jié)

Redis 實(shí)現(xiàn)后臺(tái)快照的方式非常巧妙,通過操作系統(tǒng)提供的 fork 和寫時(shí)拷貝的特性輕而易舉的就實(shí)現(xiàn)了這個(gè)功能,從這里我們就能看出作者對(duì)于操作系統(tǒng)知識(shí)的掌握還是非常扎實(shí)的,大多人在面對(duì)類似的場(chǎng)景時(shí),想到的方法可能就是手動(dòng)實(shí)現(xiàn)類似『寫時(shí)拷貝』的特性,然而這不僅增加了工作量,還增加了程序出現(xiàn)問題的可能性。

到這里,我們簡(jiǎn)單總結(jié)一下 Redis 為什么在使用 RDB 進(jìn)行快照時(shí)會(huì)通過子進(jìn)程的方式進(jìn)行實(shí)現(xiàn):

  1.  通過 fork 創(chuàng)建的子進(jìn)程能夠獲得和父進(jìn)程完全相同的內(nèi)存空間,父進(jìn)程對(duì)內(nèi)存的修改對(duì)于子進(jìn)程是不可見的,兩者不會(huì)相互影響;

  2.  通過 fork 創(chuàng)建子進(jìn)程時(shí)不會(huì)立刻觸發(fā)大量?jī)?nèi)存的拷貝,內(nèi)存在被修改時(shí)會(huì)以頁為單位進(jìn)行拷貝,這也就避免了大量拷貝內(nèi)存而帶來的性能問題;

上述兩個(gè)原因中,一個(gè)為子進(jìn)程訪問父進(jìn)程提供了支撐,另一個(gè)為減少額外開銷做了支持,這兩者缺一不可,共同成為了 Redis 使用子進(jìn)程實(shí)現(xiàn)快照持久化的原因。到最后,我們還是來看一些比較開放的相關(guān)問題,有興趣的讀者可以仔細(xì)思考一下下面的問題:

  •     Nginx 的主進(jìn)程會(huì)在運(yùn)行時(shí) fork 一組子進(jìn)程,這些子進(jìn)程可以分別處理請(qǐng)求,還有哪些服務(wù)會(huì)使用這一特性?

  •     寫時(shí)拷貝其實(shí)是一個(gè)比較常見的機(jī)制,在 Redis 之外還有哪里會(huì)用到它?

看完上述內(nèi)容,你們對(duì)Redis的快照為什么不會(huì)阻塞其他請(qǐng)求有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。

網(wǎng)站欄目:Redis的快照為什么不會(huì)阻塞其他請(qǐng)求
轉(zhuǎn)載來于:http://muchs.cn/article30/ihcopo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站制作、App設(shè)計(jì)、網(wǎng)站排名電子商務(wù)、域名注冊(cè)、網(wǎng)站收錄

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站優(yōu)化排名