golang分布式存储 读书笔记(3)——数据冗余之RS码

对象存储的数据冗余

如果数据只存储一份,存储设备坏了数据就丢失了,所以需要做数据冗余。

常见的数据冗余策略就是多副本冗余,该策略实现简单,但是代价比较高。书中介绍的冗余策略是使用Reed-Solomon纠删码实现的。

RS纠删码中有数据片校验片的概念,假如说选择4个数据片,那么就会将数据分成4个分片对象。每个分片的大小是原始对象的25%,如果选择2个校验片,那么会同时生成2个和数据片大小一样的校验片,所以一个文件最后会得到6个分片。

神奇的是,6个分片里面,只要有任意4个分片没有损坏,都可以还原出原始文件。

评价一个数据冗余策略的好坏,主要是衡量该策略对存储空间的要求和其抗数据损坏的能力。

  • 对存储空间的要求是指我们采用的冗余策略相比于不使用冗余要额外支付的存储空间,用百分比表示。
  • 抗数据损坏的能力以允许损坏或丢失的对象数量来衡量。

例如,在不使用冗余策略的情况下,我们的对象占用存储空间的大小就是它本身的大小,一旦该对象损坏,我们就丢失了这个对象,所以它对存储空间的要求使100%,抵御能力是0;如果使用双副本冗余策略,存储空间要求是200%,抵御数据损坏的能力是1(可以丢失两个副本中的任意1个);而使用4+2RS码的策略,存储空间的要求是150%,抵御能力是2(可以丢失6个分片对象中的任意2个);而对于一个M+NRS码(M个数据片加N个校验片),其对存储空间的要求是(M+N)/M*100%,抵御能力是N

RS码原理简介

原理比较复杂,这里抄一个网上博客的例子:假设选择4个数据片,2个校验片,有一个文件需要备份,文件内容是ABCDEFGHIJKLMNOP。首先将其分成4个分片,得到一个二维数组,每一行是一个数据分片,共4行。

The Original Data.png

RS算法会使用一个6*4的矩阵(6是总分片数,4是数据分片数,文中称之为Coding Matrix),和原始数据(Original Data)做乘法,得到一个新的矩阵(Coded Data):

Applying the Coding Matrix.png

可以看到,新的矩阵(Coded Data前四行和原始数据还是一样的,新增了两行不知道什么含义的数据。

现在,假设是34行数据被损坏了(索引从1开始)!

两行数据被损坏.png

那么,怎么由剩余的四行还原数据呢?

删除两行之后的等式.png

如上图,将Coding Matrix34行也去掉,这个等式依然成立。最重要的是,剩下的这个Coding Matrix是一个可逆矩阵(这个特殊的矩阵是怎么寻找出来的),等式两边同时乘以该矩阵的可逆矩阵:

两边同时乘以Coding Matrix的逆矩阵.png

最后总结,根据以下公式可以得到原始数据(由等式右边可以得到等式左边):

重建数据的公式.png

golang实现的RS库

github上有一个用golang实现的rs库。

使用go get -u -v github.com/klauspost/reedsolomon 进行安装。

关键函数

查看他的doc文档,使用New方法可以得到一个实现了Encoder接口的对象。函数原型是func New(dataShards, parityShards int, opts ...Option) (Encoder, error),需要传入数据片和校验片的大小。

其中Encoder接口有以下几个关键的函数。

  1. Verify(shards [][]byte) (bool, error)。每个分片都是[]byte类型,分片集合就是[][]byte类型,传入所有分片,如果有任意的分片数据错误,就返回false
  2. Split(data []byte) ([][]byte, error)。将原始数据按照规定的分片数进行切分。注意:数据没有经过拷贝,所以修改分片也就是修改原数据。
  3. Reconstruct(shards [][]byte) error。这个函数会根据shards中完整的分片,重建其他损坏的分片。
  4. Join(dst io.Writer, shards [][]byte, outSize int) error。将shards合并成完整的原始数据并写入dst这个Writer中。

demo

官方提供了demo,学习一下simple-encoder.gosimple-decoder.go文件。

下面的代码做了一点修改。

simple-decoder.go:打开并读取D:/objects/文件夹下面的test文件(文件内容随便),使用rs库将其分为6个文件(4个数据片,2个校验片),保存在"D:/objects/encoder/"文件夹下。

文件内容如下(A,B,C,D分别20个),移动80个字节,注意不要使用windows的记事本进行编辑

AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDD

Enocder

simple-encoder.go代码如下:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"

    "github.com/klauspost/reedsolomon"
)

const (
    dataShards = 4 // 数据分片数
    parShards  = 2 // 校验分片数
)


func main() {

    fname := "D:/objects/test"
    // Create encoding matrix.
    enc, err := reedsolomon.New(dataShards, parShards)
    checkErr(err)

    fmt.Println("Opening", fname)
    b, err := ioutil.ReadFile(fname)
    checkErr(err)

    // Split the file into equally sized shards.
    shards, err := enc.Split(b)
    checkErr(err)
    fmt.Printf("File split into %d data+parity shards with %d bytes/shard.\n", len(shards), len(shards[0]))

    // Encode parity
    err = enc.Encode(shards)
    checkErr(err)

    // Write out the resulting files.
    _, file := filepath.Split(fname)
    dir := "D:/objects/encoder/"
    for i, shard := range shards {
        outfn := fmt.Sprintf("%s.%d", file, i)

        fmt.Println("Writing to", outfn)
        err = ioutil.WriteFile(filepath.Join(dir, outfn), shard, os.ModePerm)
        checkErr(err)
    }
}

func checkErr(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %s", err.Error())
        os.Exit(2)
    }
}
Opening D:/objects/test
File split into 6 data+parity shards with 20 bytes/shard.
Writing to test.0
Writing to test.1
Writing to test.2
Writing to test.3
Writing to test.4
Writing to test.5

可以看到每个shard(分片)是20个字节,生成了6个文件,打开之后发现,test.0是20个Atest.120Btest.220Ctest.320D,正好可以拼接成完整的文件,和前面的理论符合。

Decoder

simple-decoder.go代码如下:

package main

import (
    "fmt"
    "io/ioutil"
    "os"

    "github.com/klauspost/reedsolomon"
)

const (
    dataShards = 4
    parShards  = 2
)

func main() {
    // Create matrix
    enc, err := reedsolomon.New(dataShards, parShards)
    checkErr(err)

    fname := "D:/objects/encoder/test"
    // Create shards and load the data.
    shards := make([][]byte, dataShards+parShards)
    for i := range shards {
        infn := fmt.Sprintf("%s.%d", fname, i)
        fmt.Println("Opening", infn)
        shards[i], err = ioutil.ReadFile(infn)
        if err != nil {
            fmt.Println("Error reading file", err)
            shards[i] = nil
        }
    }

    // Verify the shards
    ok, err := enc.Verify(shards)
    if ok {
        fmt.Println("No reconstruction needed")
    } else {
        fmt.Println("Verification failed. Reconstructing data")
        err = enc.Reconstruct(shards)
        if err != nil {
            fmt.Println("Reconstruct failed -", err)
            os.Exit(1)
        }
        ok, err = enc.Verify(shards)
        if !ok {
            fmt.Println("Verification failed after reconstruction, data likely corrupted.")
            os.Exit(1)
        }
        checkErr(err)
    }

    outfn := "D:/objects/decoder/test"
    fmt.Println("Writing data to", outfn)
    f, err := os.Create(outfn)
    checkErr(err)

    // We don't know the exact filesize.
    err = enc.Join(f, shards, len(shards[0])*dataShards)
    checkErr(err)
}

func checkErr(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %s", err.Error())
        os.Exit(2)
    }
}
Opening D:/objects/encoder/test.0
Error reading file open D:/objects/encoder/test.0: The system cannot find the file specified.
Opening D:/objects/encoder/test.1
Error reading file open D:/objects/encoder/test.1: The system cannot find the file specified.
Opening D:/objects/encoder/test.2
Opening D:/objects/encoder/test.3
Opening D:/objects/encoder/test.4
Opening D:/objects/encoder/test.5
Verification failed. Reconstructing data
Writing data to D:/objects/decoder/test

删除test.0test.1这两个文件,执行程序,最后还是能正常的回复成原始数据。

残留问题

  • 具体算法还没有了解,如,Coding Matrix如何得到的。
  • 官方demo中还有stream-encoder.gostream-decoder.go没有阅读。
  • 该算法的一些限制,如数据分片和校验分配的个数有没有限制?

参考

  • 《分布式对象存储--原理架构及Go语言实现》
  • github:klauspost/reedsolomon
  • Backblaze blog post
  • reedsolomon包介绍

你可能感兴趣的:(golang分布式存储 读书笔记(3)——数据冗余之RS码)