分佈式存儲Weed-FS源碼分析

基於源碼版本號 0.67 , 【Weed-FS又名叫Seaweed-FS】。

Weed-FS 是一個非常優秀的由 golang 開發的分佈式存儲開源項目, 雖然在我剛開始關注的時候它在 github.com 上面只有 star 50+, 但是我覺得這個項目是一個幾千 star 量級的優秀開源項目。 Weed-FS 的設計原理是基於 Facebook 的一篇圖片存儲系統的論文 Facebook-Haystack, 論文很長,但是其實原理就幾句話,可以看看 Facebook圖片存儲系統Haystack , 我覺得Weed-FS是青出於藍而勝於藍。

Weed-FS 這個開源系統涵蓋的面比較多, 很難在一篇文章裏面說清楚, 只能儘可能清楚的說說主要的部分。

源碼目錄結構

核心模塊

  • weed 入口目錄
  • weed/weed_server 入口目錄與HTTP服務相關
  • topology 核心模塊,主要包括 【DataCenter, Rack, DataNode】 三層拓撲結構。
  • storage 核心模塊,主要包括【Store, Volume, Needle】這三大塊存儲相關的源碼。

輔助模塊

  • sequence 負責FileID的全局有序生成
  • filer 提供支持 HTTP REST 操作的文件服務器,其實就是基於 leveldb 把文件名和目錄結構存儲起來。
  • stats 和操作系統內存和磁盤使用狀況有關的模塊
  • operation 由protobuf生成的代碼們
  • proto 存放protobuf的描述文件
  • glog 日誌模塊
  • images 圖片服務
  • util 工具函數
  • tools 工具,暫時只有一個讀索引的文件。

多數據節點維護之 Topology

topology 整個模塊最核心的數據結構是三個:

  • DataCenter
  • Rack
  • DataNode

topology 是樹狀結構,DataNode 是樹的葉子節點, DataCenter 和 Rack 是樹的非葉子節點, DataCenter 是 Rack 的父母節點。 如下圖

            DataCenter
                |
                |
       ------------------
       |                |
       |                |
      Rack            Rack
       |
       |
   ------------
   |          |
   |          |
 DataNode  DataNode

也就是在 MasterServer 維護的拓撲結構裏, 是把 VolumeServer 的相關信息存儲在 DataNode 裏, 所以在代碼裏面可以看到如下:

dc := t.GetOrCreateDataCenter(dcName)
rack := dc.GetOrCreateRack(rackName)
dn := rack.FindDataNode(*joinMessage.Ip, int(*joinMessage.Port))

每次查找對應的DataNode,都需要從 DataCenter -> Rack -> DataNode 依次找下去。

數據存儲

理解Fid

curl -F "file=@/tmp/test.pdf" "127.0.0.1:9333/submit"
{"fid":"1,01f96b93eb","fileName":"test.pdf","fileUrl":"localhost:8081/1,01f96b93eb","size":548840}

其中 "fid":"1,01f96b93eb" 就是 Fid,Fid 由三個部分組成 【VolumeId, NeedleId, Cookie】 組成。

  • VolumeId: 1 32bit 存儲的物理卷的Id
  • NeedleId: 01 64bit 全局唯一NeedleId,每個存儲的文件都不一樣(除了互爲備份的)。
  • Cookie: f96b93eb 32bit Cookie值,爲了安全起見,防止惡意攻擊。

其中 VolumeId 是由 MasterServer 分配給 VolumeServer, 每個 VolumeServer 都維護個 n 個 Volume , 每個 Volume 都有一個專屬 VolumeId,之後會細說。 Needle 屬於 Volume 裏面的一個單元,後續說。

Volume

type Volume struct {
    Id         VolumeId
    dir        string
    Collection string
    dataFile   *os.File
    nm         NeedleMapper
    readOnly   bool

    SuperBlock

    accessLock       sync.Mutex
    lastModifiedTime uint64 //unix time in seconds
}
  • VolumeId 通俗易懂,比如 "fid":"3,01f9896771" 裏面逗號前面的 3 就是 VolumeId 。
  • dir 就是該 Volume 所在的目錄,
  • Collection 很有用,每個 Volume 只能對應同一個 Collection,不同 Collection 的圖片存儲在不同 Volume,後面會講到。
  • 所以同一個 Volume 只能針對某一個 Collection ,而 同一個 Collection 的圖片可能分佈在不同的 Volume。 dataFile 就是對應的文件句柄。
  • nm NeedleMapper 看上去像是個 map ,其實是個列表,包含多個 Needle ,後面會講到。
  • readOnly 是否只讀
  • SuperBlock 超塊,後面會講到。
  • accessLock 互斥鎖
  • lastModifiedTime 最近修改時間

以上最關鍵的兩個點就是 SuperBlock 和 NeedleMapper , 這兩者在文件中佈局如下:

+-------------+
|SuperBlock   |
+-------------+
|Needle1      |
+-------------+
|Needle2      |
+-------------+
|Needle3      |
+-------------+
|Needle ...   |
+-------------+
1 Volume = 1 SuperBlock + n Needle

SuperBlock

/*
* Super block currently has 8 bytes allocated for each volume.
* Byte 0: version, 1 or 2
* Byte 1: Replica Placement strategy, 000, 001, 002, 010, etc
* Byte 2 and byte 3: Time to live. See TTL for definition
* Rest bytes: Reserved
 */
type SuperBlock struct {
    version          Version
    ReplicaPlacement *ReplicaPlacement
    Ttl              *TTL
}

SuperBlock 內維護的數據基本上就是該 Volume 的元數據。

  • ReplicaPlacement : 在後面的 Replication 會講
  • Ttl :Time To Live 爲了定時刪除的功能

【TTL】

定時刪除功能,這個感覺很酷炫,但是在Weed-FS裏面的實現原理很簡單, 按 Volume 來進行分塊,當每次用戶上傳一個自帶TTL的文件(需要定時刪除的文件)時, 會把這個文件存儲在合適的 Volume 裏面(如何選出合適的 Volume 之後再說), 存儲的時候每個文件會帶有 TTL 這個屬性, 當讀取出來之後發現該文件已經過期(超時時間到),則會返回一個 Not Found 結果, 而每個 Volume 維護一個最大超時時間,當這個時間抵達時,說明整個 Volume 所有的文件都超時了, 然後 VolumeServer 通知 MasterServer 這個 Volume 已經被標識爲 Dead 狀態, 意味着 MasterServer 不會再爲這個 Volume 分配新的 Fid。 然後再經過一段合適的時間後由 VolumeServer 將這個 Volume 從磁盤上安全的刪除掉。 詳細請看在Weed-FS自帶的文檔ttl

Needle

/*
* A Needle means a uploaded and stored file.
* Needle file size is limited to 4GB for now.
 */
type Needle struct {
    Cookie uint32 `comment:"random number to mitigate brute force lookups"`
    Id     uint64 `comment:"needle id"`
    Size   uint32 `comment:"sum of DataSize,Data,NameSize,Name,MimeSize,Mime"`

    Data         []byte `comment:"The actual file data"`
    DataSize     uint32 `comment:"Data size"` //version2
    Flags        byte   `comment:"boolean flags"` //version2
    NameSize     uint8  //version2
    Name         []byte `comment:"maximum 256 characters"` //version2
    MimeSize     uint8  //version2
    Mime         []byte `comment:"maximum 256 characters"` //version2
    LastModified uint64 //only store LastModifiedBytesLength bytes, which is 5 bytes to disk
    Ttl          *TTL

    Checksum CRC    `comment:"CRC32 to check integrity"`
    Padding  []byte `comment:"Aligned to 8 bytes"`
}

Needle 結構體裏面的 Cookie 和 Id 就是上文提過的 Fid 裏面的 Cookie 和 NeedleId, 其他就是一些存儲相關的變量,沒什麼奇淫巧計。就是簡單的存儲結構而已。

數據備份之 Replication

Replication 和 Topology 嚴重相關, 在配置文件中可以配置多種備份模式,詳見 weed-fs-wiki 。

+-----+---------------------------------------------------------------------------+
|001  |replicate once on the same rack                                            |
+-----+---------------------------------------------------------------------------+
|010  |replicate once on a different rack in the same data center                 |
+-----+---------------------------------------------------------------------------+
|100  |replicate once on a different data center                                  |
+-----+---------------------------------------------------------------------------+
|200  |replicate twice on two other different data center                         |
+-----+---------------------------------------------------------------------------+

比如在 001 模式,即在同一個 rack 中的不同 DataNode 中備份一份。 假設在 rack1 中含有 DataNode1, DataNode2, DataNode3 三個數據節點中【隨機】選出兩個數據節點, 比如選出 DataNode1, DataNode2 然後同時寫入這兩個數據節點。 假設 rack1 只有一個數據節點的時候,而備份模式是 001 模式, 則無法正常備份,服務會報錯。

注意到,選擇備份數據節點的方法是【隨機】,所以就會出現從三個數據節點中隨機選擇兩個的情況下,

curl -v -F "file=@/tmp/test.json" localhost:8081/5,1ce2111f1

topo.NextVolumeId 負責生成 VolumeId , 負責在 VolumeGrowth 裏的時候分配 Volume 的時候, 生成一個全局唯一的新 VolumeId, 在 Weed-fs 中,是支持 多 MasterServer 集羣的。 當有多個 MasterServer,生成一個全局唯一的新 VolumeId 是很重要, 在 Weed-fs 中是通過goraft 來實現的。

【強一致性】

Weed-FS 的備份實現是強一致性的。 當一個 VolumeServer 接受到上傳文件的 POST 請求時, 將該文件作爲一個 Needle 寫入本地 Volume 之後, 會根據該文件所分配的 VolumeId 判斷是否需要備份, 如果需要備份,則進行備份(需要請求另外其它的 VolumeServer 服務器)。 過程詳見ReplicatedWrite (topology/store_replicate.go)。 當備份完畢後,再對該 POST 請求進行答覆。 所以用戶每次上傳圖片時,當收到了答覆之後, 則可以認爲此備份已完成。這個和最終一致性不同,屬於強一致性。

上述實現強一致性的過程中, 有個必要條件就是【 VolumeServer 需要知道往其它那些 VolumeServer 備份】。 在 Weed-FS 的實現中是藉助 MasterServer 來實現, 因爲備份的基本單位是 Volume, 在 MasterServer 中,對每個 VolumeId 都維護對應的備份機器列表。 可以通過如下示例命令查看:

curl "localhost:9333/dir/lookup?volumeId=4&pretty=y"
{
  "volumeId": "4",
  "locations": [
    {
      "url": "127.0.0.1:8081",
      "publicUrl": "localhost:8081"
    },
    {
      "url": "127.0.0.1:8080",
      "publicUrl": "localhost:8080"
    }
  ]
}

如上示例中可以看出,對應的 volumeId=4 的 Volume, 可以看出對應的備份機器列表有兩臺,分別是 "127.0.0.1:8081" 和 "127.0.0.1:8080" 。

實際上對於每臺 VolumeServer 查找其它備份機器的時候, 也是通過如上 HTTP api 向 MasterServer 詢問。 只不過不是每次都詢問,因爲只要詢問過了之後就會緩存下來,只有在緩存裏面找不到才詢問。

【Collection】

示例如下:

啓動 MasterServer

weed master

啓動 VolumeServer

weed volume -dir="/tmp/data1" -max=5  -mserver="localhost:9333" -port=8080

申請Fid

curl "http://127.0.0.1:9333/dir/assign?collection=pictures"
{"fid":"4,01d50c6fbf","url":"127.0.0.1:8080","publicUrl":"localhost:8080","count":1}
curl "http://127.0.0.1:9333/dir/assign?collection=mp3"
{"error":"No free volumes left!"}
curl "http://127.0.0.1:9333/dir/assign?collection=pictures"
{"fid":"5,0147ed0fb7","url":"127.0.0.1:8080","publicUrl":"localhost:8080","count":1}

申請Fid的示例解釋:

  1. 因爲默認情況下,VolumeServer 啓動時, 未申請任何 Volume,當第一次 /dir/assign 的時候, 會分配 Volume,因爲 weed volume 的參數-max=5 所以一次性分配 5 個 Volume ,並且這 5 個 Volume 的 Collection 屬性都是 pictures, 甚至可以看到在 ls /tmp/data1 的結果如下:
/tmp/data1
pictures_1.dat pictures_1.idx pictures_2.dat pictures_2.idx pictures_3.dat pictures_3.idx pictures_4.dat pictures_4.idx pictures_5.dat pictures_5.idx

可以看出每個卷的文件名以 Collection 來命名。

2. 因爲已有的 5 個 Volume 的 Collection 屬性都是 pictures, 所以此時如果需要 /dir/assign 一個非 pictures Collection 的 Fid 時失敗,

3. 當申請一個屬於 pictures Collection 的 Fid 成功。

也就是在每次申請 Fid 時,會針對 Collection 進行檢查,來保證存入 Volume 的每個 Needle 所屬的 Collection 一致。 在實際應用中可以通過 Collection 來類別的分片。

【Volume 的大小限制】

在每次 VolumeServer 向 MasterServer 發送心跳信息的時候, 會在 storage.VolumeInfo.Size 裏面註明當前 Volume 的大小信息(Size)。 所以可以以此來限制 Volume 的大小。 如下函數:

func (vl *VolumeLayout) isWritable(v *storage.VolumeInfo) bool {
    return uint64(v.Size) < vl.volumeSizeLimit &&
        v.Version == storage.CurrentVersion &&
        !v.ReadOnly
}

當 VolumeInfo.Size 大於 VolumeLayout.volumeSizeLimit 時, 則將該 Volume 標記爲不可寫。 而 VolumeLayout.volumeSizeLimit 的值可以在啓動 MasterServer 的時候配置。 由 weed help master 可知:

-volumeSizeLimitMB=30000: Master stops directing writes to oversized volumes.

每個 Volume 的最大 Size 默認是 30G , 而每個 VolumeServer 可以配置 n 個 Volume,根據所在機器不同的硬盤大小配置不同的 n . 由weed help volume 可知:

-max="7": maximum numbers of volumes, count[,count]...

每個 VolumeServer 默認的 Volume 大小是 7 。

所以默認情況下,當一個 VolumeServer 使用的磁盤超過 7 * 30G = 210G 之後, 該 VolumeServer 屬於只讀狀態, MasterServer 不會再分配新的 Fid 給它。

但是其實這裏會有漏洞,如果此時不通過請求 MasterServer 獲取 Fid, 而是直接自己構造 Fid 向 VolumeServer POST 文件的話, VolumeServer 還是會一直接受上傳的文件, 直到大小超過在 storage/needle.go 裏面寫死的一個常量:

MaxPossibleVolumeSize = 4 * 1024 * 1024 * 1024 * 8

其實在 VolumeServer 裏面也有維護一個變量叫 volumeSizeLimit ,

type Store struct {
    ...
    volumeSizeLimit uint64 //read from the master
    ...
}

此變量的值是從 MasterServer 獲取的, 當每次 VolumeServer 寫入 Needle 到 Volume 的時候, 都會檢查 Volume Size 是否超過 volumeSizeLimit , 當超過的時候會打錯誤日誌, 但是不會停止寫入,也就是不會拒絕上傳的文件。 有且只有當大小超過 MaxPossibleVolumeSize 的時候纔會拒絕寫入磁盤。

【擴容】

對於 Weed-FS 來說,擴容非常簡單,

啓動 MasterServer:

./weed master -mdir="/tmp/weed_master_tmp"

啓動 VolumeServer1:

weed volume -dir="/tmp/data1" -max=5  -mserver="localhost:9333" -port=8080

當 VolumeServer1 因爲抵達 Volume 大小上限而無法繼續接受上傳數據時, 此時繼續 submit 上傳數據 MasterServer 則會返回錯誤(因爲在 MasterServer 內已經將 VolumeServer1 標記爲不可寫)。

curl -F "file=@/tmp/test.pdf" "127.0.0.1:9333/submit"
{"error":"No free volumes left!"}

此時直接再啓動 VolumeServer2 即可

weed volume -dir="/tmp/data2" -max=5  -mserver="localhost:9333" -port=8081

此時 VolumeServer2 啓動之後會自動向 MasterServer 的 Topology 結構註冊新的 DataNode , 當 MasterServer 接受到新的 submit 請求的時候,會將該上傳文件寫入 VolumeServer2 (因爲此時 VolumeServer1 已經不可寫)。

也就是說如果當線上出現容量問題的時候,擴容只需要加機器即可,簡單有效。

總結

  • 每個 MasterServer 通過 Topology 維護多個 VolumeServer 。
  • 每個 VolumeServer 維護多個 Volume 。
  • 每個 Volume 包含多個 Needle ,Needle 即文件。
  • 多臺 VolumeServer 之間的多機備份實現是強一致性。

  • 多臺 MasterServer 之間的主從關係是是通過 goraft 實現。
轉帖:http://yanyiwu.com/work/2015/01/09/weed-fs-source-analysis.html

發佈了28 篇原創文章 · 獲贊 12 · 訪問量 105萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章