内存真能当SSD用了!!!

共 3455字,需浏览 7分钟

 ·

2021-08-11 01:21

在之前的文章《阿里终面:为什么不能把SSD当内存用?》中讨论了为什么SSD不能当内存用这一话题,然后就有很多同学跑过来咨询文章中提到的新硬件。
在这里说下,大家有什么问题可以直接添加我的个人微信:coder_saver,备注”读者”即可,feel free to contact me

在这篇文章中博主提到了Intel有一款新设备,即能当内存用也能像磁盘一样持久化内存数据,很多同学咨询这种新硬件的编程问题,那么在这篇文章中小风哥就给大家聊聊这话题。

持久内存

在这里简单说下,这种新硬件就是Intel推出的傲腾持久内存,Intel Optane Persistent Memory ,这种硬件可以当做内存来用,和普通内存一样,但同时也具有非易失特性,像磁盘或者SSD一样断电后内存中的数据不会丢失,就是这么的神奇。

对于这类持久化内存来讲就真的没有重启一说了,因为数据会一直保存在内存里,加电后直接用即可,你的程序就再也没有启动或者初始化一说了,这是一种全新的设备,对程序员来说有一定的挑战。
那么针对这类硬件该如何编程呢?

面向存储编程
实际上数据结构或者说数据存放在两个地方:内存以及存储设备,这里的存储设备就是程序员熟悉的磁盘或者SSD。
对于需要将数据存放在存储设备的程序员来说,通常必须小心的维护数据的一致性,为什么呢?
对于高可靠程序来说你必须能随时应对断电或者程序崩溃,如果数据没有及时的从内存刷入磁盘,那么此时你的数据将会丢失;而如果在写入磁盘的过程中发生了断电或者程序崩溃crash,那么此时写入磁盘就是不完整的数据。

对于面向存储设备编程的程序员来说解决上述问题有一个常用的方法,那就是write-ahead logging。
你不是会随时断电或者随时程序崩溃吗,我在真正写入磁盘之前先写一段log,这段log的内容可能是这样的:“我要往磁盘中写入一句话,这句话是“小风哥太帅了!””。
那么假设在将真正的数据“小风哥太水了!”这个字符串写入磁盘的过程中机房断电或者程序崩溃,放心,在这种情况下是丢失不了数据的,此后程序在重启时通过再次读取该log:“我要往磁盘中写入一句话“小风哥太帅了!”,该程序就能获得足够的信息来再次往磁盘中写数据,这就是write-ahead logging的妙用。
到这里我先应该能大体明白这类程序员所面临的挑战了。

面向内存编程
而对于面向内存编程,也就是通常不需要关心数据持久存储问题的程序员来说也没那么容易,虽然你不需要关心数据持久存储所面临的一致性问题,但你需要在程序运行过程中解决多线程访问的一致性问题。

当多个线程访问同一段内存时,程序员通常需要加锁,这样当一个程序修改这段内存时可以确保其它线程不会看到中间状态——也就是修改到一半时的内存数据。
当断电或者程序崩溃后内存中的内容就消失了,因此你不需要去关心持久存储所面临的一致性问题。

内存数据断电后消失、程序崩溃后内存内容消失以及磁盘数据可以持久存储这些特性对于当前的程序员来说已经像空气一样习以为常了。

面向持久内存编程
现在告诉你,有一种新硬件,这种硬件能让你直接当内存来用,也就是可以直接字节寻址但与此同时断电后内容又不消失,你觉得会怎样?
我想会有很多同学大呼神奇,该技术可以让你获得大量廉价内存,同时内存中的数据在断电以及程序崩溃时内容不丢失。

但神奇的不止是这种硬件,针对该硬件进行编程同样需要编程思维上的转变。
从特性上看,该硬件即是内存又是磁盘,因此上面关于持久数据一致性以及多线程一致性的考虑都适用于该硬件,也就是说针对该硬件进行编程时你即需要考虑多线程访问一致性,也需要考虑持久数据一致性。
于此同时,最让C/C++的程序员头疼的问题之一,即内存泄漏在持久内存的场景下就更有挑战了,在普通内存下内存泄漏后大不了重启,而在持久内存场景下,如果出现了内存泄漏,那就是持久的内存泄漏,重启不再起作用
这些程序员来说一个极大的挑战。

For example
我们来看一个简单的示例:
假设这段代码出自银行的账户系统,定义了一个简单的结构体:结构体包含两项:用户姓名和账户余额:
struct account {  string name;  int money;};
当有新用户存钱时,那么需要创建一个实例然后更新姓名和账户余额:
struct account *xfg = new account();xfg->name = "xiaofengge";xfg->money = 100000000// 单位人民币
是的,你没有看错,小风哥在这段代码里已经财务自由了
这段代码在程序员看来平淡无奇如同白开水一般。
第一行代码从堆上分配一段内存用来构建data对象,后两行用来初始化各个字段,简单吧。
假设此时程序在执行到第3行时机器断电,或者系统崩溃,那么此时内存会一扫而空,不会再有mydata的数据存在,小风哥我在这家银行不会有任何信息存在,当然还包括我的1亿巨款。
但假设该程序不是运行在普通内存而是持久内存当中会怎么样呢?
让我们再来看一下这段代码:
struct data *xfg = new data();xfg->name = "xiaofengge";xfg->money = 100000000;
假设在执行到第三行时机房断电了,注意,此时程序的数据都保存在持久内存中,那么此时断电小风哥的账户名称已经保存下来了,还不错,但最重要的1亿元却没有保存下来,那么当程序再次启动时小风哥就只能看到一个空的账户了。
现在你应该意识到基于持久内存进行编程的难点了吧。

基于持久化内存编程的复杂性
程序员在基于持久内存进行编程时需要时刻意识到这是一块内存,因此需要维护多线程访问的一致性,但与此同时这又是一块存储设备,需要维护在运行时以及持久化的数据一致性。
基于此现状,当前的支持持久内存的库都支持这样一种特性,也即原子特性,atomic。

原子在不用的应用场景下有不同的语义,在多线程编程场景下,原子也即意味着除了当前线程之外没有任何一个线程更看到数据的中间状态,换句话说就是不会有多个线程同时去修改一块内存。
但原子在持久内存下的语言就不太一样了,原子在这种场景下的语义是说不管在任何时刻断电也好、程序崩溃也好,当程序重启后不会看到数据的中间状态,该数据要么已经正确的持久化要么还没有开始持久化。
这里的数据和上面一样,小到一个字节,大到一个非常复杂的结构体。
多线程中的锁只能保证内存更新的原子性,但不能保证数据持久化的原子性

解决方案
为实现数据持久化原子性,持久内存编程SDK通常从数据中借鉴一个叫做事务的概念,transaction。
事务的意思是这样的:假设某个数据可能需要经过A、B、C、D几个步骤才能修改完毕,我们把这四个步骤打包放到事务中,那么事务就可以确保这四个步骤要么全部执行完毕,要么全部都不去执行。这样即使在任意一个步骤断电或者程序崩溃都不会影响到数据的一致性问题。
如果你对持久化内存编程非常感兴趣,关注公众号码农的荒岛求生并回复pmem即可下载详细编程资料。
值得注意的是,程序员常用的磁盘flush操作只是确保当该函数执行完成后数据已经被写到了磁盘,但flush并不等同于事务,因为如果在flush过程中如果断电或者系统崩溃那么数据就处于薛定谔状态,可能数据已经被完全写到磁盘了,也可能只写了一部分,当然,也可能什么都没写。

有的同学可能不理解为什么读写磁盘时要flush,原因在于操作系统会把内存当做磁盘的缓存用,出于性能的考虑你写到磁盘中的数据并不会立即刷入磁盘,而是会有一个异步任务来完成写磁盘操作,这就是Linux下的page cache机制,关于这类机制的实现原理请参见博主的深入理解操作系统,关注公众号码农的荒岛求生并回复操作系统即可。

总结
本文介绍一种全新的内存设备,这类设备可以被操作系统识别为内存,但又像磁盘一样断电后内容不丢失,这类设备尤其适用于对内存容量要求高以及程序启动时间长的场景。
但,这类设备在编程上对程序员来说是一大挑战,这种持久内存在未来是否会成为主流也尚待观察。
浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报