首页 > 代码库 > 如何选择纠删码编码引擎 | 纠删码技术详解(上)

如何选择纠删码编码引擎 | 纠删码技术详解(上)

作者介绍: 
徐祥曦,七牛云工程师,独立开发了多套高性能纠删码/再生码编码引擎。
柳青,华中科技大学博士,研究方向为基于纠删码的分布式存储系统。

前言:
随着数据的存储呈现出集中化(以分布式存储系统为基础的云存储系统)和移动化(互联网移动终端)的趋势,数据可靠性愈发引起大家的重视。集群所承载的数据量大大上升,但存储介质本身的可靠性进步却很小,这要求我们必须以更加经济有效的方式来保障数据安全。

副本与纠删码都是通过增加冗余数据的方式来保证数据在发生部分丢失时,原始数据不发生丢失。但相较于副本,纠删码能以低得多的存储空间代价获得相似的可靠性。比如 3 副本下,存储开销为 3,因为同样的数据被存储了三份,而在 10+3(将原始数据分为 10 份,计算 3 份冗余)的纠删码策略下,存储开销为为 1.3。采用纠删码能够极大地减少存储系统的存储开销,减少硬件、运维和管理成本,正是这样巨大的收益驱使各大公司纷纷将纠删码应用于自己的存储系统,比如 Google、Facebook、Azure、EMC 等等国际巨头,在国内以淘宝、华为、七牛云等为代表的公司也在自己的存储系统上应用了纠删码。

最典型的纠删码算法是里德-所罗门码(Reed-Solomon 码,简称 RS 码)。RS 码最早应用于通信领域,经过数十年的发展,其在存储系统中得到广泛应用,比如光盘中使用 RS 码进行容错,防止光盘上的划痕导致数据不可读;生活中经常使用的二维码就利用了RS 码来提高识别的成功率。近年 RS 码在分布式存储系统中的应用被逐渐推广,一方面是分布式存储系统存储的存储容量和规模增大的需求;另一方面是由于纠删码编码速度在近年得到迅猛提升。随着对高性能纠删码引擎在实际系统中应用需要,也催生了对纠删码在具体系统中实现的各种优化手段。并为相关的决策者带来了困扰——究竟什么样的编码引擎才是高效的呢?

我们将以这个问题展开对纠删码技术的剖析,帮助企业更全面,深入的了解纠删码在存储系统中的应用并更好地做出技术选型。本系列文章将从纠删码的基本原理开始,随后引出如何判断编码引擎优劣这个问题,接下来将深度分析代码实现,帮助开发者顺利完成定制开发。

本系列共计上下两篇篇文章:

(上篇)如何选择纠删码编码引擎

(下篇)实现高性能纠删码引擎

本文作为系列首篇,我们将一起探讨纠删码的编码原理与如何选择编码引擎这两个问题。

一 、纠删码编码原理

在展开分析之前,我们先来看一看 RS 码是如何工作的。

下图展示了 3+2(3 份数据,2 份冗余)下对 2 字节长度的数据进行编码与数据修复过程:

技术分享






为了计算冗余数据,首先我们需要选举出一个合适的编码矩阵。编码矩阵的上部为一个单位矩阵,这样保证了在编码后原始数据依然可以直接读取。通过计算编码矩阵和原始数据的乘积,可以到最终的结果。

下面介绍解码过程,当 1,2 两块数据丢失,即:

技术分享

当数据块发生丢失,在编码矩阵中去掉相应行,等式仍然保持成立。这为我们接下来恢复原始数据提供了依据。

原始数据的修复过程如下:

技术分享

为了恢复数据,首先我们求剩余编码数据的逆矩阵,等式两边乘上这个逆矩阵仍然保持相等。与此同时,互逆矩阵的乘积为单位矩阵,因此可以被消掉。那么所求得的逆矩阵与剩余块的数据的乘积就是原始数据了。

数据编码以字节为单位,如果将被编码数据看做一个「数组」,「数组」中每个元素是一个字节,数据按照字节顺序被编码。编码过程是计算编码矩阵中元素和「数组」的乘积过程。为保证乘积的运算结果仍旧在一个字节大小以内(即 0-255),必须应用到有限域[1]。有限域上的算术运算不同于通常实数的运算规则。我们通常事先准备好乘法表,并在算术运算时对每一次乘法进行查表得到计算结果。早期的编码引擎之所以性能不佳,是因为逐字节查表的性能是非常低的。倘若能一次性对多字节进行查表以及相应的吞吐和运算,引擎的工作效率必将大幅度提升。

许多 CPU 厂商提供了包含更多位数的寄存器(大于 64 位),这类寄存器和相应支持的运算使得用户程序可以同时对大于机器位数的数据进行运算,支持这类寄存器和运算的指令称之为SIMD(Single Instruction Multiple Data)指令集,比如 Intel 支持的 SSE 指令集最大支持 128 bits 的数据运算,AVX2 指令集最大支持 512 bits 的数据运算。它们为我们对一个「数组」数据分别执行相同的操作,提高了数据运算的并行性。目前,市面上所有高性能的纠删码引擎均采用了该项技术以提高编解码性能。

二、编码引擎评判标准

我们将从以下几个关键指标来对编码引擎进行分析:

1、 高编/解码速度;

2、参数可配置;

3、编码速度稳定性;

4、代码简洁、稳定;

5 、降低修复开销等。

2.1 高编/解码速度

上文提到,依赖于SIMD 技术 RS 码编码性能有了大幅度的提高。其中,我们可以利用多种指令集扩展以供加速,引擎应该能自动根据 CPU 的特性而选择最优的指令集扩展进行加速。

速度是最基本的要求。不过在这里我很难给出一个绝对的数字来衡量速度,因为其受参数,运行平台的影响极大。在下文中提到的三款引擎均有出色的性能表现,可以以它们为基准来衡量引擎的编码速度。除此之外,我们还可以将逐字节查表(下称基本方法)的编码速度与利用 SIMD 技术加速的编码速度做对比,两者之间应该有非常直观的差距。以我的个人电脑为例(i5-4278U 2.6GHz),在 10+4 的策略下(每个数据块大小为 128KB),基本方法的速度为(原始数据总量/编码耗时)318.1 MB/s,而通过 AVX2 指令集加速后达到了 5558.6 MB/s[2],在 SSSE3 指令集的加速下也有 2978.87 MB/s 。

另外,解码速度应该大于或等于编码速度(视丢失的数据块数量而定),下图截自在我本机上运行的修复原始数据块的性能测试结果:

技术分享

2.2 参数可配置

一款合理的纠删码引擎必须能做到编码策略在理论范围内可随意切换,这指的是如果要将编码策略进行变化时,仅需从接口传入不同参数而不需要改动引擎本身。这大大降低了后续的开发和维护所需要的精力。一个可配置参数的编码引擎可以根据数据的冷热程度和数据重要程度选择不同的编码系数,比如可靠性要求高的数据可以选择更多冗余。

2.3 编码速度稳定性

速度的稳定性指的是对于不同尺寸的数据块会有相近的性能表现。由于系统缓存的影响,当被编码数据的大小和缓存大小相当时,编码应该具有最快的速度。当编码数据的大小大于缓存大小时,内存带宽成为编码速度的瓶颈,文件大小和编码时间呈现近似线性关系。这样,数据编码时间是可预期的,用户的服务质量也是可保障的。在实际中,我们对于大文件进行定长分块,依次编码,分块大小和缓存大小保持一定关系:以 10+4 编码方法为例,对比数据块尺寸分别为取 L3 Cache Size 的 1/12 以及 12 倍。如 L3 Cache Size 的大小为 12MB,则每一块的数据尺寸分别取 1MB,144MB。倘若大数据块下编码速度远远低于小数据块,则说明该引擎 CPU cache 的优化工作做得不充分。对于上述参数来说,大数据块的速度应该不低于小数据块的 70% 。同样以我的个人电脑为例(L3 Cache 大小为 3MB):

技术分享

2.4 代码简洁、稳定

为了利用 SIMD 加速我们不得不引入汇编代码或者封装后的 CPU 指令,因此代码形式并不常见。为了增强可读性可将部分逻辑抽离到高级语言,然而会损失部分性能,这其中的利弊需要根据团队的研发实力进行权衡。

接下来的可维护性也非常重要。首先是接口稳定,不会随着新技术的引入而导致代码大规模重构;另外代码必须经过有合理的测试模块以便在后续的更新中校验新算法。

比如早先的 SIMD 加速是基于 SSE 指令集扩展来做的,随后 Intel 又推出 AVX 指令集进一步提高了性能,引擎应该能即时跟上硬件进步的步伐。在比方说,再生码(可以理解为能减少修复开销的纠删码)是将来发展的趋势,但我们不能因为算法的升级而随意改变引擎的接口。

2.5 降低修复开销

纠删码的一大劣势便是修复代价数倍于副本方案。k+m 策略的 RS 码在修复任何一个数据块时,都需要k 份的其他数据从磁盘上读取和在网络上传输。比如 10+4 的方案下,丢失一个数据块将必须读取 10 个块来修复,这个修复过程占用大量磁盘 I/O 和网络流量,并使得系统暴露在一种降级的不稳定状态。因此,实际系统中应该尽量避免使用过大的 k 值。

再生码[2] 便是为了缓解数据修复开销而被提出的,它能够极大减少节点失效时所需要的吞吐的数据量。然而其复杂度大,一方面降低了编码速度,另外一方面牺牲了传统 RS 码的一些优秀性质,在工程实现上的难度也大于传统纠删码。

三、著名引擎对比

目前被应用最广泛并采用了 SIMD 加速的引擎有如下几款:

  1. Intel 出品的 ISA-L[4]

  2. J.S.Plank 教授领导的 Jerasure[4]

  3. klauspost 的个人项目(in Golang)[6]

这三款引擎的执行效率都非常高,在实现上略有出入,以下是具体分析:

3.1 ISA-L

纠删码作为 ISA-L 库所提供的功能之一,其性能应该是目前业界最佳。需要注意的是 Intel 采用的性能测试方法与学术界常用的方式略有出路,其将数据块与冗余块的尺寸之和除以耗时作为速度,而一般的方法是不包含冗余块的。另外,ISA-L 未对 vandermonde 矩阵做特殊处理,而是直接拼接单位矩阵作为其编码矩阵,因此在某些参数下会出现编码矩阵线性相关的问题。好在 ISA-L 提供了cauchy 矩阵作为第二方案。

ISA-L 之所以速度快,一方面是由于 Intel 谙熟汇编优化之道,其次是因为它将整体矩阵运算搬迁到汇编中进行。但这导致了汇编代码的急剧膨胀,令人望而生畏。

另外 ISA-L 支持的指令集扩展丰富,下至 SSE,上到 AVX512,平台适应性最强。

3.2 Jerasure2.0

不同于 ISA-L 直接使用汇编代码,Jerasure2.0 使用 C 语言封装后的指令,这样代码更加的友好。另外 Jerasure2.0 不仅仅支持 GF(2^8) 有限域的计算,其还可以进行 GF(2^4) - GF(2^128) 之间的有限域。并且除了 RS 码,还提供了 Cauchy Reed-Solomon code (CRS 码)等其他编码方法的支持。它在工业应用之外,其学术价值也非常高。目前其是使用最为广泛的编码库之一。目前 Jerasure2.0 并不支持 AVX 加速,尽管如此,不过在仅使用 SSE 的情况下,Jerasure2.0 依然提供了非常高的性能表现。不过主要作者之一 James S. Plank 教授转了研究方向,另外一位作者 Greenan 博士早已加入工业界。因此后续的维护将是个比较大的问题。

3.3 klauspost 的 ReedSolomon

klauspost 利用 Golang 的汇编支持,友好地使用了 SIMD 技术,此款引擎的 SIMD 加速部分是目前我看到的实现中最为简洁的,矩阵运算的部分逻辑被移到了外层高级语言中,加上 Golang 自带的汇编支持,使得汇编代码阅读起来更佳的友好。不过 Go 并没有集成所有指令,部分指令不得不利用 YASM 等汇编编译器将指令编译成字节序列写入汇编文件中。一方面导致了指令的完全不可读,另外一方面这部分代码的语法风格是 Intel 而非 Golang 汇编的 AT&T 风格,平添了迷惑。这款引擎比较明显的缺陷有两点:1.对于较大的数据块,编码速度会有巨大的下滑;2.修复速度明显慢于编码速度。

四、自己实现一款引擎

可能是由于对开源库后续维护问题的担忧,也有可能是现有方案并不能满足企业对某些特定需求和偏好,很多公司选择了自研引擎。那么如何写出高效的代码呢?在上面的简单介绍中,受限于篇幅我跳过了很多细节。比如 SIMD 技术是如何为纠删码服务的,以及如何利用 CPU Cache 做优化等诸多重要问题。我们会在后续的文章中逐步展开其实现,欢迎大家继续关注。


附录:

  1. 许以超 马松雅. 代数编码与密码[M]. 北京:高等教育出版社, 2015.

  2. 徐祥曦 Reed-Solomon

  3. Alexandros G Dimakis, P Godfrey, Yunnan Wu, Martin J Wainwright, and Kannan Ramchan-dran. Network coding for distributed storage systems. Information Theory, IEEE Transactions on, 56(9):4539–4551, 2010.

  4. Intel ISA-L

  5. Jerasure

  6. klauspost Reed-Solomon

如何选择纠删码编码引擎 | 纠删码技术详解(上)