一般我們存儲簡單處理就是寫三副本,但是三副本的成本太大了,采用糾删碼可以比較好的降低存儲空間的成本,具體看下golang中的代碼實作。
// 糾删碼測試
// 将一個檔案拆分成10分,删除其中的任意三分,嘗試還原檔案
// 這邊需要注意的一點是檔案的拆分後的順序和還原的順序是相關的,順序錯誤是無法還原的
// 其中Encoder接口有以下幾個關鍵的函數。
// Verify(shards [][]byte) (bool, error)。每個分片都是[]byte類型,分片集合就是[][]byte類型,傳入所有分片,如果有任意的分片資料錯誤,就傳回false。
// Split(data []byte) ([][]byte, error)。将原始資料按照規定的分片數進行切分。注意:資料沒有經過拷貝,是以修改分片也就是修改原資料。
// Reconstruct(shards [][]byte) error。 這個函數會根據shards中完整的分片,重建其他損壞的分片。
// Join(dst io.Writer, shards [][]byte, outSize int) error。将shards合并成完整的原始資料并寫入dst這個Writer中。
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/klauspost/reedsolomon"
"github.com/qtj/gosdk/file"
)
定義和初始化驗證一些參數
var (
srcFile string // 原始檔案
dstDir string // 目标目錄
recoverName string // 還原後的檔案名稱
dataShards int // 資料分片
parityShards int // 校驗分片
oper string // 操作動作
)
func init() {
flag.StringVar(&srcFile, "srcFile", "qtjErasureCode.exe", "原始檔案名稱")
flag.StringVar(&dstDir, "dstDir", "dstDir", "目标目錄")
flag.StringVar(&recoverName, "recoverName", "recoverName", "還原後的檔案名稱")
flag.StringVar(&oper, "oper", "", "split和recover二選一,split會将一個檔案拆分成類似10個資料檔案和3和校驗檔案,recover的時候可以删除目标目錄下的三個檔案做還原即可")
flag.IntVar(&dataShards, "dataShards", 10, "資料分片個數")
flag.IntVar(&parityShards, "parityShards", 3, "校驗分片個數")
flag.Parse()
}
主調用
// qtjErasureCode.exe -oper=split先将檔案拆分
// 删除目前目錄下的子檔案夾dstDir裡面的任意三個檔案
// qtjErasureCode.exe -oper=recover -recoverName="recover.exe" 将檔案還原
// 最後比對md5發現是一緻的
func main() {
if !strings.Contains(dstDir, "/") && !strings.Contains(dstDir, "\\") {
dstDir = file.GetCurDir() + dstDir + "/"
}
if oper == "split" {
file.RemoveDirTree(dstDir)
file.CreateDirTree(dstDir)
if err := splitFile(); err != nil {
fmt.Println(err)
return
}
} else if oper == "recover" {
if err := recoverFile(); err != nil {
fmt.Println(err)
return
}
} else {
fmt.Println("錯誤的操作動作參數,oper必須為split或者recover")
return
}
}
還原檔案驗證
func recoverFile() error {
if recoverName == "" {
return fmt.Errorf("還原後的檔案名稱%s不能為空", recoverName)
}
// 資料分10片和校驗3片
enc, err := reedsolomon.New(dataShards, parityShards)
if err != nil {
return fmt.Errorf("建立資料分片和校驗分片失敗,%s", err.Error())
}
shards := make([][]byte, dataShards+parityShards)
for i := range shards {
splitName := fmt.Sprintf("%ssplit%010d", dstDir, i)
// 不管檔案是否存在,需要保留原先的順序
if shards[i], err = ioutil.ReadFile(splitName); err != nil {
fmt.Printf("讀取檔案[%s]失敗,%s\n", splitName, err.Error())
}
fmt.Println(splitName)
}
ok, err := enc.Verify(shards)
if ok {
fmt.Println("非常好,資料塊和校驗塊都完整")
} else {
if err = enc.Reconstruct(shards); err != nil {
return fmt.Errorf("重建其他損壞的分片失敗,%s", err.Error())
}
if ok, err = enc.Verify(shards); err != nil {
return fmt.Errorf("資料塊校驗失敗2,%s", err.Error())
}
if !ok {
return fmt.Errorf("重建其他損壞的分片後資料還是不完整,檔案損壞")
}
}
f, err := os.Create(recoverName)
if err != nil {
return fmt.Errorf("建立還原檔案[%s]失敗,%s", recoverName, err.Error())
}
// 這部分的大小決定了還原後的大小和原先的是不是一緻的,不然使用md5比對或者大小都是不一樣的
// 實際生産需要一開始就拆分檔案時候就記錄總的大小
//if err = enc.Join(f, shards, len(shards[0])*dataShards); err != nil {
_, ln, err := file.GetFileLenAndMd5(srcFile)
if err != nil {
return fmt.Errorf("計算原始檔案[%s]大小失敗,%s", srcFile, err.Error())
}
if err = enc.Join(f, shards, int(ln)); err != nil {
return fmt.Errorf("寫還原檔案[%s]失敗,%s", recoverFile(), err.Error())
}
return nil
}
分隔檔案處理
func splitFile() error {
// 資料分10片和校驗3片
enc, err := reedsolomon.New(dataShards, parityShards)
if err != nil {
return fmt.Errorf("建立資料分片和校驗分片失敗,%s", err.Error())
}
bigfile, err := ioutil.ReadFile(srcFile)
if err != nil {
return fmt.Errorf("讀取原始檔案[%s]失敗,%s", srcFile, err.Error())
}
// 将原始資料按照規定的分片數進行切分
shards, err := enc.Split(bigfile)
if err != nil {
return fmt.Errorf("針對原始檔案[%s]拆分成資料[%d]塊,校驗[%d]塊失敗,%s", srcFile, dataShards, parityShards, err.Error())
}
// 編碼校驗塊
if err = enc.Encode(shards); err != nil {
return fmt.Errorf("編碼校驗塊失敗,%s", err.Error())
}
for i := range shards {
splitName := fmt.Sprintf("%ssplit%010d", dstDir, i)
fmt.Println(splitName)
if err = file.SaveFile(shards[i], splitName); err != nil {
return fmt.Errorf("原始檔案[%s]拆分檔案[%s]寫失敗,%s", srcFile, splitName, err.Error())
}
}
return nil
}