浅析EC纠删码

此处关于公式的排版有些散乱, 可以去看这个

https://zhuanlan.zhihu.com/p/645376690

Overview

EC(Erasure Code), 中文一般称为纠删码, 所谓"纠删"是指, 它可以"纠正删除", 即补全相对于最初缺失的数据, 而不在已有的数据中检查出错误来(查错一般可以用各类哈希).

想象一下, 假如我们有一些很重要的数据需要保存(比如是自己的比特币密钥?), 在不考虑数据安全的情况下, 如何保证自己的数据不会因存储介质损毁而丢失呢? 刻在石头上?

"重要的事情说三遍"

在讲纠删码之前, 我们可以用一个最简单并且很靠谱的方法: 将数据复制三份. 分别存在三个不同的硬盘(/光盘/U盘 or etc)中, 假如单个硬盘损坏的概率是0.01, 那么三个硬盘同时坏掉的概率就是0.000001. 只要有一个硬盘还能用, 我们就还能拿到数据. 这就是所谓的多副本容灾.

到这里, 简单算一下, 假如一个硬盘的损坏概率为0.01, 那么数据能正常获取到的概率就是\(1 - (0.01)^3 = 0.999999\), 但是呢, 这样一来, 一份数据我们需要存三份, 此时数据的冗余度就是3.

所以, 这种方法的缺点就很明显了, 贵. 像是比特币密钥这种2kb大小多一点的数据还好, 但如果是更大且重要的数据, 比如是一部电影, 你可能需要三个硬盘, 每个硬盘都有1TB, 这样的话, 你需要3TB的硬盘空间, 而且还要花三倍的钱.

所以, 这就是EC的用武之地了, 它可以让你用更少的硬盘, 来达到差不多的容灾效果.

其实也没那么寸啦

除了像是比特币密钥这种特别特别重要的数据, 我们其实并不需要那么高的容灾效果. 一般来说, 硬盘的年损坏概率(AFR)是0.01左右. 如果我们有k块数据需要存, 如果我们能做到将数据存储在k+1块硬盘中, 并且容忍任意一块硬盘损坏的话, 那么此时的容灾效果就是\((1-q)^{k+1} + (k+1)q(1-q)^k\), 其中q是硬盘的年损坏概率.

这里q取0.01, k取3(假设我们有3份数据要存), 那么此时的容灾效果就是\((1-0.01)^4 + 4*0.01*(1-0.01)^3 \approx 0.999408\), 冗余度为\(1.33\);

那, 如何为k块数据额外添加一个冗余块呢? 最简单的方式----加和!

假设这k块数据分别为\(D_1, D_2, ..., D_k\), 那么我们可以将这k块数据加和, 得到\(D_{k+1} = D_1 + D_2 + ... + D_k\), 这样, 我们就得到了一个冗余块\(D_{k+1}\).

这样, 如果任意一个数据块丢失了, 我们可以\(D_{k+1} - \sum D_i\)来恢复丢失的数据块, 其中\(i \in [1, k]\). 而如果冗余块自己丢失了, 我们可以用\(D_{k+1} - \sum D_i\)来恢复冗余块.

加和Plus

但是, 如果只是简单加和, 那就只能获得一个冗余块, 如果我们需要十个甚至九个呢? 这时候就需要带系数的加和了, 相信大家在初中就学过这个东西了, 就是多项式.

还是假设我们有三块数据要存, 采用不同的系数, 获得两个不同的数字.

\[\begin{cases} X_1 + 2X_2 + 3X_3 = Y_1 \\ 2X_1 + 3X_2 + 4X_3 = Y_2 \\ \end{cases} \]

其中, \(X_n\)是数据块, \(Y_n\)是冗余块. 在这个例子中, 假如我们不知道任意两个X或者Y的值, 那么就变成了一个二元一次方程组, 通过其他三个数字, 我们就可以解出这个方程组, 从而得到丢失的数据块.

所以, 这个道理很浅显, 只要构成的方程有解, 有几个方程, 就可以解出几个未知数, 或者说, 我们就可以容忍几块数据的丢失.

那么, 这个前提就很重要, 需要一个有解的方程组.

有解的方程组 (加和 Plus Pro)

下面我们可能就需要用一个比较高端的概念了, 矩阵(别害怕, 就是方程组pro 而已).

我们可以将上面的方程组写成矩阵的形式:

\[\begin{bmatrix} 1 & 2 & 3 \\ 2 & 3 & 4 \\ \end{bmatrix} \]

其中, 矩阵的每一行代表一个方程, 矩阵的每一列代表一个未知数. 如果此方程有解, 那么这个矩阵就是一个可逆矩阵.

接下来, 我们用矩阵的语言来表述一下数据的冗余计算以及恢复过程.

假设我们有k块数据要存, 希望有r块冗余数据来抵抗r块数据的丢失, 那么我们可以构造一个r行k列的矩阵, 记为\(M_{r\times k}\), 其中每一行代表一个数据块, 每一列代表一个冗余块.

那么这个系数矩阵就是:

\[M_{r\times k} = \begin{bmatrix} c_{1,1} & c_{1,2} & ... & c_{1,k} \\ c_{2,1} & c_{2,2} & ... & c_{2,k} \\ ... & ... & ... & ... \\ c_{r,1} & c_{r,2} & ... & c_{r,k} \\ \end{bmatrix} \]

其中, \(c_{i,j}\)代表第i行第j列的系数. 然后, 将数据块作为一个列向量, 记为\(D_{k\times 1}\), 与系数矩阵相乘, 得到冗余块的列向量\(Y_{r\times 1}\):

\[Y_{r\times 1} = M_{r\times k} \times D_{k\times 1} \]

\[\begin{bmatrix} c_{1,1} & c_{1,2} & ... & c_{1,k} \\ c_{2,1} & c_{2,2} & ... & c_{2,k} \\ ... & ... & ... & ... \\ c_{r,1} & c_{r,2} & ... & c_{r,k} \\ \end{bmatrix} \cdot \begin{bmatrix} d_1 \\ d_2 \\ ... \\ d_k \\ \end{bmatrix} = \begin{bmatrix} y_1 \\ y_2 \\ ... \\ y_r \\ \end{bmatrix} \]

其中, \(y_i\)代表的就是生成后的冗余块.

那么, 如果我们丢失了几个数据块(小于等于r), 比如是q个数据块丢失(q<=r), 那么根据之前冗余块的运算规则, 我么可以在上面的矩阵中, 随意选择q行, 组成一个等式:

\[\begin{bmatrix} c_{i_1,1} & c_{i_1,2} & ... & c_{i_1,k} \\ c_{i_2,1} & c_{i_2,2} & ... & c_{i_2,k} \\ ... & ... & ... & ... \\ c_{i_q,1} & c_{i_q,2} & ... & c_{i_q,k} \\ \end{bmatrix} \cdot \begin{bmatrix} d_1 \\ d_2 \\ ... \\ d_{q_1} \\ d_{q_2} \\ ... \\ d_{k} \\ \end{bmatrix} = \begin{bmatrix} y_{i_1} \\ y_{i_2} \\ ... \\ y_{i_q} \\ \end{bmatrix} \]

这里, d带有q下标的数据块就是丢失的数据块. 而y是我们选择的q个冗余块.
这样, 就构成了一个方程组, 通过解这个方程组, 我们就可以得到丢失的数据块. 或者在矩阵的理论中, 我们可以用逆矩阵来表达这个求解过程:

\[C_{q\times k} \cdot D_{k\times 1} = Y_{q\times 1} \]

我们要解出含有未知量的矩阵\(D_{q\times 1}\), 那么我们就需要将\(C_{q\times q}\)求逆, 得到\(C^{-1}_{q\times q}\), 然后两边同时乘以\(C^{-1}_{q\times q}\), 得到:

\[D_{k\times 1} = C^{-1}_{k\times q} \cdot Y_{q\times 1} \]

这样其实有一点点冗余的, 矩阵的话, 是将全部D都求出来了, 但是我们只需要求出丢失的数据块, 所以我们可以将矩阵的维度缩小一点, 只求出丢失的数据块, 而不是全部数据块. 当然, 这个就不展开了, 反正就是矩阵的一些小技巧.

呐, 到这里, 我们就可以得出结论了, 只要我们构造出一个可逆矩阵, 就可以用这个矩阵来计算冗余块, 并且可以通过这个矩阵来恢复丢失的数据块.

但是这里有一个问题, 可逆的矩阵多的是, 但是我们要求这个矩阵的任意q行都是可逆的, 这个就比较高端了, 通常来说常用的并且满足这个需求的矩阵有两种, 一种是Vandermonde矩阵, 另一种是Cauchy矩阵. 这两个矩阵具体如何构造, 以及为什么满足这个需求, 这里就不展开了, 有兴趣的可以自己去查一下 (不难, 很有意思, 但是我这里写不下了)=

有限域

到这, 其实你已经理解EC是什么东西了, 但是我们知道, 计算机, 它懂什么计算:). 我们这套理论基本说的是实数域上的, 但是计算机不擅长这个, 即使叫它算个浮点数, 都会有偏差. 并且, 以最简单的加和来看, 如果数据的数字比较大, 那么势必会溢出, 从而导致计算错误, 或者用高精度算法? 那过于沙雕了, 并且存储空间也会变得很大.

所以, 既然实数域不行, 我们可以尝试一下有限域. 有限域的概念很简单, 我们可以将其视为一个有限集合, 集合里面的数字(元素)个数是有限的, 同时这些数字之间还有一些运算规则, 比如加减乘除, 但是这些运算规则有一些特殊, 比如加法满足交换律, 乘法满足交换律和结合律, 除法满足分配律等等.

接下来的内容其实看不懂也没关系, 只要知道有限域是什么东西就行了, 总之就是, 有限域是一种独特的数学结构, 有限域上的运算规则和实数域上的运算规则有一些相似之处, 但是也有一些不同之处. 有限域的概念很简单, 但是如果你想深入了解, 那么就需要一些高等代数的知识了, 比如群, 环, 域, 有限域, 以及有限域的构造等等.
通过有限域, 我们可以让计算机"无损"的进行加减乘除等运算, 并且不会溢出(就像时钟一样, 12点加1小时, 就是1点, 而不是13点)

这里, 我们可以用一个简单的例子来说明一下, 比如我们有一个有限域, 里面的数字是0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 但是我们规定, 如果数字大于11, 那么就取模, 比如12取模为0, 13取模为1, 14取模为2, 以此类推. 那么, 我们可以定义一下加法和乘法:

\[\begin{cases} a + b = (a + b) \mod 12 \\ a \times b = (a \times b) \mod 12 \\ \end{cases} \]

这样, 我们就成功的构造了一个有限域, 里面的数字是0到11, 且满足加法和乘法的运算规则.

矩阵求逆, 或者说方程求解可以"实现"的一个重要前提是, 任意实数都存在自己的加法逆元; 除了0外, 任意实数都存在自己的乘法逆元(证明不会, 反正总之这样, 我们才能求逆矩阵)

但是像我们上面这个有限域的例子, 它的乘法逆元是有问题的, 比如, 数字2, 不存在一个数字, 使得2乘以这个数字等于1, 因为2乘以任意数字, 都会得到一个偶数, 但是1是奇数, 所以不存在这样的数字. 所以, 这个有限域是不满足求逆矩阵的条件的.

相信机智的你已经发现了, 如果让这个例子变得可以求逆, 那么数字个数必须是质数, 因为如果数字个数是合数, 那么一定存在一个数字, 它的因数中包含了2, 那么这个数字就不可能存在乘法逆元了. 比如, 如果是模7运算, 那么一切都是好的, 但是如果是模8运算, 那么数字4就没有乘法逆元, 因为4的因数中包含了2.

但是, 这就引入另一个问题了, 如果数字个数是质数, 那显然不太适合计算机, 它比较喜欢2的幂次, 比如一个字节, 一个kb, 其数字范围都是2的幂次, 如果数字个数是质数, 那么计算机就不太好处理了.

到这, 问题已经比较明显了, 如果使用传统加法与乘法, 那么显然域的数字个数不能是质数, 但是如果数字个数不是质数, 那么计算机算起来又不方便. 所以, 我们需要一种新的加法与乘法, 既能让计算机方便的计算, 又能让域的数字个数不是质数.

都什么年代了, 还在用传统加法与乘法?

上面的表述不太严谨, 其实恰好有一种情况, 元素个数既是质数, 而且又是计算机喜欢的2的幂次, 那就是2本身.

所以, 我们可以将有限域的数字个数取为2. 此时的加法与乘法, 会有一些特殊的性质:

GF(2)

在GF(2)中, 我们依旧使用之前取模的方式, 但是此时的取模是2, 也就是说, 任意数字都只有0和1两种可能. 那么, 此时的加法与乘法的运算规则就变成了:

0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 0
0 * 0 = 0
0 * 1 = 0
1 * 0 = 0
1 * 1 = 1

很明显, 这里的加法就是异或, 乘法就是与. 这样, 我们就可以用计算机的位运算来实现加法与乘法了.

并且, 矩阵求逆的加法逆元? 乘法逆元? 我们看到, 任意数字加上自己, 都会得到0, 所以加法逆元就是自己(不仅存在, 而且唯一); 除0外, 任意数字乘以自己, 都会得到自己本身, 所以乘法逆元也是存在的(唯一且存在哦). 所以, 在GF(2)上进行矩阵求逆, 是完全没有问题的.

另外一个问题, 只有两个数字, 是不是不太够用? 其实足够了. 想象一下, 如果我们将处理的数据从每个字节拆开, 视为是逐bit处理, 那么每个bit都是0或者1, 那么就可以用GF(2)来表示了. 即使是处理字节, 也可以将字节拆开, 逐bit处理, 然后再拼接起来.

当然, 实际使用时, 肯定不是逐bit处理的, 而是逐字节处理的, 但是原理是一样的, 依旧是将加法与乘法转换为异或 与 与运算.

GF(2^8)

GF(2^8)就是GF(2)的扩展, 也就是说, 我们可以将GF(2)的数字个数扩展到2^8, 也就是256个数字. 这里的2^8, 也就是2的8次方, 也就是1个字节, 也就是计算机最喜欢的数字个数.

我这里其实是一种简化的表述, 当然更严谨的还会涉及到域的扩张及多项式的运算, 但是这里就不展开了, 反正就是这么个意思, 有兴趣的可以自己去查一下. 其实只要理解了GF(2)是如何工作的就足够了. 如果对此感兴趣, 可以看一下Reed-Solomon的论文, 里面有详细的介绍. 或者, 完全没看懂GF是什么也没关系, 总之记住是一种区别于实数的计算法则, 一直更适合计算机宝宝体质的计算方式就行了

当然, 更给力的还要GF(2^16), GF(2^32), GF(2^64)等等. 看一次想处理多少数据, 就用多少位的GF(2^x)就行了.

标准EC

到这里, 其实EC需要了解的东西已经差不多了

  • 有限域, EC的矩阵运算使用模2有限域, 也就是GF(2) (加法变成了异或, 乘法变成了与)
  • 矩阵求逆, 也就是方程求解, 用于恢复丢失的数据块. 系数矩阵要选择那种任意q行都可逆的矩阵, 比如Vandermonde矩阵, Cauchy矩阵等等.
posted @ 2024-01-29 00:25  RiversJin  阅读(192)  评论(0)    收藏  举报