Data Integrity
1. Introduction
现代文件系统具有数据和元数据校验和功能,以防止数据损坏。 然而,损坏的检测是在读取时完成的,这可能是在数据写入后几个月。 此时,应用程序尝试写入的原始数据很可能会丢失。
解决方案是确保磁盘实际上存储了应用程序想要存储的内容。 最近添加的 SCSI 系列协议(SBC 数据完整性字段、SCC 保护提案)以及 SATA/T13(外部路径保护)尝试通过添加对将完整性元数据附加到 I/O 的支持来解决此问题。 完整性元数据(或 SCSI 术语中的保护信息)包括每个扇区的校验和以及确保各个扇区以正确顺序写入的递增计数器。 对于某些保护方案,I/O 也被写入磁盘上的正确位置。
当前的存储控制器和设备实施各种保护措施,例如校验和和清理。 但这些技术在各自独立的域中工作,或者最多在 I/O 路径中的相邻节点之间工作。 DIF 和其他完整性扩展的有趣之处在于,保护格式定义良好,I/O 路径中的每个节点都可以验证 I/O 的完整性,并在检测到损坏时拒绝它。 这不仅可以防止腐败,还可以隔离故障点。
2. The Data Integrity Extensions
正如所写的,协议扩展仅保护控制器和存储设备之间的路径。 然而,许多控制器实际上允许操作系统与完整性元数据(IMD)交互。 我们一直在与多家 FC/SAS HBA 供应商合作,以实现保护信息能够在其控制器之间传输。
SCSI 数据完整性字段的工作原理是向每个扇区附加 8 字节的保护信息。 数据+完整性元数据存储在磁盘上的 520 字节扇区中。 在控制器和目标之间传输时,数据 + IMD 是交错的。 T13提案类似。
由于操作系统处理 520(和 4104)字节扇区非常不方便,因此我们联系了几家 HBA 供应商并鼓励他们允许分离数据和完整性元数据分散收集列表。
控制器将在写入时交错缓冲区并在读取时分割它们。 这意味着 Linux 可以通过 DMA 方式将数据缓冲区传入和传出主机内存,而无需更改页面缓存。
此外,SCSI 和 SATA 规范强制要求的 16 位 CRC 校验和在软件中计算起来有些繁重。 基准测试发现,计算此校验和对许多工作负载的系统性能有重大影响。 一些控制器允许在与操作系统连接时使用更轻量级的校验和。 例如,Emulex 支持 TCP/IP 校验和。 写入时,从操作系统接收到的 IP 校验和将转换为 16 位 CRC,反之亦然。 这允许 Linux 或应用程序以非常低的成本生成完整性元数据(与软件 RAID5 相比)。
IP 校验和在检测误码方面比 CRC 弱。 然而,真正的优势在于数据缓冲区和完整性元数据的分离。 这两个不同的缓冲区必须匹配才能完成 I/O。
数据和完整性元数据缓冲区的分离以及校验和的选择被称为数据完整性扩展。 由于这些扩展超出了协议机构(T10、T13)的范围,Oracle 及其合作伙伴正尝试在存储网络行业协会内对它们进行标准化。
3. Kernel Changes
Linux 中的数据完整性框架使保护信息能够固定到 I/O 并发送到支持它的控制器或从支持它的控制器接收。
SCSI 和 SATA 完整性扩展的优点是它们使我们能够保护从应用程序到存储设备的整个路径。 但同时这也是最大的缺点。 这意味着保护信息必须是磁盘可以理解的格式。
一般来说,Linux/POSIX 应用程序对其所访问的存储设备的复杂性是不可知的。 虚拟文件系统交换机和块层使硬件扇区大小和传输协议等对应用程序完全透明。
然而,在准备要发送到磁盘的保护信息时,需要这种级别的详细信息。 因此,端到端保护方案的概念本身就是一种分层违规。 应用程序知道自己正在访问的是 SCSI 还是 SATA 磁盘,这是完全不合理的。
Linux 中实现的数据完整性支持试图向应用程序隐藏这一点。 就应用程序(以及某种程度上的内核)而言,完整性元数据是附加到 I/O 的不透明信息。
当前的实现允许块层自动生成任何 I/O 的保护信息。 最终的目的是将完整性元数据计算移动到用户数据的用户空间。 源自内核的元数据和其他 I/O 仍将使用自动生成接口。
某些存储设备允许每个硬件扇区用 16 位值进行标记。 该标记空间的所有者就是块设备的所有者。 IE。 大多数情况下是文件系统。 文件系统可以使用这个额外的空间来标记他们认为合适的扇区。 由于标记空间有限,块接口允许通过交织的方式标记更大的块。 这样,8*16 位的信息可以附加到典型的 4KB 文件系统块。
这也意味着 fsck 和 mkfs 等应用程序将需要访问权限才能从用户空间操作标签。 正在为此开发一个直通接口。
4. Block Layer Implementation Details
4.1 Bio
当启用 CONFIG_BLK_DEV_INTEGRITY 时,数据完整性补丁会向结构 bio 添加一个新字段。 bio_integrity(bio) 返回指向包含生物完整性有效负载的 struct bip 的指针。 本质上,bip 是一个精简的 struct bio,它包含一个包含完整性元数据和所需内务信息(bvec 池、向量计数等)的 bio_vec
内核子系统可以通过调用bio_integrity_alloc(bio)来启用bio上的数据完整性保护。 这将分配 bip 并将其附加到 bio。
随后可以使用 bio_integrity_add_page() 附加包含完整性元数据的各个页面。
bio_free() 将自动释放 bip。
4.2 Block Device
由于保护数据的格式与物理磁盘相关,因此每个块设备都已使用块完整性配置文件(struct blk_integrity)进行了扩展。 该可选配置文件使用 blk_integrity_register() 注册到块层。
该配置文件包含用于生成和验证保护数据以及获取和设置应用程序标签的回调函数。 该配置文件还包含一些常量,以帮助完成、合并和拆分完整性元数据。
分层块设备需要选择适合所有子设备的配置文件。 blk_integrity_compare() 可以帮助解决这个问题。 目前支持 DM 和 MD 线性、RAID0 和 RAID1。 由于应用程序标签,RAID4/5/6 将需要额外的工作。
5.0 Block Layer Integrity API
5.1 普通文件系统
普通文件系统不知道底层块设备能够发送/接收完整性元数据。 在写入的情况下,IMD 将由块层在 Submit_bio() 时自动生成。 READ 请求将导致 I/O 完整性在完成后得到验证。
IMD 生成和验证可以使用以下方式切换:
/sys/block/<bdev>/integrity/write_generate
和:
/sys/block/<bdev>/integrity/read_verify
标志.
5.2 完整性感知文件系统
具有完整性意识的文件系统可以准备附加 IMD 的 I/O。 如果块设备支持的话,它还可以使用应用程序标记空间。
bool bio_integrity_prep(bio);
要为 WRITE 生成 IMD 并为 READ 设置缓冲区,文件系统必须调用 bio_integrity_prep(bio)。
在调用该函数之前,必须设置bio数据方向和起始扇区,并且bio应该添加所有数据页。 调用者需要确保在 I/O 进行时 BIOS 不发生变化。 如果由于某种原因准备失败,请完成有错误的简历。
5.3 传递现有完整性元数据
生成自己的完整性元数据或能够从用户空间传输 IMD 的文件系统可以使用以下调用:
struct bip * bio_integrity_alloc(bio, gfp_mask, nr_pages);
分配生物完整性有效负载并将其挂在生物上。 nr_pages表示完整性bio_vec列表中需要存储多少页保护数据(类似于bio_alloc())。
完整性有效负载将在 bio_free() 时释放。
int bio_integrity_add_page(bio, page, len, offset);
将包含完整性元数据的页面附加到现有简介。 Bio 必须具有现有的 bip,即必须已调用 bio_integrity_alloc()。 对于写入,页面中的完整性元数据必须采用目标设备可以理解的格式,但值得注意的例外是,当请求遍历 I/O 堆栈时,扇区号将被重新映射。 这意味着使用此调用添加的页面将在 I/O 期间被修改! 完整性元数据中的第一个引用标记必须具有值 bip->bip_sector。
只要 bip bio_vec 数组 (nr_pages) 中有空间,就可以使用 bio_integrity_add_page() 添加页面。
完成读取操作后,附加页面将包含从存储设备接收的完整性元数据。 由接收者来处理它们并在完成后验证数据完整性。
5.4 将块设备注册为能够交换完整性元数据
要在块设备上启用完整性交换,gendisk 必须注册为具有以下功能:
int blk_integrity_register(gendisk, blk_integrity);
blk_integrity 结构是一个模板,应包含以下内容:
static struct blk_integrity my_profile = {
.name = "STANDARDSBODY-TYPE-VARIANT-CSUM",
.generate_fn = my_generate_fn,
.verify_fn = my_verify_fn,
.tuple_size = sizeof(struct my_tuple_size),
.tag_size = <tag bytes per hw sector>,
};
“name”是一个在 sysfs 中可见的文本字符串。 这是用户层 API 的一部分,因此请谨慎选择,切勿更改。 格式是标准的体型变体。 例如。 T10-DIF-TYPE1-IP 或 T13-EPP-0-CRC。
‘generate_fn’ 生成适当的完整性元数据(用于写入)。
“verify_fn”验证数据缓冲区是否与完整性元数据匹配。
“tuple_size”必须设置为匹配每个扇区的完整性元数据的大小。 IE。 DIF 和 EPP 为 8。
必须设置“tag_size”来标识每个硬件扇区可用的标记空间字节数。 对于 DIF,该值为 2 或 0,具体取决于控制模式页 ATO 位的值。