天天看點

golang檔案存儲糾删碼實作定義和初始化驗證一些參數主調用還原檔案驗證分隔檔案處理

一般我們存儲簡單處理就是寫三副本,但是三副本的成本太大了,采用糾删碼可以比較好的降低存儲空間的成本,具體看下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
}