技术与生活 性能之重 持久内存编程指南—乱译连载 (4. 持久内存编程基础概念)

持久内存编程指南—乱译连载 (4. 持久内存编程基础概念)

本章是持久内存编程概念概览。当开发使用持久内存的应用时,必须考虑下面几方面:1) 原子更新;2) FLUSH不是事务的;3) 应用启动时的职责;4) 硬件配置调优。在一个产品级应用中应对好以上挑战,需要更复杂的编程、广泛的测试,以及性能分析。

4  持久内存编程基础概念

第3章讲了操作系统如何通过内存映射文件方式把持久内存暴露给应用。本章建立在这个基础模型上,讨论编程挑战。理解这些挑战是持久内存编程的必要部分,尤其是当设计一个在应用中断(如crash、断电等)后恢复策略时。不过,不要因为害怕这些挑战,而阻止您进行持久内存编程!第5章 描述了如何利用现有的解决方案来节省时间,并且减少编程复杂度。

4.1 有什么不同

应用程序员通常以内存数据结构和存储数据结构来思考。对于数据中心应用,开发者需要很小心地维护存储上数据结构的一致性,在面临系统崩溃时更是如此。通常使用日志技术(如:预写式日志 write-ahead logging)来解决该问题,更新首先写到日志,然后刷到持久存储。当数据修改进程中断后,重新启动时应用可以依赖日志中足够的信息来完成操作。该技术已经有许多年历史了,其挑战是正确的实现以及减少维护该日志的耗时。开发者经常依赖于提供一致性的数据库、开发库和现代文件系统的组合来达到一致性。尽管如此,根本上还是需要应用开发者对存储上、运行时、系统崩溃恢复时数据结构一致性的设计策略负责。

与存储上数据结构不同,应用开发者需要维护运行时内存数据结构的一致性。当应用需要多线程访问相同的数据结构时,通常会使用锁技术,目的是当一个线程对一个数据结构执行复杂的的更改时,另一个线程不能只看到一部分变更。当应用程序退出或者崩溃时,或者系统崩溃时,内存内容会丢失,所以需要像维护在存储上的数据结构一样,维护内存数据结构在多个应用运行之间的一致性。

上面的解释显而易见,但这些是假设存储状态在多个运行应用上状态保持一致,内存内容是易失的。此假设在应用程序的开发方式中是如此基础,以至于大多数开发人员都从没有考虑太多事情。当然,持久内存的不同之处在于它是持久的,所以存储和内存的所有考虑都适用。应用程序负责维护运行和重新启动之间的数据结构一致性,以及使用线程安全锁维护内存数据结构的一致性。

如果持久性内存和存储一样具有这些属性和需求,为什么不使用多年来开发的代码来存储呢?这种方法确实有效;在持久内存上使用存储api是我们在第3章中描述的编程模型的一部分。如果持久内存上现有的存储api足够快,并且能够满足应用程序的需要,则无需进一步工作。但是为了充分利用持久内存的优势,数据结构在持久性上读写到位,访问是在字节粒度上进行的,而不是使用块存储堆栈,应用程序将希望内存映射它并直接访问它。这消除了数据路径中基于缓冲区的存储api。

4.2  原子更新

【8字节对齐才能提供原子性保护】

支持持久内存的每个平台都有一系列原子性的本地内存操作。在Intel硬件上,原子持久存储是8字节。因此,当存储到持久内存的一个对齐的 8字节处于in-flight状态时,如果程序或者系统crash,在恢复这些8字节后,要么包含旧内容,要么是新内容。Intel处理器有指令可以存储超过8字节,但这些不提供失效原子性保护,所以在遇到如断电类失效事件时,这些字节可能被断开剩一部分。

【多条指令不能保证原子性】

有时更新内存数据结构需要多条指令,所以自然这些更新在遇到断电失效时被撕裂,因为电源可能在任何两个指令间失去。运行时锁阻止其他线程看到部分已完成的变更,但锁不能提供任何失效原子性保护。当应用需要变更超过8字节到持久内存时,必须基于硬件提供的基本原子性能力(如 Intel硬件提供的8字节失效原子性)构建一个原子操作。

4.3  事务

组合多条指令形成一个原子操作称作事务。在数据库领域,缩略词ACID描述了事务的特性:原子性、一致性、隔离性、持久性。

4.3.1  原子性

原子性指多个操作组成一个单一原子性动作,要么全部完成,要么全不完成,甚至面对系统失效时也是如此。对于持久内存,常用技术有:

  • Redo日志,所有变化首先写到日志,所以当恢复时,可以向前重做。
  • Undo日志,记录信息到日志,通过这些信息,可以在恢复时把只做了部分的更新回退;
  • 原子指针更新,通过原子性地更新一个单一指针来使得变化的位置生效,通常把指针从指向旧数据更新到指向新数据。

上表列得不全,也忽略了更为复杂的细节。

一个通常要考虑的事儿是,事务经常包含内存分配与释放。例如:添加一个节点到树数据结构通常包含新节点的分配。如果一个事务回滚了,内存必须被释放以防止内存泄露。现假设一个执行多持久内存分配和释放操作的事务,其中的每个操作都必须是相同原子操作的一部分。很明显,这种事务的实现比写新值到日志或者更新一个单一的指针更为复杂。

4.3.2 一致性

是指事务只把数据结构从一个有效状态迁移到另一个。对于持久内存,程序员通常会使用锁来保障更新是线程安全的,这也经常间接实现了一致性。如果一线程无法看到中间状态,是因为锁阻止了该线程的访问,而当锁被移除时,数据访问又是安全的了,这就是为什么从另一个线程来观察当前数据结构的状态是安全的了。

4.3.3 隔离性

多线程(并发)执行在现代应用程序中很常见。在进行事务性更新时,隔离允许并发更新具有与按顺序执行相同的效果。在运行时,持久内存更新的隔离通常是通过锁定来实现的。由于内存是持久性的,因此必须为应用程序中断时正在运行的事务考虑隔离。持久内存程序员通常在重新启动时检测这种情况,并在允许通用线程访问数据结构之前适当地向前重做或向后回滚事务。

4.3.4 持久性

如果一个事务当它在持久媒体上并且完成时,被认为是持久的。即使当系统断电或者crash,事务也是保持完成状态。这通常意味着变更必须从CPU cache中flush完成。这通过使用标准API,如: Linux msync() 调用或者特定平台指令如:Intel的 CLWB 来完成。

持久内存内存映射文件就是一个原数据的range,应用怎样才能找到range里的数据结构?必须有一个从开始点计数的数据结构的位置。这经常称作root object(第7章会描述)。Root object 经常被PMDK中的高层库使用来访问数据。

4.4  Flushing不是事务的

区分FLUSH与事务更新这两个概念的不同很重要。我们会调用msync() or fsync() 和 FlushFileBuffers() 来将更新的数据FLUSH到存储,但这些FLUSH从不保障更新是事务性的。应用在负责FLUSH变更到存储之外,还要负责维护存储数据结构的一致性。在使用持久内存时,也是如此。第3章中的简单程序存储了一个string到持久内存中并执行FLUSH而持久化。但那些代码并不是事务的,当失效时,该数据变化可能处于任何状态,可能是部分提交、部分丢失、全部提交。

Cache的基本特性就是临时持有数据以提高性能,但在一个事务准备提交前,他们一般不持有数据。正常的系统活动也会引起cache压力,并且在任何时间以任何顺序驱逐数据。在第3章中的电源失效中断例子,有可能使被存储的string丢失或者部分丢失。考虑cache的flush操作就相当重要了。如第2章的决策树(图Figure 2-5)所示,应用在启动时会检测平台特性,使用该决策树以确定在持久内存上是否需要cache flush。例如有些平台场景,在断电时,CPU cache会自动flush。甚至有些平台上flush指令都不需要,但在面临失效时,事务却一直需要以保持数据一致性。

4.5  启动时职责

如第2章中流程图(Figures 2-5 和2-6)所示,概括了应用使用持久内存的职责。这些职责包括:检测平台细节、可用的指令集、媒体故障等等。对于存储,这些类型也会在数据系统的存储栈上发生。一旦持久内存上的文件是内存映射的,可以跳过kernel直接访问其上的数据。

作为一个程序员,容易被诱惑映射持久内存并且开始使用它,如第3章例子所示。对于要达到产品化质量的编程,你应该确保程序履行了这些启动时的职责。例如,如果你跳过了图Figure 2-5中的检查,那么即使不需要flush CPU cache,你的应用也可能自始至终地在做该操作,或者需要该操作,你却没做。如果你跳过了图Figure 2-6中的检查,你的应用将忽略掉媒体故障,从而引起非预期的错误数据结果以及未定义的行为。

4.6  硬件配置调优

当向持久内存中存储巨大的数据结构时,有几种方法来copy数据并且持久化:

  1. 使用常用的存储操作然后flush cache;
  2. 使用专用指令,像intel的non-temporal 存储指令,该指令绕过了CPU cache。

持久内存写性能比常规内存慢,所以你应该设法使写入持久内存更有效,在存储到持久内存之前,通过组合更多的小数据形成更大的数据,一起写入。对于持久内存最优写入的大小依赖于持久内存产品本身及其所插入的平台。这些例子展示了使用持久内存时不同平台的不同特性,并且任何产品化质量应用都将在意向目标平台调到最优。自然地,帮我们做此项调优工作的一个方法是利用那些已经被调优并且有效的库或者中间件。

4.7  本章小结

本章是持久内存编程概念概览。当开发使用持久内存的应用时,必须考虑下面几方面:

  1. 原子更新;
  2. FLUSH不是事务的;
  3. 应用启动时的职责;
  4. 硬件配置调优。

在一个产品级应用中应对好以上挑战,需要更复杂的编程、广泛的测试,以及性能分析。下一章介绍PMDK,设计用来帮助应用开发者解决上面的挑战。

作者: charlie_chen

编程是一生最爱: >> 架构与设计; >> 软件工程; >> 项目管理; >> 产品研发。
联系我们

联系我们

022-XXXXXXXX

在线咨询: QQ交谈

邮箱: 1549889473@qq.com

欢迎交流。
关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部