【譯】MinIO版本控制、元數據和存儲深入剖析

起初是來源於一個SUBNET問題。一位客戶觀察到他們的部署出現了性能衰減現象。經過一些調查,我們發現客戶運行的一個腳本是問題的原因。該腳本是一個同步腳本,每次運行之間有一分鐘的延遲,用於上傳目錄的內容。

由於客戶使用了服務器端複製,目標存儲桶啓用了版本控制。上傳腳本採用了一種簡單的方法,每次運行時都會上傳所有文件。爲了控制版本數量,客戶使用了自動對象過期規則,刪除所有早於一天的版本。

我們發現的問題是,即使文件很小,版本數量在生命週期運行之間累積的數量也會對系統造成重大負載。

雖然我們可以建議客戶快速解決問題,但我們將其視爲改進我們如何處理過多版本號的機會。

元數據結構

MinIO服務器沒有數據庫。這是早期做出的設計選擇,也是MinIO能夠在數千個服務器上以容錯的方式進行擴展的主要因素。MinIO不使用數據庫,而是使用一致性哈希和文件系統來存儲對象的所有信息和內容。

當我們實施版本控制時,我們調查了各種選項。在版本控制之前,我們將元數據存儲爲JSON格式。這使得我們可以直接查看元數據,從而更容易調試問題。然而,在我們的研究中,我們發現雖然這很方便,但通過切換到二進制格式,我們可以將磁盤使用量減少約50%,CPU使用量也減少了大約相同的比例。

我們決定選擇MessagePack作爲我們的序列化格式。這保持了JSON的可擴展性,允許添加/刪除鍵。最初的實現只是一個標題,後面跟着一個具有以下結構的MessagePack對象:

 
{
  "Versions": [
    {
      "Type": 0, // Type of version, object with data or delete marker.
      "V1Obj": { /* object data converted from previous versions */ },
      "V2Obj": {
          "VersionID": "",  // Version ID for delete marker
          "ModTime": "",    // Object delete marker modified time
          "PartNumbers": 0, // Part Numbers
          "PartETags": [],  // Part ETags
          "MetaSys": {}     // Custom metadata fields.
          // More metadata
      },
      "DelObj": {
          "VersionID": "", // Version ID for delete marker
          "ModTime": "",   // Object delete marker modified time
          "MetaSys": {}    // Delete marker metadata
      }
    }
  ]
}

先前版本的元數據在更新時被轉換,新版本則根據操作添加爲“V2Obj”或“DelObj”。

我們嘗試讓我們的基準測試在真實數據上運行,以便它們適用於實際使用。我們評估了讀寫多達10,000個版本(每個版本有10,000個分片)的時間,作爲我們的

最壞情況。基準測試顯示,解碼這需要約120毫秒。讀取的內存分配相當大,但由於大多數對象只有1個部分,因此10,000個對象代表了最壞情況,因此被認爲是可以接受的。

這是約一年的磁盤表示。內聯數據被添加到格式中,允許小對象的數據存儲在與元數據相同的文件中。但這並沒有改變元數據表示,因爲內聯數據存儲在元數據之後。

這意味着在只需要讀取元數據的情況下,我們可以在達到元數據結尾時停止讀取文件。這可以通過最多連續兩次讀取來實現。

對象版本

讓我們退後一步,看看對象版本控制作爲一個概念。簡而言之,它記錄對象的更改,並允許您回到過去。簡單的客戶端不需要擔心版本控制,只需在最新的對象版本上操作即可。

後端只需跟蹤上傳的版本即可。例如:

$ mc ls --versions play/test/ok.html
[2021-12-14 18:00:52 CET]  18KiB 83b0518c-9080-45bb-bfd3-3aecfc00e201 v6 PUT ok.html
[2021-12-14 18:00:43 CET]     0B ff1baa7d-3767-407a-b084-17c1b333ea87 v5 DEL ok.html
[2021-12-11 10:01:01 CET]  18KiB ff471de8-a96b-43c2-9553-8fc21853bf75 v4 PUT ok.html
[2021-12-11 10:00:21 CET]  18KiB d67b20e2-4138-4386-87ca-b37aa34c3b2d v3 PUT ok.html
[2021-12-11 10:00:11 CET]  18KiB 47a4981a-c01b-4c6a-9624-0fa44f61c5e9 v2 PUT ok.html
[2021-12-11 09:57:13 CET]  18KiB f1528d08-482d-4945-b8ee-e8bd4038769b v1 PUT ok.html

這裏顯示了6個ok.html版本的寫入和1個版本(v5)的刪除。元數據將跟蹤版本。

在最簡單的情況下,管理元數據純粹是追加更改的問題。然而,在現實中可能並不是那麼簡單。例如,當更改被寫入時,磁盤可能處於離線狀態。如果我們可以寫入足夠的磁盤,我們就接受這個更改,但這意味着當磁盤迴來時,它們將需要進行修復。複製和分層可能需要更新舊版本,標籤可以添加到版本中等等。

幾乎任何更新都意味着我們需要檢查版本是否仍然按正確順序排序。對象版本嚴格按“修改時間”排序,這意味着對象版本上傳的時間。對於上面的結構,這意味着我們需要加載所有版本才能訪問此信息。

當版本數量非常高時,初始設計開始顯示其侷限性。對於某些操作,我們需要所有版本信息都可用,有時需要超過1GB的內存才能完成。使用這麼多內存將對可以進行的併發操作數量產生很大限制,這當然是不可取的。

設計考量

起初,我們評估了將所有版本的元數據存儲在單個文件中的可行性。我們很快就拒絕了將單個版本存儲爲單個文件的方法。一個版本的元數據通常少於1KB,因此列出所有版本將導致隨機IO的爆炸。

列出單個版本還會返回版本計數和“後繼”修改時間,即任何更新版本的時間戳。因此,我們需要了解所有版本的信息,這意味着每個版本都有一個文件將對性能產生反作用。

我們考慮了一種日誌類型的方法,其中更改是附加而不是在每次更新時重寫元數據。雖然這可能對寫入有優勢,但在讀取時會帶來很多額外的處理。這不僅適用於單個讀取,而且還會顯著減慢列表速度。因此,這不是我們想要追求的方法。

相反,我們決定看看我們通常需要進行所有版本操作的信息以及我們需要哪些信息來處理單個版本。

大多數操作要麼操作“最新版本”,要麼操作特定版本。如果您進行GetObject調用,可以指定版本ID並獲取該版本,否則您的請求將被視爲針對最新版本。大多數操作都類似。唯一操作涉及所有版本的是ListObjectVersions調用,它返回對象的所有版本。

對象變異需要檢查現有版本ID和修改時間以進行排序。列表需要跨磁盤合併版本,因此需要能夠檢查元數據在各個磁盤上是否相同。

如果我們可以訪問此信息,則沒有任何操作需要一次性在內存中解壓縮所有版本元數據。對性能的影響是巨大的。

實現

實際上,我們決定進行一個相當小的更改,以實現所有這些改進。我們不再將所有版本完全解壓縮到內存中,而是改爲以下結構:

// xlMetaV2 contains all versions of an object.
type xlMetaV2 struct {
	// versions sorted by modification time,
	// most recent version first. 
	versions []xlMetaV2ShallowVersion
}

// xlMetaV2ShallowVersion contains metadata information about
// a single object version.
// metadata is serialized.
type xlMetaV2ShallowVersion struct {
	header xlMetaV2VersionHeader
	meta   []byte
}



// xlMetaV2VersionHeader contains basic information about an object version.
type xlMetaV2VersionHeader struct {
	VersionID [16]byte
	ModTime   int64
	Signature [4]byte
	Type      VersionType
	Flags     xlFlags
}

現在,我們仍然將所有版本“存儲在內存中”,但我們現在將每個版本的元數據保留爲序列化形式。實際上,這個序列化數據只是從磁盤加載的元數據的子切片。這意味着我們只需要爲所有版本分配一個固定大小的切片。爲了執行我們的操作,我們有一個帶有有限信息的頭文件,每個版本都有足夠的信息,我們永遠不需要掃描所有元數據。

磁盤上的表示也已更改以適應此。以前,所有元數據都存儲爲一個包含所有版本的大對象。現在,我們將其寫成這樣:

  • 帶版本的簽名
  • 頭數據的版本(整數)
  • 元數據的版本(整數)
  • 版本計數(整數)

讀取此頭文件允許我們在xlMetaV2實例中分配“versions”。由於xlMetaV2ShallowVersion中的所有字段大小都是固定的,因此這將是我們唯一需要的分配。

xl.meta 全部結構
 

對於每個版本,有兩個二進制數組,一個包含序列化的xlMetaV2VersionHeader,另一個包含完整的元數據。對於所有操作,我們只讀取頭文件並將序列化的完整元數據保留在內存中。在磁盤上,這每個版本增加了約30-40個字節,即使這是重複的數據,由於性能提升,這仍然是一個可以接受的折衷。

這意味着對於隻影響單個版本的常規變異,我們只需要反序列化該特定版本,應用變異並序列化該版本。同樣地,對於刪除和插入,我們永遠不必處理完整的元數據。

對象版本可以(重新)排序,而不移動更多的頭文件和序列化的元數據。此外,我們現在還保證保存的文件已經預先排序。這意味着我們現在可以快速識別最新版本,並且獲取“後繼”修改時間是微不足道的。

對於最常見的讀取操作,這意味着我們一旦找到所需內容就可以立即返回。列表現在可以一次反序列化一個版本-節省內存並提高性能。

 

升級

MinIO已經有數千個部署,存儲了數十億個對象,因此平穩升級非常重要。當更改對象存儲方式時,我們採用“不轉換”方法,即即使數據處於舊格式,我們也不會更改存儲的數據。對於許多對象,轉換所有對象將是一項耗時和資源消耗巨大的任務。

任務的一個重要部分是確保現有數據不會消耗大量資源進行讀取。我們不能接受升級會顯著降低性能。但我們不希望爲舊版本保留重複的代碼。現有數據通常最終需要進行轉換,我們希望儘早處理。

在這種情況下,我們能夠利用MessagePack的一些優勢。雖然我們仍然需要反序列化所有版本,但我們可以做一些技巧:

  • 查找“Versions”數組。
  • 讀取大小。 分配我們需要的[]xlMetaV2ShallowVersion。
  • 對於數組中的每個元素:
  • 反序列化到臨時位置
  • 基於反序列化的數據創建xlMetaV2VersionHeader。
  • 觀察反序列化時消耗的字節數。
  • 從序列化數據創建子切片。

這與以前的版本一樣快,因爲我們正在處理相同的數據。唯一的區別是我們一次只需要一個版本在內存中,這顯著減少了內存使用。

通過這個技巧,我們能夠以與以前相同的處理量加載現有元數據。如果需要將元數據寫回磁盤,則現在處於新格式,並且在下一次讀取時不需要進行轉換。

擴展性基準測試和分析

 

爲了評估擴展性,我們總是嘗試創建真實的負載,但也力求達到最壞的情況。在這種情況下,我們有一個明確的客戶案例,超過1000個版本開始導致性能問題。雖然我們會對系統的各個部分進行基準測試,但真正的測試始終是綜合端到端的測量。

爲了評估MinIO的擴展能力,我們使用了我們的Warp S3基準測試工具。Warp使得創建非常特定的負載成爲可能。對於這個測試,我們使用了put(PutObject)和stat(HeadObject)基準測試。我們創建了許多對象,每個對象都有不同數量的版本,並觀察我們能夠多快地從隨機對象/版本中獲取元數據。

我們在一個分佈式服務器上進行了測試,只使用了一個NVME。這些數字並不代表完整的MinIO集羣,而只代表單個服務器。不再拖延,讓我們看看這些數字:

 

垂直軸表示每秒提交到服務器的對象版本數量。請注意,每個樣本代表版本數量的數量級。請注意,更高的條形圖意味着更多的操作/秒,這意味着更高的性能。

分析這些數字,當版本數量爲10和100時,我們基本上看到相同的上傳速度。這是預期的,因爲我們沒有觀察到“正常”版本數量可能出現的問題。即便如此,輕微的加速也是好的觀察結果。

當版本數量爲1,000時,我們觀察到了我們調查的問題,並且我們很高興看到這種情況下的1.9倍加速。

當版本數量爲10,000時,出現了一個有趣的問題。此時,我們的服務器在NVME存儲上達到了IO飽和狀態,約爲1.5GB/s。這將瓶頸從系統的一個部分轉移到另一個部分,從內存轉移到存儲。添加對象的第10,000個版本所需的時間僅爲350毫秒。

查看讀取性能:

 
 
 

需要注意的第一件事是規模不同。我們正在處理超過一個數量級的更多對象。您可以看到一個明顯的好處是不必反序列化所有對象版本 - MinIO可以平穩地擴展,以提供最佳性能,無論版本化對象的數量如何。我們優化的結果是,對於具有1000個或更多版本的對象的讀取,在整體性能和系統響應性方面實現了3-4倍的速度提高。

請記住,這些測試是在單個服務器上進行的,使用單個NVME驅動器,不是最適合性能測試的系統。即使在這種配置下,我們每秒讀取大約180個對象版本信息,因此,雖然讀取10000個對象版本比讀取較少數量的對象要慢,但絕不會無響應。

結論和未來改進

我們的主要目標是減少多個版本的處理開銷。我們將內存使用量降低了幾個數量級,並提高了版本處理的速度。

解決問題的好處在於,總會有更多的挑戰需要解決。在這個迭代中,我們將可行版本數量增加了一個數量級。然而,在MinIO,我們從來不滿足於此——我們已經在研究進一步提高已經是世界上最快的分佈式對象存儲的性能和可擴展性。

我們可以觀察到,在某個點上,我們達到了IO限制,其中變異變得負擔重重,因爲總元數據大小。變異文件在千字節範圍內是可以接受的,但一旦達到兆字節範圍,對系統來說就變得很密集了。

現在我們有一個稱爲“XL”的單一數據格式。在未來,我們可能會研究將頭文件和完整元數據/內聯數據拆分爲兩個文件的選項。這對於少量版本來說是次優的,因爲IOPS很珍貴,但對於1000個以上的版本,這可能是我們可以採取的方法。讓我們稱之爲“XXL”格式。

控制系統使用的數據格式有很大的優勢。這種控制水平意味着您可以不斷改進系統,並確保您可以這樣做。在MinIO,我們的目標是不斷改進並幫助我們的客戶擴展。當然,我們可以簡單地告訴客戶,如果不擁有太多相同數據的版本,他們將獲得最佳性能,但這不是MinIO的方式。我們爲您做重活,讓MinIO的客戶可以專注於他們的應用程序,將備份存儲留給我們。

不要只聽廣告,看療效的話 - 下載MinIO,親自體驗差異。

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