Redis VM 相關闡述

redis官網對於棄用VM的描述 English
下面是谷歌翻譯後內容,肯定有出入,請高手指教。

更新:自從Redis 2.6以後,虛擬內存被棄用了,所以這裏的文檔只是出於歷史的原因。

虛擬內存技術規範

本文檔詳細介紹了Redis虛擬內存子系統的內部結構。目標用戶不是最終用戶,而是願意理解或修改虛擬內存實現的程序員。

鍵與值:什麼是換出?
VM子系統的目標是釋放將Redis對象從內存傳輸到磁盤的內存。這是一個非常通用的命令,但具體而言,Redis僅傳輸與值關聯的對象。爲了更好地理解這個概念,我們將使用DEBUG命令顯示從Redis內部的角度來看,一個保存值的鍵是如何看起來的:
**這裏寫圖片描述**
從上面的輸出中可以看出,Redis頂級散列表將Redis對象(鍵)映射到其他Redis對象(值)。虛擬內存只能交換磁盤上的值,與密鑰關聯的對象總是被佔用在內存中:這種交換保證了非常好的查找​​性能,因爲Redis VM的主要設計目標之一是具有與Redis類似的性能當經常使用的數據集的部分適合RAM時禁用VM。

交換值在內部看起來如何
當一個對象被換出時,這就是哈希表項中發生的事情:

  • 密鑰繼續保持代表密鑰的Redis對象。
  • 該值設置爲NULL

所以你可能想知道我們在哪裏存儲給定的值(與給定的鍵相關聯)被換出的信息。只是在關鍵的對象!
這就是Redis Object結構robj的外觀:
這裏寫圖片描述
正如你所看到的,有幾個關於VM的領域。最重要的是存儲,可以是這個價值之一:

  • REDISVMMEMORY:關聯的值在內存中。
  • REDISVMSWAPPED:關聯的值被交換,並且哈希表值項被設置爲NULL。
  • REDISVMLOADING:該值在磁盤上交換,條目爲NULL,但有一個作業將對象從交換加載到內存(此字段僅在線程VM處於活動狀態時使用)。
  • REDISVMSWAPPING:該值在內存中,該條目是一個指向實際Redis對象的指針,但是有一個I / O作業將這個值傳送到交換文件。

如果一個對象在磁盤上交換(REDISVMSWAPPED或REDISVMLOADING),我們怎麼知道它存儲在哪裏,它是什麼類型等等?這很簡單:vtype字段設置爲交換的Redis對象的原始類型,而vm字段(即redisObjectVM結構)保存關於對象位置的信息。這是這個額外結構的定義:
這裏寫圖片描述
正如你所看到的,結構包含了交換文件中對象所在的頁面,使用的頁面數量和對象的最後訪問時間(這對於選擇什麼對象是一個好候選的算法是非常有用的用於交換,因爲我們想要在磁盤上傳輸很少訪問的對象)。
正如你所看到的,雖然所有其他字段在舊的Redis對象結構中使用未使用的字節(由於自然內存對齊問題,我們有一些空閒位),但是vm字段是新的,並且實際上使用了額外的內存。即使VM被禁用,我們是否應該支付這樣的內存成本?沒有!這是創建一個新的Redis對象的代碼:
這裏寫圖片描述
正如你可以看到,如果虛擬機系統沒有啓用,我們只分配內存的sizeof(* o)-sizeof(struct redisObjectVM)。假設vm字段是對象結構中的最後一個字段,並且在禁用VM的情況下永遠不會訪問這些字段,所以我們是安全的,沒有VM的Redis不會支付內存開銷。

交換文件
下一步瞭解虛擬機子系統的工作原理是理解對象如何存儲在交換文件中。好消息是,這不是某種特殊格式,我們只是使用相同格式來存儲.rdb文件中的對象,這是Redis使用SAVE命令生成的常用轉儲文件。
交換文件由給定數量的頁面組成,其中每個頁面大小是給定的字節數。這個參數可以在redis.conf中修改,因爲不同的Redis實例可能對不同的值有更好的效果:它取決於你存儲在裏面的實際數據。以下是默認值:
vm-page-size 32
vm-pages 134217728
Redis在內存中佔用了一個“位圖”(一個連續的位數組,設置爲0或1),每一位代表磁盤上的交換文件的頁面:如果給定位設置爲1,則表示已經使用的頁面(有一些Redis對象存儲在那裏),而如果相應的位是零,頁面是空閒的。
在內存中使用這個位圖(這將調用頁表)在性能方面是一個巨大的勝利,而且使用的內存很小:我們只需要1位磁盤上的每個頁面。例如,在下面的例子中,每個32字節的134217728頁(4GB交換文件)僅使用16 MB的RAM作爲頁表。

將內存中的對象傳輸到交換
爲了將一個對象從內存傳遞到磁盤,我們需要執行以下步驟(假設非線程虛擬機,只是一個簡單的阻塞方法):

  • 查找需要多少頁才能將此對象存儲在交換文件上。這只是簡單地完成調用函數rdbSavedObjectPages返回磁盤上的對象使用的頁面的數量。請注意,這個函數不會複製.rdb保存代碼,只是爲了理解在磁盤上保存一個對象之後的長度,我們使用打開/dev /null並在那裏寫對象的技巧,最後按順序調用ftello檢查所需的字節數量。我們所做的基本上是將對象保存在虛擬的非常快的文件上,即/dev / null。
  • 現在我們知道了交換文件中需要多少頁面,我們需要在交換文件中找到這個數量的連續空閒頁面。這個任務是由vmFindContiguousPages函數完成的。正如你所猜測的,如果交換已滿,這個函數可能會失敗,或者如此分散,以至於我們無法輕易找到所需數量的連續空閒頁面。當發生這種情況時,我們只是放棄對象的交換,這將繼續存在於內存中。
  • 最後,我們可以將對象寫在磁盤上的指定位置,只需調用函數vmWriteObjectOnSwap即可。

正如你可以猜測,一旦對象被正確寫入交換文件,它將從內存中被釋放,關聯的鍵中的存儲字段被設置爲REDISVMSWAPPED,並且所使用的頁面被標記爲在頁表中被使用。

將對象加載回內存
從交換到內存加載一個對象更簡單,因爲我們已經知道對象的位置以及它使用了多少頁面。我們也知道對象的類型(加載函數需要知道這個信息,因爲在磁盤上沒有標題或任何其他關於對象類型的信息),但是它被存儲在相關的鍵的vtype字段中見上面。
調用函數vmLoadObject傳遞與我們想要加載的值對象關聯的鍵對象就足夠了。該函數還將負責修復鍵的存儲類型(這將是REDISVMMEMORY),將頁面標記爲在頁表中釋放,等等。
函數的返回值是加載的Redis對象本身,我們將不得不在主哈希表中重新設置值(而不是在值最初被換出時替換爲對象指針的NULL值) 。

如何阻止虛擬機工作
現在我們有了所有的構建塊來描述阻塞虛擬機如何工作。首先,關於配置的一個重要細節。爲了在Redis server.vm_max_threads中啓用阻塞VM,必須將其設置爲零。稍後我們將會看到在線程化虛擬機中如何使用這個最大數量的線程信息,因爲現在所需要的就是當Redis設置爲零時,Redis將恢復到完全阻止虛擬機。
我們還需要引入另一個重要的VM參數,即server.vm_max_memory。該參數非常重要,因爲它用於觸發交換:Redis將嘗試僅在交換對象時使用比最大內存設置更多的內存,否則不需要交換,因爲我們正在匹配用戶請求的內存使用情況。

阻止VM交換
對象從內存交換到磁盤發生在cron函數中。這個函數每秒調用一次,而最近在git上的Redis版本中每100毫秒調用一次(即每秒10次)。如果這個函數檢測到我們內存不足,也就是說,使用的內存大於vm-max-memory設置,它開始在調用函數vmSwapOneObect的循環中將對象從內存傳輸到磁盤。這個函數只需要一個參數,如果是0,它將以阻塞方式交換對象,否則如果是1,則使用I / O線程。在阻塞的情況下,我們只需要以零作爲參數來調用它。
vmSwapOneObject執行以下步驟:

  • 在檢查的關鍵空間,以找到一個很好的候選人交換(我們稍後會看到一個很好的候選人交換是)。
  • 關聯的值以阻塞的方式傳輸到磁盤。
  • 密鑰存儲字段設置爲REDISVMSWAPPED,而對象的vm字段設置爲正確的值(交換對象的頁面索引以及用於交換對象的頁面數量)。
  • 最後,值對象被釋放,並且哈希表的值條目被設置爲NULL。

該函數被一次又一次地調用,直到發生以下情況之一:沒有辦法交換更多的對象,因爲交換文件已滿或者幾乎所有的對象已經在磁盤上傳輸,或者只是內存使用已經在虛擬機最大內存參數。

內存不足時需要交換什麼值?
瞭解什麼是交換的好選擇並不難。隨機抽取幾個對象,並且每個對象的可交換性都被換算爲:
swappability = age*log(size_in_memory)
age是密鑰未被請求的秒數,而sizeinmemory是對象在內存中使用的內存量(以字節爲單位)的快速估計。所以我們試圖換出很少被訪問的對象,並且我們試圖將較大的對象換成較小的對象,但是後者是不太重要的因素(因爲使用了對數函數)。這是因爲我們不希望更大的對象被換出,而且爲了轉移它而需要的I / O和CPU越多,對象越大。

阻止VM加載
如果請求對與被換出的對象關聯的鍵的操作會發生什麼?例如,Redis可能恰好處理以下命令:
GET foo
如果foo鍵的值對象被交換了,我們需要在處理操作之前把它加載回內存。在Redis中,關鍵查找過程集中在lookupKeyRead和lookupKeyWrite函數中,這兩個函數用於實現訪問密鑰空間的所有Redis命令,所以我們在代碼中有一個單獨的點來處理從交換文件到內存。
所以這是發生了什麼事情:

  • 用戶調用一些具有交換鍵參數的命令
  • 命令實現調用查找函數
  • 查找功能搜索頂級散列表中的鍵。如果與所請求的鍵相關聯的值被交換(我們可以看到檢查鍵對象的存儲字段),我們在返回給用戶之前以阻塞的方式將其加載回內存中。

這是非常簡單的,但事情會變得更有趣的線程。從阻塞虛擬機的角度來看,唯一真正的問題是使用另一個進程保存數據集,即處理BGSAVE和BGREWRITEAOF命令。

當VM處於活動狀態時保存背景
在磁盤上保留的默認Redis方式是使用子進程創建.rdb文件。 Redis調用fork()系統調用來創建一個具有內存數據集的精確副本的子對象,因爲fork重複了整個程序內存空間(實際上歸功於一個稱爲Copy on Write內存頁的技術在父進程和子進程,所以fork()調用將不需要太多的內存)。
在子進程中,我們有一個在給定時間點的數據集的副本。其他由客戶端發出的命令將僅由父進程提供服務,不會修改子數據。
子進程只會將整個數據集存儲到dump.rdb文件中,最後退出。但是當VM處於活動狀態時會發生什麼?值可以換出,所以我們沒有在內存中的所有數據,我們需要訪問交換文件,以檢索交換值。雖然子進程正在保存交換文件在父進程和子進程之間共享,因爲:

  • 父進程需要訪問交換文件,以便在執行對換出的值的操作時將值加載回內存。
  • 子進程需要訪問交換文件以檢索完整的數據集,同時將數據集保存在磁盤上。

爲了避免兩個進程訪問同一個交換文件時出現問題,我們做了一件簡單的事情,那就是在後臺保存過程中,不允許在父進程中將值交換出去。這樣,這兩個進程將以只讀方式訪問交換文件。這種方法存在的問題是,儘管子進程正在保存,但是即使Redis使用的內存大於最大內存參數指定的內存,也不能在交換文件上傳輸新值。這通常不是一個問題,因爲後臺保存將在很短的時間內終止,如果仍然需要一定百分比的值將在磁盤上儘快交換。
此方案的一種替代方法是啓用只有在使用BGREWRITEAOF命令執行日誌重寫時纔會出現此問題的“附加文件”。

阻塞虛擬機的問題
阻塞虛擬機的問題在於:阻塞:)在批處理活動中使用Redis時,這不是問題,但對於實時使用,Redis的一個優點是低延遲。當客戶端正在訪問換出的值,或者Redis需要換出值時,阻塞虛擬機的延遲行爲將會變得很糟糕,同時其他客戶端也不會被服務。
換出鑰匙應該在後臺進行。同樣,當客戶端正在訪問換出的值時,其他訪問內存值的客戶端應該大致與VM被禁用時一樣快。只有處理換出密鑰的客戶應該被延遲。
所有這些限制都要求實現一個無阻塞的虛擬機。

線程機制的虛擬機
基本上有三種主要方法可以將阻塞虛擬機變成非阻塞虛擬機。 * 1:一種方法是顯而易見的,在我看來,根本不是一個好主意,就是把Redis本身變成一個線程服務器:如果每個請求都由另一個線程自動服務,其他客戶端不需要等待被阻擋的Redis速度很快,導出原子操作,沒有鎖,只有一千行代碼,因爲它是單線程的,所以這不是我的選擇。 * 2:對交換文件使用非阻塞I / O。畢竟,你可以認爲Redis已經是基於事件循環的,爲什麼不以非阻塞的方式處理磁盤I / O呢?由於兩個主要原因,我也拋棄了這種可能性。一個是非阻塞文件操作,不像套接字,是一個不兼容的噩夢。這不僅僅是調用select,你需要使用特定於操作系統的東西。另一個問題是I / O只是處理虛擬機所花費的時間的一部分,另一個重要部分是用於對交換文件進行編碼/解碼數據的CPU。這是我選擇的選項三,即… * 3:使用I / O線程,即處理交換I / O操作的線程池。這是Redis虛擬機正在使用的,所以我們來詳細說明這是如何工作的。

I / O線程
線程化虛擬機的設計目標在以下幾個方面,按重要程度排列:

  • 簡單的實現,競爭條件的小空間,簡單的鎖定,虛擬機系統或多或少地完全脫離其餘的Redis代碼。
  • 良好的性能,客戶端訪問內存中的值沒有鎖。
  • 能夠在I / O線程中解碼/編碼對象。

上述目標導致Redis主線程(服務於實際客戶端的線程)和I / O線程使用作業隊列與單個互斥體進行通信。基本上,當主線程需要某些I / O線程在後臺完成一些工作時,它會在server.io_newjobs隊列中(即,僅鏈接列表)推送一個I / O作業結構。如果沒有活動的I / O線程,則啓動一個線程。此時某些I / O線程將處理I / O作業,並將處理結果壓入server.io_processed隊列。 I / O線程將使用UNIX管道向主線程發送一個字節,以表示已經處理了新的作業,並且結果已準備好進行處理。
這就是iojob結構的樣子:
這裏寫圖片描述
I / O線程只能執行三種類型的作業(類型由結構的類型字段指定):

  • REDISIOJOBLOAD:將與給定密鑰關聯的值從交換加載到內存。交換文件內的對象偏移量是頁面,對象類型是key->
    vtype。此操作的結果將填充結構的val字段。
  • REDISIOJOBPREPARE_SWAP:計算爲了將val指向的對象保存到交換中所需的頁數。此操作的結果將填充頁面字段。
  • REDISIOJOBDO_SWAP:將val指向的對象轉移到頁面偏移頁面的交換文件。

主線程委託上述三個任務。其餘的都由主線程自己處理,例如在交換文件頁表中找到適當範圍的空閒頁面(這是一個快速操作),決定交換哪個對象,改變Redis對象的存儲字段以反映一個值的當前狀態。

非阻塞虛擬機作爲阻塞虛擬機的概率增強
所以現在我們有辦法請求處理慢VM操作的後臺作業。如何將其添加到由主線程完成的其餘工作的混合?雖然阻止虛擬機意識到一個對象被換出來只是當對象被查找時,這對我們來說已經太遲了:在C中,在命令中間啓動一個後臺作業並不重要,離開該函數並重新在I / O線程完成我們所要求的(也就是說,沒有協同程序或延續或類似的)的情況下輸入計算。
幸運的是,這樣做有很多簡單的方法。我們喜歡簡單的東西:基本上把虛擬機的實現看成一個阻塞的虛擬機,但是增加一個優化(使用非阻塞虛擬機的操作,我們可以執行),使得阻塞的可能性很小。
這就是我們所做的:

  • 每次客戶端向我們發送命令時,在命令執行之前,我們檢查命令的參數向量以搜索交換的密鑰。畢竟我們知道每個命令的參數是關鍵,因爲Redis命令格式非常簡單。
  • 如果我們檢測到請求的命令中至少有一個密鑰在磁盤上被交換,我們將阻塞客戶端,而不是真的發出命令。對於與所請求的鍵關聯的每個交換值,都會創建一個I/ O作業,以便將這些值返回到內存中。主線程繼續執行事件循環,而不關心被阻塞的客戶端。
  • 同時,I / O線程正在內存中加載值。每次I / O線程完成一個值的加載,它都會使用一個UNIX管道向主線程發送一個字節。管道文件描述符具有在主線程事件循環中相關的可讀事件,即函數vmThreadedIOCompletedJob。如果此函數檢測到阻塞客戶端所需的所有值都已加載,則客戶端將重新啓動並調用原始命令。

所以你可以把它看作是一個阻塞的虛擬機,它幾乎總是在內存中有正確的密鑰,因爲我們暫停了將要發出有關換出值的命令的客戶機,直到這個值被加載。
如果檢查什麼參數是一個鍵的函數以某種方式失敗,那麼沒有任何問題:查找函數將看到一個給定的鍵與一個換出的值相關聯,並將阻止加載它。所以當我們無法預測哪些鍵被觸摸的時候,我們的非阻塞虛擬機會回覆到阻塞狀態。
例如,在SORT命令與GET或BY選項一起使用的情況下,預先知道要請求的密鑰並不是微不足道的,所以至少在第一個實現中,SORT BY / GET使用阻塞的VM實現。

在交換的密鑰上阻塞客戶端
如何阻止客戶?暫停基於事件循環的服務器中的客戶端是相當簡單的。我們所做的只是取消它的讀取處理程序。有時候我們會做一些不同的事情(例如BLPOP),只是將客戶端標記爲阻塞,而不處理新的數據(只是將新數據累加到輸入緩衝區中)。

中止I / O作業
關於我們的阻塞和非阻塞虛擬機之間的交互有一些難以解決的問題,也就是說,如果一個阻塞操作從一個非阻塞操作同時“感興趣”的密鑰開始,會發生什麼?
例如,當執行SORT BY時,通過排序命令以阻塞方式加載幾個鍵。同時,另一個客戶端可能會用簡單的GET密鑰命令請求相同的密鑰,這將觸發創建I / O作業以在後臺加載密鑰。
處理這個問題的唯一簡單方法是能夠終止主線程中的I / O作業,以便如果我們想要以阻塞方式加載或交換的密鑰處於REDISVMLOADING或REDISVMSWAPPING狀態(即,關於這個鍵有一個I / O的工作),我們可以殺死關於這個鍵的I / O作業,然後繼續我們要執行的阻塞操作。
這並不像現在這樣微不足道。在某個特定時刻,I / O作業可能處於以下三個隊列之一:

  • server.io_newjobs:作業已經排隊,但沒有線程正在處理它。
  • server.io_processing:作業正在由I / O線程處理。
  • server.io_processed:作業已經被處理。能夠殺死一個I / O作業的函數是vmCancelThreadedIOJob,這就是它所做的。
  • 如果作業在newjobs隊列中,那很簡單,從隊列中移除iojob結構就足夠了,因爲沒有線程仍在執行任何操作。
  • 如果作業在處理隊列中,則線程正在搞亂我們的工作(可能還有相關的對象!)。我們唯一能做的就是等待物品以阻塞的方式移動到下一個隊列。幸運的是,這種情況很少發生,所以這不是一個性能問題。
  • 如果作業正在處理隊列中,我們將其標記爲取消標記,將iojob結構中的已取消字段設置爲1。函數處理完成的作業將被忽略並釋放該作業而不是真正處理作業。

面臨的問題
這個文檔是不完整的,唯一的方法就是閱讀源代碼,但是爲了使代碼的審查/理解變得簡單,應該是一個很好的介紹。
有什麼不清楚這個網頁?請留下評論,我會盡力解決這個問題可能整合在這個文件的答案。

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