本文深入探讨了消除编码(Erasure Coding)的原理和实现,特别是在去中心化区块链和数据存储系统中的应用。通过详细的数学基础和编码、解码过程的示例,展示了消除编码如何提供数据的高可用性、降低存储成本,并增强安全性,适应未来的存储挑战。
想象一下你参与了一个新的去中心化区块链,旨在链上存储大量数据——比如 NFT 艺术作品、交易历史和用户生成的档案。网络中的每个节点仅托管总账本的一部分。一些节点意外离线;其他节点可能是恶意的。直接复制每个字节多次是昂贵的,并且会减慢整个区块链的速度。
与此同时,丢失数据不是一个选项:如果关键记录消失,去中心化信任模型就会崩溃。你如何能在不淹没存储成本的情况下保持数据 高度可用?
这就是出错编码(erasure coding)大显身手的地方——将数据分割成碎片,如果被截获或丢失,每个片段都是 不可用 的,但如果你能收集到足够多的生存片段,它们是完全可重构的。与简单的复制(比如存储三份或五份备份)相比,这种方法大大降低了开销,并提供了内置的安全优势,因为没有单个片段揭示你的全部数据。
本文深入探讨了出错编码如何工作,解释了它背后的线性代数、有限域算术、二进制矩阵优化以及其他数学基础。到最后,你将看到区块链项目、云服务以及无数其他以数据为中心的平台如何依赖数学来保护关键任务数据的安全,同时避免重复副本带来的沉重负担。
我们将讨论:
让我们深入探讨。
$[N, K]$ 出错编码将原始数据转化为 $N$ 个编码片段(碎片),具有以下特性:
这意味着你可以获得对故障的容忍(最多 $N - K$),而无需存储多个完整副本。
下面是一个显示概念流动的图示:
flowchart LR
A[原始数据] --> B[分割成 K 个数据块]
B --> C[编码成 N 个碎片矩阵乘法]
C -- 一些碎片丢失 --> D[仅 K 个碎片剩余]
D --> E[解码子矩阵的逆矩阵]
E --> F[重构数据]
让我们理解一下上面图示中的内容。
降低成本
提供者数量 ($S$) | 故障次数 ($F$) | 复制开销 | 出错编码开销 | 存储节省 |
---|---|---|---|---|
4 | 2 | 3X | 2X | 33% |
5 | 2 | 3X | 1.67X (近似) | ~44% |
增强安全性
因为每个碎片只是一个片段,没有单个碎片(少于 $K$)揭示原始数据。这是一个内置的“信息切片”层级,可以增强你的安全姿态。
出错编码的核心是矩阵乘法。我们将数据块视为向量,然后与编码器矩阵进行乘法乘。以下是一些关键定义:
对于出错编码,我们构造一个矩阵 $E$(大小 $N\times K$)。编码后的碎片为:
$$ \text{编码} = E \times \text{数据向量}. $$
更准确地说,任何矩阵都不行。我们需要任何 $K \times K$ 子矩阵在我们的选择域上是可逆的。柯西矩阵是经典选择:
$$ C_{i,j} = \frac{1}{x_i - y_j}, \quad 1 \leq i \leq N,\ 1 \leq j \leq K, $$
其中 ${x_1, \dots, x_N}$ 和 ${y_1, \dots, y_K}$ 在一个域中都是不同的元素。
构建
为什么子矩阵是可逆的
每个 $K\times K$ 的子矩阵也是柯西矩阵(只是比大矩阵的小一点)。柯西矩阵的行列式具有一个已知的封闭形式,如果 $x_i$ 和 $y_j$ 集合是不同的,它的值 永远不为零。
假设 $K=2$,$N=3$。让 ${x}={1,2,4}$,${y}={0,3}$。那么,
\begin{bmatrix} 1 & -\tfrac{1}{2} \ \tfrac{1}{2} & -1 \ \tfrac{1}{4} & 1 \end{bmatrix}. $$
实际警告:我们最终将在一个 有限域 中进行所有算数计算,以避免浮点问题。但这个矩阵展示了结构。
E \times \begin{bmatrix} d_1 \ d_2 \end{bmatrix}. $$
解码:如果我们只有碎片 1 和 3,我们从第一行和第三行构造 $E$ 的 $2\times2$ 子矩阵,求逆,然后将其乘以 $\begin{bmatrix}\text{碎片 1}\ \text{碎片 3}\end{bmatrix}$ 以得到 $\begin{bmatrix} d_1 \ d_2 \end{bmatrix}$。
上述分数 $\tfrac{1}{2}, \tfrac{1}{4}, \dots$ 在实际数字系统中可能导致精度问题。有时,有限域通过让每个数字成为具有 精确 模算术的一组元素来解决这个问题。
$GF(7)$ 的算术表(加法和乘法):
加法($mod \space 7$):
+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
1 | 1 | 2 | 3 | 4 | 5 | 6 | 0 |
2 | 2 | 3 | 4 | 5 | 6 | 0 | 1 |
3 | 3 | 4 | 5 | 6 | 0 | 1 | 2 |
4 | 4 | 5 | 6 | 0 | 1 | 2 | 3 |
5 | 5 | 6 | 0 | 1 | 2 | 3 | 4 |
6 | 6 | 0 | 1 | 2 | 3 | 4 | 5 |
乘法($mod \space 7$):
× | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
2 | 0 | 2 | 4 | 6 | 1 | 3 | 5 |
3 | 0 | 3 | 6 | 2 | 5 | 1 | 4 |
4 | 0 | 4 | 1 | 5 | 2 | 6 | 3 |
5 | 0 | 5 | 3 | 1 | 6 | 4 | 2 |
6 | 0 | 6 | 5 | 4 | 3 | 2 | 1 |
示例:$\tfrac{1}{2} \equiv 4\ (\mod 7)$,因为 $2 \times 4 = 8 \equiv 1\ (\mod 7)$。
素数域 $GF(p)$ 对于小字母表或数值数据很好。然而,大多数现实世界数据是二进制的。$GF(2^n)$(例如 $GF(8)$、$GF(16)$、$GF(256)$ 等)通常更自然:
让不可约多项式为 $y^3 + y + 1$。那么每个元素是一个 3 位图案:$000$、$001$、$010$、…、$111$。
假设我们将 $\alpha = y$(表示 $010$ 的多项式)。那么:
$$ \alpha^3 = \alpha \times \alpha^2 = (y) \times (y^2) = y^3 \equiv y + 1 \quad (\text{mod } y^3 + y + 1), $$
因此 $\alpha^3 = \alpha + 1$。这个恒等式帮助计算更高的幂。
我们来直接乘法:
$$ (y^2 + y + 1) \times (y^2 + 1)\quad \text{mod } (y^3 + y +1). $$
多项式乘法:
$$
(y^2 \cdot y^2) + (y^2 \cdot 1) + (y \cdot y^2) + (y \cdot 1) + (1 \cdot y^2) + (1 \cdot 1)
$$
$$
= y^4 + y^2 + y^3 + y + y^2 + 1
$$
$$
= y^4 + y^3 + 2y^2 + y + 1.
$$
因为我们在 $GF(2^3)$ 中,注意 $2y^2 = 0$(在二进制中系数 2 为 0),所以我们得到:
$$
y^4 + y^3 + y + 1.
$$
现在在 $y^3 + y + 1$ 的基础上简化:
这个结果,$\alpha^2 + \alpha$,是另一个 $GF(8)$ 中的多项式。仔细跟踪这些步骤确保了精确的、 无损 的算术。
虽然 $GF(2^n)$ 算术在概念上很清晰,但对于大数据来说,直接的多项式乘法可能会消耗大量 CPU。一个常见的优化是将 $GF(2^n)$ 的每个元素表示为一个 $n\times n$ 的二进制矩阵:
而且许多现实系统跳过了显式矩阵表示,而只是简单地在硬件中执行重复的 XOR 操作。例如,如果你只有系数为 $0$ 或 $1$,可以选取一个带有许多 $0/1$ 模式的编码矩阵,因此每行的乘法基本上是:
$$ \text{碎片} = \bigoplus_{\substack{\text{位置 } j \ \text{其中行}[j]=1}} \text{数据块}_j, $$
其中 $\oplus$ 是 XOR。
在存储或传输原始二进制数据时,使用像 $GF(7)$ 或 $GF(11)$ 这样素数域可能会产生不匹配:每个域元素可能在最近到 2 比特或 3 比特块的对应上产生浪费,增加存储开销。扩展域通过让我每个元素直接代表 $n$ 位串,避免这种情况,从而与二进制数据无缝对接。
例如,$GF(2^3)$ 是一个 8 元素的域,使用 3 位表示($000$ 到 $111$)并且对不可约多项式进行模算术。在这里,我们将看到:
我们选一个不可约多项式 $y^3 + y + 1 = 0$。将这 8 个元素标记为二进制串 ${000, 001, 010, 011, 100, 101, 110, 111}$。我们还可以将每个元素视为 $y$ 多项式中 $degree < 3$ 的多项式:
元素 # | 多项式 | 3 位表示 |
---|---|---|
0 | $0y^2 + 0y + 0$ | 000 |
1 | $0y^2 + 0y + 1$ | 001 |
2 | $0y^2 + 1y + 0$ | 010 |
3 | $0y^2 + 1y + 1$ | 011 |
4 | $1y^2 + 0y + 0$ | 100 |
5 | $1y^2 + 0y + 1$ | 101 |
6 | $1y^2 + 1y + 0$ | 110 |
7 | $1y^2 + 1y + 1$ | 111 |
现在,让我们理解 $GF(2^3)$ 中算术操作是如何工作的。
加法
110
$\oplus$ 111
= 001
(即 1)。减法
乘法
示例:$6 \times 7$ 意味着
$$ (y^2 + y) \;\times\; (y^2 + y + 1) = (y^4 + y^3 + y^2) + (y^3 + y^2 + y) =(y^4 + y) $$
$$ = y(y^3 + 1) = y(y + 1 + 1) = y*y = y^2 \;\;\text{mod}\;\; (y^3 + y +1). $$
经多项式扩展和减少,结果是 $y^2$,即 100
(元素 #4)。
$GF(2^3)$ 的乘法表可能如下所示:
$*$ | 000 |
001 |
010 |
011 |
100 |
101 |
110 |
111 |
---|---|---|---|---|---|---|---|---|
000 |
000 |
000 |
000 |
000 |
000 |
000 |
000 |
000 |
001 |
000 |
001 |
010 |
011 |
100 |
101 |
110 |
111 |
010 |
000 |
010 |
100 |
110 |
011 |
001 |
111 |
101 |
011 |
000 |
011 |
110 |
101 |
111 |
100 |
001 |
010 |
100 |
000 |
100 |
011 |
111 |
110 |
010 |
101 |
001 |
101 |
000 |
101 |
001 |
100 |
010 |
111 |
011 |
110 |
110 |
000 |
110 |
111 |
001 |
101 |
011 |
010 |
100 |
111 |
000 |
111 |
101 |
010 |
001 |
110 |
100 |
011 |
除法
在 $GF(2^n)$ 中,除法一开始看起来不太寻常。一般来说,$GF(2^n)$ 中的除法是通过乘以乘法逆元来完成的。
我们来看看到底该如何计算:
$$ \frac{6}{7} \quad\text{in}\quad GF(2^3). $$
第 1 步:将数字表示为二进制和多项式
让我们将每个数字映射到其 3 位二进制串,然后到相应的 $y$ 多项式:
十进制 | 二进制 | 多项式 |
---|---|---|
6 | 110 |
$y^2 + y$ |
7 | 111 |
$y^2 + y + 1$ |
我们的目标是在 $GF(2^3)$ 中计算:
$$ \frac{6}{7} \;=\; 6 \;\times\; 7^{-1}, $$ 其中 $7^{-1}$ 由 $7 \times 7^{-1} = 1$ 定义。
第 2 步:找到 7 的逆元($7^{-1}$)
我们测试所有非零元素以找到 $\alpha$,使得 $7 \times \alpha = 1$。
每个产品 $7 \times \alpha$ 是通过使用多项式乘法在多项式 $y^3 + y + 1$(不可约多项式)下计算的。以下是如何做的:
$7 \times 1$(二进制 001
)
$$
(y^2 + y + 1) \times 1 = y^2 + y + 1 \quad (\text{= 7, 二进制 111
}).
$$
$7 \times 2$(二进制 010
= $y$)
$$
(y^2 + y + 1) \times y = y^3 + y^2 + y.
$$
使用 $y^3 \equiv y + 1$ 进行简化:
$$
(y + 1) + y^2 + y = y^2 + 1 \quad (\text{= 5, 二进制 101
}).
$$
$7 \times 5$(二进制 101
= $y^2 + 1$)
$$
(y^2 + y + 1)(y^2 + 1) = y^4 + y^3 + y^2 + y^2 + y + 1.
$$
简化($y^2 + y^2 = 0$)并且简化 $y^4 = y^2 + y$,$y^3 = y + 1$:
$$
(y^2 + y) + (y + 1) + y + 1 = 1 \quad (\text{= 1, 二进制 001
}).
$$
这确认了 $7^{-1} = 5$。
逆元测试结果
候选 $\alpha$ | 计算 $7 \times \alpha$ | 结果 |
---|---|---|
001 (1) |
7 | ≠ 1 |
010 (2) |
5 | ≠ 1 |
011 (3) |
4 | ≠ 1 |
100 (4) |
6 | ≠ 1 |
101 (5) |
1 | ✅ = 1 |
110 (6) |
2 | ≠ 1 |
111 (7) |
3 | ≠ 1 |
第 3 步:执行除法 $\,6 \div 7\,$
现在计算:
$$
6 \div 7 = 6 \times 5 = (y^2 + y)(y^2 + 1).
$$
多项式乘法 $$ (y^2 + y)(y^2 + 1) = y^4 + y^3 + y^2 + y. $$
在 $y^3 + y + 1$ 上模运算
011
}).
$$因此我们得到, $$ \frac{6}{7} = 3 \quad \text{in} \quad GF(2^3). $$
(二进制:110 ÷ 111 = 011
)
假设你要对短旋律“C, C, D, C, F, E”进行出错编码(每个音符映射到数字 2
、2
、3
、2
、5
、4
),但现在你想避免块的膨胀。你选择 $GF(2^3)$,它直接处理 3 位块——没有比特浪费。让我们一起处理编码/解码细节。
我们有旋律:C, C, D, C, F, E。假设我们指定以下数值表示:
这些数字在 $GF(2^3)$ 中是元素,如果我们用二进制解释它们:
010
(多项式 $y$) 011
($y + 1$) 100
($y^2$) 101
($y^2 + 1$)因此,序列“CCDCFE”的表示为:[2, 2, 3, 2, 5, 4]
。
对于一个 $[3,2]$ 出错编码,每个数据块大小为 $K=2$。将 6 个音符序列分成为 3 个块:
我们需要一个 $3 \times 2$ 的矩阵 $E$ 在 $GF(2^3)$ 中,其中每个 $2 \times 2$ 的子矩阵都是可逆的。这是成功的 $[3,2]$ 出错编码的关键要求:任意两个碎片都应该足以重构两个原始数据块。
一种经典的方法,如前所述,是通过选择不同的 ${x_1,\dots,x_N}$ 和 ${y_1,\dots,y_K}$ 在我们的域中并让
$$ C_{i,j} = \frac{1}{x_i - y_j}. $$
然而,有时一个“类似柯西”的矩阵或其他经过仔细选择的矩阵也可以工作——只要它满足可逆性的标准。在这个示例中,我们使用:
$$ E \;=\; \begin{bmatrix} 1 & 1\ 2 & 3\ 4 & 5 \end{bmatrix} \quad \text{in } GF(2^3). $$
以下是它为何有效:
001
、010
、011
、100
、101
)。 在实践中,你可以通过计算每对的行列式来验证子矩阵的可逆性,通过有限域算术完成。这一步通常在离线或通过代码完成,确保你拥有一个确保可逆的矩阵,而不必严格依赖于 $\frac{1}{x_i - y_j}$ 公式。
因此,尽管你可以显式构造一个矩阵来使用正式的柯西定义,但只要确保所有 $2\times2$ 的小行列式是可逆的,这就足够了。我们的示例 $\begin{bmatrix}1 & 1\2 & 3\4 & 5\end{bmatrix}$ 符合该条件,因此作为有效的 $[3,2]$ 编码器。
因此,我们可以使用:
$$ E \;=\; \begin{bmatrix} 1 & 1\ 2 & 3\ 4 & 5 \end{bmatrix}, $$ 其中每个条目都是 $GF(2^3)$ 中的一个元素。在二进制/多项式形式中:
001
($y^0$) 010
($y$) 011
($y + 1$) 100
($y^2$) 101
($y^2 + 1$)为了编码一个块 $\begin{bmatrix} x \ y \end{bmatrix}$,我们计算:
$$ \begin{bmatrix} \text{碎片}_1\ \text{碎片}_2\ \text{碎片}_3 \end{bmatrix} = E \times \begin{bmatrix} x\ y\end{bmatrix}. $$
这样每个块就生成 3 个输出碎片。让我们详细处理每个块:
块 1: [2, 2]
数据向量:$[2, 2]$
乘法:
碎片 1 = $[1, 1] \cdot [2, 2]$
000
。碎片 2 = $[2, 3] \cdot [2, 2]$
100
)。 110
)。 100
\oplus 110
= 010
= 2。 010
(十进制 2)。碎片 3 = $[4, 5] \cdot [2, 2]$
011
= 3。 001
(在十进制中即 1)。 011
\oplus 001
= 010
= 2。 010
(十进制 2)。因此,块 1 编码为碎片向量 = $[0,\; 2,\; 2]$。
块 2: [3, 2]
数据向量:$[3, 2]$
乘法:
碎片 1 = $[1, 1] \cdot [3, 2]$
011
\oplus 010
= 001
= 1。 碎片 2 = $[2, 3] \cdot [3, 2]$
110
= 6。 110
= 6。 110
$\oplus 110`` =
000` = 0。 碎片 3 = $[4,5] \cdot [3,2]$
011
$\oplus $ 100
= 111
= 7(十进制)。 111
$\oplus $ 001
= 110
= 6(十进制)。 因此,块 2 → $[1, 0, 6]$ (十进制) 即 [001, 000, 110]
(二进制)。
块 3: [5, 4]
数据向量:$[5, 4]$
乘法:
碎片 1 = $[1, 1]\cdot [5, 4]$
101
\oplus 100
= 001
= 1。 碎片 2 = $[2, 3]\cdot [5, 4]$
011
$\oplus $ 100
= 111
= 7。 001
$\oplus $ 111
= 110
(十进制是 6)。 这样,我们已经逐步翻译并符合了你希望的格式操作。- Shard 3 = $[4,5]\cdot [5,4]$
010
= 2 在十进制中。 010
= 2。 因此,块 3 → $[1,6,0]$。
将所有内容汇总,每个块的编码碎片(十进制)如下:
以二进制(每个3位)表示:
[000, 010, 010]
[001, 000, 110]
[001, 110, 000]
每一行存储在一个单独的代码碎片中(Shard 1, Shard 2, Shard 3)。如果任何单个碎片失败,你可以从剩下的两个碎片中进行解码。
假设在块 2 中我们丢失了碎片 2,但保留了碎片 1 和碎片 3:
已知:
001
(十进制 1) 110
(十进制 6) 对应的行: $E$ 中的行 #1 和 #3:
$$ E' = \begin{bmatrix} 1 & 1 \ 4 & 5 \end{bmatrix} \ (\text{在 GF}(2^3)). $$
$E'$ 的逆:
恢复原始 $\begin{bmatrix}x\ y\end{bmatrix}$ = $(E')^{-1} \cdot [\text{Shard1}, \text{Shard3}]^T$.
你会发现 $[3, 2]$ 出现,确认数据块 [3,2]
(D, C)。
这种方法与二进制数据完美匹配。没有“未使用的排列”或部分位被浪费——每个可能的 3 位字符串都是一个有效的元素。
sequenceDiagram
participant U as 用户数据
participant S as 云/存储中的碎片
participant E as 编码器/解码器
U ->> E: 提供数据块
E ->> E: 编码(GF(2^n) 中的矩阵乘法)
E ->> S: 分发 N 个碎片
Note over S: 一些碎片丢失/失败
S ->> E: 提供任何 K 余下的碎片
E ->> E: 解码(子矩阵反演)
E ->> U: 返回原始数据
让我们演示在 $GF(8)$ 中简化的 $[4, 2]$ 码。我们将展示每个步骤如何与数学相结合:
编码器构造(类似于 Cauchy 或定制设计):
给定 GF(8) 的不可约多项式 p(y),
定义大小为 (4 x 2) 的 encoder_matrix E,每个条目在 GF(8) 中。
示例(不一定是最小的):
E = [
[1, 1], # 行 0
[2, 3], # 行 1
[4, 5], # 行 2
[6, 7] # 行 3
]
编码:
function encode_block(data_vector):
# data_vector 在 GF(8) 中的长度为 2
# encoder_matrix E 规模为 4 x 2
shards = E * data_vector # 在 GF(8) 中矩阵乘法
return shards # 长度 4
解码:
function decode_shards(shards_subset, row_indices):
# shards_subset 的长度 K=2(从 4 中任意选 2 个碎片)
# row_indices 是它们来自 E 的哪些行(例如,[0,2])
E_sub = submatrix(E, row_indices) # 形状 2 x 2
E_sub_inv = invert(E_sub in GF(8)) # 在 GF(8) 中查找矩阵逆
data_vector = E_sub_inv * shards_subset
return data_vector
下面,我们在 $GF(7)$ 中进行一个显式示例。假设:
$$ E= \begin{bmatrix} \frac{1}{2} & 1 \ \frac{1}{3} & \frac{1}{2} \ \frac{1}{4} & \frac{1}{3} \end{bmatrix} \quad (\text{全操作模 }7). $$
我们在 $GF(7)$ 中解释 $\frac{1}{2}\equiv4$, $\frac{1}{3}\equiv5$, $\frac{1}{4}\equiv2$。因此:
$$ E = \begin{bmatrix} 4 & 1 \ 5 & 4 \ 2 & 5 \end{bmatrix} \ (\mod 7). $$
\begin{bmatrix} (4\times2 + 1\times2)\mod7\ (5\times2 + 4\times2)\mod7\ (2\times2 + 5\times2)\mod7 \end{bmatrix}. $$
也就是:
因此,$\text{shards}=[3,4,0]$。
$$ E'= \begin{bmatrix} 4 & 1\ 2 & 5 \end{bmatrix}. $$
计算其在 $GF(7)$ 中的逆。行列式 $\Delta = 4\times5 - 1\times2 = 20 - 2 = 18 \equiv 4 \ (\mod7)$。$\Delta=4$ 的逆是 $2$,因为 $4\times2=8\equiv1\ (\mod7)$。
逆矩阵为:
\begin{bmatrix} 3 & 5\ 3 & 1 \end{bmatrix}. $$
现在乘以 $(E')^{-1}\times[3,0]^T$:
\begin{bmatrix} 2\ 2 \end{bmatrix}. $$
我们准确恢复
$$ \begin{bmatrix}2\ 2\end{bmatrix} $$
——我们的原始数据向量。
修复开销:
当一个碎片丢失时,你可以通过从任何 $K$ 生存的碎片解码并重新编码来重新生成它。这可能是带宽密集型的。高级解决方案(如局部重建码)通过添加局部奇偶校验碎片来缓解这一点。
量子威胁:
纠删编码本身并不受到量子计算的威胁(它是线性代数)。然而,通常在其“之上”运行的加密层必须适应后量子方法。
实现复杂性:
我写过关于以 Reed Solomon 码实现以太坊 Danksharding 中的纠删编码的文章。你可以在 这里 阅读。
纠删编码架起了抽象线性代数与实际数据可靠性之间的桥梁。通过将数据分割为碎片,我们获得了:
随着数据量的激增,纠删编码变得势在必行。掌握有限域算术和矩阵力学的知识,你已经准备好实施或优化纠删编码系统。现在就应用它,以保护你的数据免受意外存储故障的影响,并且做到经济高效。
- 原文链接: github.com/thogiti/thogi...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!