阿里内核月报2014年4月-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

阿里内核月报2014年4月

简介: 在2014年LSF&MM峰会上,Dave Chinner和Ted Ts'o一起主持了一个跨越2天而占用2个时间段的会议。 这个会议的主题是,是文件系统还是块设备层才是支持SMR设备的正确接口。最后,它的讨论范围 有点超出这个主题。

Ideas for supporting shingled magnetic recording (SMR)

在2014年LSF&MM峰会上,Dave Chinner和Ted Ts'o一起主持了一个跨越2天而占用2个时间段的会议。 这个会议的主题是,是文件系统还是块设备层才是支持SMR设备的正确接口。最后,它的讨论范围 有点超出这个主题。

Zone信息接口和缓存

Ts'o一开始就描叙了他正在处理的一个SMR设备返回的Zone信息kernel级别的C接口。SMR设备将会 报告驱动器中存在的Zones, 它们的特性(大小,仅串行,。。。)和对于每个仅串行Zone写指针 的位置。Ts'o的想法是以紧凑的形式在内核中缓存这些信息,这样就不需要向设备发送多个"report zones"命令。代替地,举例说,感兴趣的内核子系统能查询Zones大小和它的写指针的位置。

Ts'o说用户接口是ioctl(),但James Bottomley认为sysfs接口更有意义。Chinner担心sysfs将会有 成千的条目,Ric Wheeler提醒一个跟定的设备可能实际上有上万的Zones.
Ts'o说他正在用的数据结构假定Zones大部分都被成组成大小相同的Zone区域。这个接口将支持其他 设备布局。Zach Brown想知道内核为什么需要缓存这些信息,既然这也许要求探寻SCSI总线,寻找 重置写指针命令。没有人认为探寻总线是可靠的,但一些人认为不允许访问裸SCSI貌似合理的。 Bottomley泼冷水Scsi sg层bypass Ts'o的缓存。

一个关于如何处理主机管理的设备的问题出现了(主机必须确保所有写串行Zones都是串行的)。Ts'o 说他在主机感知的设备中看到了可怕的一秒延迟(主机能犯错,转换层将重映射非串行写,它能导致 垃圾回收和可怕的延迟),这意味着用户将想Linux支持主机管里的行为。那将避免了主机感知的设备 上出现的延迟。

但是,正如Chinner指出的,在用户空间有一些不能改变的固定布局。举例说,mkfs零化分区末端, 并且SMR设备必须能与之工作。他是高度怀疑主机管理的设备将与Linux完全工作。今天Linux上没有什么 能运行在主机管里的SMR设备上。但那些设备将可能生产起来更便宜,因此它们将是可用的,并且用户向 支持它们。就主机管理的设备vs主机感知的设备问题轮询了屋内设备制造者们,最后也没有太大的结论。

Ts'o建议使用设备映射在内核中创建转换层来支持主机管理的设备。“我们修复缺陷比供应商推出新的 固件更快”。但正如Chris Mason指出的,任何新的设备映射层对用户可用都不会超过3年,但未来有需求 支持2种SMR设备。第一个会议在这就超时了,没有太多实际的结论。
当Ts'o再一次捡起这个话题的时候,他向前迈出了更多。在许多场景下,块设备正在做更多的事情。举例 来说,SMR和带有dm-thin的瘦provisioning.文件系统为基本的旋转型驱动器所做的优化布局在其他场景 下是不明智的。对于SSD驱动器,转换层和驱动器是如此的快以致文件系统不需要关心转化层和驱动器固件 中所做的。对于SMR和其他场景,那也许不是真的,因此有必要重新思考一下文件系统层。

将文件系统一分为二

这是Chinner关于文件系统思考的关键。他提醒到他已经开始写一些东西,并且对其他的建议和想法是开放的, 但他想到一些关于他思考的反馈。一个文件系统真正是由2个层组成:名字空间层和块分配层。Linux文件 系统已经做了许多工作来为旋转型驱动器优化块分配,但有其他类型的设备,SMR和永久内存,它们的优化就 有些落后了。

因此,为了优化各种类型设备的块分配,从文件系统名字空间处理中分拆出块分配是由意义的。名字空间部分 将保持不变,所有的块分配部分移入一个智能块设备中,它能知道底层设备的特性并能相应地分配块。

名字空间层希望一组分配是连续的,但块分配层可能基于它的知识改写这些决定。假如它正在SMR设备上写块, 意识到它不能在连续的位置上写数据,它将返回附近的块。对于旋转型媒介,它将返回连续块,但对于永久 内存,我们不关心,因此,它仅返回一些便利的块。任何不支持COW的文件系统都不能为SMR做优化,因为你不 能在串行Zones中改写数据。那意味着需要向ext4和XFS中添加COW功能。

但将文件系统一分为二意味着盘上格式能改变。名字空间层所关心的是它上面的元数据是一致的。但Ts'o抛出 了一个问题:它与存在15年之久的基于对象的存储有怎样的不同?
Chinner说他不准备将像文件和inodes等东西移到块分配层。那将仅是分配和回收块的层。他问,为什么在每 个文件系统中为不同种设备优化块分配?

Chinner的想法和基于对象的存储另一个不同是元数据与文件系统呆在一块儿,不像基于对象的存储那样将它 们移到设备上。Chinner说他将不寻求分配一个能连接属性的对象,仅创建为特定设备优化的分配器。一旦那 被实现了,在多个文件系统间共享分配器是有意义的。

Mason提醒到Chinner描述的东西非常像FusionIO DirectFS文件系统。Chinner说他并不惊奇。他寻找但没有 发现太多关于DirectFS的文档和过去其他人提到这些想法。它不必是新的,但他正在试着将它作为解决现存 的问题的一种方法。

Bottomley问如何得到一些我们能测试的东西。Chinner原以为需要半年的时间,但目前看来在它能工作之前, 还有许多工作要做。他问“我们应该采用这种方法?”,Wheeler认为它值得期待;它避免了重复,并且利用 新设备的优点。其他人也都表示乐观,但他们希望Chinner当他做的时候时刻记住基于对象的存储为什么不 工作的原因。Chinner认为大约在6到12个月后一个POC将会出现。

Data integrity user-space interfaces

今年LSF/MM 2014上,Darrick Wong和Zach Brown主持了一个关于数据完整性(DIF)用户态接口的讨论。这一用户态接口的相关代码Darrick已经发补丁提交到社区了。这一接口使用DIF规范来为相关的块数据添加必要的校验和,以方便用户检查数据的损坏。

在去年的LSF/MM峰会上,Darrick也曾讨论过DIF/DIX相关的话题,但是当时没有任何代码给出。今年,他编写了一套相关的接口来实现此功能。James Bottomley认为提供这样的借口给用户会让用户过多的了解系统内部的信息,这样做过于复杂了。而Martin Petersen则认为没必要暴露底层借口给用,而是用库函数来封装相应的接口来方便用户的使用。

Ted Ts'o指出目前的借口需要扩展aio中的iocb数据接口,后面他指出了他不太喜欢这样做的原因。而Zach则认为如果用户需要将自己的数据放入iocb中,则必须要对这个接口做必要的修改。但这一工作需要不同用户之间的协调。Kent Overstreet倾向于定义一个新的系统调用来完成这一工作,而不是使用已有的aio相关接口。但是Darrick则更倾向于使用已有接口。

Ted指出他对扩展iocb结构有顾虑的原因是google内部实现了一个自己的I/O调度器,而这个调度器也对iocb结构进行了修改。所以对iocb结构的修改会造成google内核代码rebase时候的麻烦。而同时,Ted指出google也没有打算把他们I/O调度器的相关代码贡献给社区的计划。Darrick说能不能在iocb中添加一个标志为来标识具体哪些成员被使用。看来这块还需要更多的协调工作。不过Ted说原则上他不反对这个用户态接口。

Copy offload

2014年的LSFMM峰会上Martin和Zach介绍了copy offload的进展,这个特性主要是在服务器的CPU和网络不参与的条件下直接拷贝数据。Hannes和Doug同时也和大家同步了copy offload的一些额外的option。

Martin和Zach的演讲题目是 "Copy Offload: Are We There Yet?"(看到这个题目,我只能呵呵了,每年都炒,每年都没啥进展)。Martin想和大家说“是的,谢谢”,但是似乎大家还是一如既往的很感兴趣,所以Martin给大家介绍了一下他做的一些工作,比如基于Hanne3s的VPD(vital product data)重写模块,现在接口已经很简洁,只包含了源和目标设备以及各自的便宜量,当然还有拷贝的大小(以block为单位)。内部的实现目前是使用了SCSI XCOPY命令,因为这个目前基本被所有的大厂商支持。

如果底层存储设备支持的很好的话,copy offload可以轻而易举得拷贝大量数据,于是乎有人问samba的支持如何。Zach的回应是需要一个新的接口,这个接口会使用文件描述符以及文件范围来操作。但是由于copy offload这个操作可能会导致你的拷贝部分成功,所以用户态程序需要处理这个问题。

Error handling

Hannes Reinecke在今年的LSFMM上带来了两个和块设备层错误处理相关的话题。第一个是分区扫描错误,第二个是SCSI错误处理路径的彻底重构。

Reinecke已经添加了许多更加细致的错误码,用于帮助诊断问题。这期间他碰到一个问题,EMC的驱动在分区扫描至磁盘末尾时返回ENOSPC。而他更希望看到的是和其他七个内核分区扫描一样的返回ENXIO。因此Reinecke在SCSI代码里面将该错误重新映射为EXNIO。否则的话这段代码处理会有问题,因为ENOSPC指的是达到了上限。

Al Viro提出了他的担忧,重新映射后的错误码将会扩散到用户态,给很多工具带来很多困惑。Reinecke让他确信重新映射后的错误码仅会传播到块设备层。能够区分实际的IO错误和扫描到磁盘末尾,将使得分区扫描器能够在探测到IO错误之后停止扫描。

在另外一个讨论上,Reinecke给出一个关于从SCSI错误在各个level恢复(LUN, target, bus, ...)的提议。他认为部分层面直接reset没有任何意义,而是应该看具体探测到的错误进行判断。例如,如果target是不可达的,对LUN进行reset,或者对bus都是无意义的。而应该尝试重新传输一次,假如再失败,那么将需要进行机器重启。这才是一个命令超时或者返回出错所应该走的路径。

针对这个,底下听众有大量的抱怨,主要是此时并不是必须要重启。当其中一个LUN或者target出现错误,其他LUN的传输链路将被破坏,即使他们处理IO正常。部分问题是源于LUN的reset命令不会time out,Reinecke说到。

但是Roland Dreier注意到一个丢失的IO可能导致整个存储阵列需要reset,这将会花费一分多钟处理。此外,一旦进入错误处理路径,所有到该机器的有问题IO都会停止等待。在一些大存储阵列里面,丢失一个包可能导致很长一段时间没有任何IO。Reinecke争论到命令会重试,但是承认一个严重的错误确实是会导致这种情况发生。

当然,让事情复杂的进一步还有,存储厂商对不同的错误都各自进行不同的处理。对一个厂商的错误恢复步骤可能和另外一个厂商的相同,也可能不同。最终,似乎大家都同意,Reinecke的修改将会使得事情比现在更好,是正确道路上往前的一步。

A revoke() update and more

在今年的LSF/MM峰会上Al Viro介绍了系统调用revoke()的最新进展。revoke()的作用就是将一个文件名对应的所有文件句柄都关闭,这样调用进程就知道他能够独享这个文件名对应的文件或者设备,在这个topic中Al顺便介绍了一下他对read()和write()各个变种的统一工作。

Viro开始的时候提到revoke()是他这个session里面最没啥意思的,因为代码基本已经ok,实现也很简单,文件在打开的时候如果声明是可以revoke的话就会增加一个引用计数,这样如果revoke()被调用了,他就会阻塞等待直到所有打开的文件句柄都关闭(也就是说引用计数变成0),同时保证后面的任何open都会失败。

目前在procfs以及sysfs里面已经有些类似的逻辑,这些等到revoke合并以后就会被干掉。实现revoke的一个重要前提是它不能对正常的路径有任何的性能损耗,因为大部分应用都不会使用这个特性,目前来看poll()和mmap()还有一些问题。Viro在做这个的同时注意到内核里面很多代码都有bug,囧。比如如果一个debugfs里面的一个文件在打开以后被删除了,对这个打开句柄的任何read/write操作都会使内核挂掉(真的,假的,我了个去)。动态debugfs就是一个杯具,所以Viro希望revoke()在debugfs里面能够工作的ok。

接下来Viro提到了他对read/readv/splice_read以及write/writev/splice_write代码的统一工作。目前看read/readv以及write/writev已经合并得差不多了,而splice*目前看很糟糕。在理想情况下,这些变种应该在上层路径上保持一致,一直到处理函数才有各自的区别。但现实情况是他们对数据有不同的视图,splice*系列的函数的数据是放置在page里面的,而read/write则是放置在iovec里面,也许需要创建一个新的数据结构来包装这些。

另一个问题是iov_iter。iov_shorten()会尝试重新计算iovec里面对应的网络包的个数,所以如果存在short read/write,iovec就会被修改。更糟糕的是,iovec的修改是和协议相关的,这个对用户非常不友好。事实上,CIFS的一个哥们火上浇油说CIFS每次都是拷贝一份iovec因为它也不知道底层会对iovec做什么样的修改。Viro最后总结说iovec就应该和协议无关,所以他正在干掉iov_shorten以及其他会缩小iovec的地方,这个可能最终会导致sendpage()被干掉。

Thin provisioning

Thin provisioning

(Eric Sandeen、Lukáš Czerner、Mike Snitzer和Dmitry Monakhov在2014年的LSFMM上讨论thin provisioning的问题。Thin provisioning是什么意思想必不用多说,但如何翻译倒不太一致,正式文件中居然是。。。自动精简配置??好吧,还是叫超卖更顺口些。现在的linux的标准用法已经是在dm层上用dm-thin target来实现thin provisioning了,目前dm-thin也已经相当稳定,大家今天讨论的主要是一些性能问题)

Ted Ts'o:开会

Snitzer: dm-thin现在的块分配算法对上层应用是透明的,基本就是写时按需分配,这样的话如果我在dm-thin数据卷上有多个volume,每个volume上边都有进程在顺序写,它们再去读时就不是顺序的了。我想像CFQ那样,给每个volume准备一个待提交的struct bio的有序列表,然后让各个volume轮流去写盘,每次写一大片,这样读的时候这一大片还是顺序读。我不会把这东西搞得像电梯算法那么复杂,主要是为了提高些locality。另外,XFS和ext4能不能向我暴露出来allocation group的边界在哪?我可以把这当成一个hint,这样写IO跨越边界时我就知道一个大块写已经结束了。

Joel Becker、Dave Chinner: 你要这个干啥?你用逻辑块号就行了。你真正想要的只是一个hint,别去关心它到底是不是allocation group的边界,这是文件系统的内部细节。

Ted Ts'o:总之,文件系统应该提供一个抽象的hint,用来给dm-thin判断locality

Ted Ts'o:下一个议题~~, dm-thin必须知道哪些块被释放了,不然很快就入不敷出了,我看用DISCARD命令就不错。

Roland Dreier: 多数线上系统管理员都把这东西关掉了。

Martin Petersen: 另外TRIM命令的支持也一直不太好

某人:估计目前唯一可靠的办法是用独立的工具做离线trim。

Snitzer:提醒大家注意一下,我们关心的不是那些硬件设备对DISCARD命令的真实支持情况到底怎么样,它可能支持,可能不支持,眼下咱们没能力关心,咱们关心的仅仅是把这个命令传到dm-thin这一层,让dm-thin知道哪块空间被释放了就足够了,甚至直接约定不往下传给设备也行。mount时加个-o discard就足够了。

Ted Ts'o:下一个议题~~, fallocate()是用来预分配空间的,不过目前这命令只是在文件系统层上有效,根本影响不到块设备层。这样在dm-thin上造成的结果就是有可能用户事先调用了fallocate()分配出来了空间,然后实际写的时候遇到-ENOSPC --- 因为dm-thin那边没有空间了,这完全违反了fallocate()的语义。要是不把fallocate()的块分配这事交给块设备层,这事情解决不了。

Dave Chinner: 不可能这么做,违反分层了。

Eric Sandeen、 Dave Chinner:提醒大家一下,文件系统没空间后的行为很不一样,XFS的话,如果数据已经写到redolog里了,它就会持续重试往数据盘里写,ext4和btrfs就不会这样。一般来说,用户程序对-ENOSPC的处理都不太好,持续地返回-ENOSPC说不定会让用户程序彻底疯掉。
Dmitry Monakhov:我建议文件系统应该有一个标准化的途径向用户报告各种事件,包括像磁盘没有空间了这种错误。比如从VFS层发个uevent上去,前几天我提这个建议的时候,大家都表示同意,不过没人知道该从哪开始着手做,或者什么时候能搞定。

Ted Ts'o:。。。散会

(就像LSMM的大多数议程一样,开完会后开发者们还是什么也定不下来,但不管怎么说---总比不开会强。)

Block multi-queue status

Nic Bellinger在2014 LSFMM上主持讨论了block multi-queue的现状。blkmq在3.13的时候被merge到了内核,但是只支持virtio_block驱动且基本上可以工作,虽然到现在又有些变动,但是整体架构上是OK的。Micron的mtip32xx SSD驱动的转换也基本完成了,目前的驱动是单队列且共享tags,转换后是有八条队列,IOPS可以达到180w,和未修改的驱动一样。它在2-socket系统上表现良好,但是在4-socket机器上有所下降。

这其中的一个问题是因为缺少tags。有人说他们替换掉了per-cpu ida后就消除了tags问题。而Matthew Wilcox也提出了tag的另一个问题:linux上的实现是每个LUN上唯一,但是规范上只要求每target唯一。James Bottomley也说规范允许16bit的tags,而不是目前用的8bit。

Bellinger继续介绍了他和Hellwig在给SCSI添加多队列的工作。从2008年起就开始有人指出SCSI core在小随机IO性能方面的问题,这大部分是由于锁的cache-line bounce,这使得这类IO的IOPS只能达到250K。使用多SCSI host后可以使性能达到100w IOPS,但是会有1/3的CPU耗在spinlock争抢。因此Bellinger使用blkmq来预分配SCSI command,sense buffer, 保护信息和request,他的初级实现不包含错误处理,出现的任何错误都会造成oops,但是可以达到170w IOPS。Hellwig实现了错误处理并做了merge的计划,但是他俩在是否将faster IOPS作为默认模式上还没有达成一致,这也会让合并有些困难。另外,Bellinger认为驱动的转换相对比较容易,而Bottomley认为在消除驱动的锁方面仍有较多工作要做。

Large-sector drives

虽然Linux kernel对4k sector的设备的支持才[刚刚出来 http://lwn.net/Articles/377895/], 2014年LSF/MM峰会上Ric Wheeler又开始招呼大家开始讨论sector尺寸大于4k的设备了。Ric首先给设备制造商抛出一个问题:当时他们选择4k sector size的原因是啥,是因为他们自愿的还是因为受到内核的限制。这个问题厂商们并没有正面回答,不过他们的一致意见是最好在sector size上能够有更多的灵活性,比如可以支持64k或者128k这样的。

Dave Chinner提到了目前内核需要修改的地方,目前看似乎文件系统以及处理分区的代码需要修改。他还提到目前page cache代码的假设都是能够基于page大小做IO,Jan Kara也提醒大家目前页面回收算法也是基于上面这个假设,所以如果页面大小(4K)小于sector大小,可能比较麻烦。Dave提到一种解决方案是用类似IRIX中的chunk方案,这玩意可以处理多内存页的buffer,但是他不缺定是否靠谱。另一条路子就是允许小于sector尺寸的读写,但是这个就不可避免的会出现读-修改-写回这种场景,性能就杯具了。

最后Ric总结说目前设备制造商还没有开始推动大sector尺寸的设备支持,所以内核社区还有时间,但是内核社区的一个基本原则就是先要搞定Sector尺寸大于page尺寸的情况,后面应该就差不多了。

Direct I/O status

Kent Overstreet今年在LSF/MM峰会上主持讨论了Direct I/O目前的状态。Direct I/O的作用是允许用户跨国系统page cache直接访问磁盘上的数据。

Kent首先提到他的biovec的相关工作已经被社区合并了。这些工作之后就可以很容易的拆分一个bio结构。剩下的一个前提准备是make_request()函数。该函数需要能够处理任意大小的bio。

在完成了所有上述准备工作之后,内核就可以抛弃buffer_head从而直接使用bio来进行DIO操作了。这样代码的复杂度就可以有效地降低。同时,DIO代码路径上也不再需要通过buffer_head来获取文件系统元数据信息。同时,由于make_request()函数可以处理任意大小的bio,buffered I/O路径也可以进行相应的优化,文件系统不再需要处理bio拆分这样无效率的工作了。

FedFS, NFS, Samba, and user-space file servers

虽然LSFMM分了三个tracks讨论,但多数话题都无法将存储和文件系统分开讨论,恐怕只有Chuck Lever和Jim Lieb牵头的讨论是个例外,他们主要提及了用户空间文件服务器,尤其是FedFS和用户空间NFS。

Lever首先从介绍FedFS是什么东东开始:FedFS(Federated Filesystem)可以让管理员把几个完全不同的文件服务器导出的文件系统合并成一棵文件系统树呈现给用户。FedFS通过一种称为referrals的概念表示“底层”文件服务器上的文件或者目录信息。因此,FedFS文件系统实际上是树状组织一个referrals集合。

物理保存referrals信息的对象称为“junctions”。不同文件系统实现junctions的方式并不一样:Samba使用符号连接保存访问数据所需要的元信息,而FedFS则使用带有特殊扩展属性的空目录表示。虽然去年会议上对统一junctions格式的意向达到了一致,但Samba和FedFS依然各行其是。

接下来就是怎么统一的讨论了:NFS的开发者觉得空目录方法不好,这个设计类似于Windows的reparse points概念。有人提出如果能引入一种新inode,或者至少引入一个新标志位就完美了,但Tso反对说,这意味着需要修改所有文件系统才能支持它(作为文件系统小白,我表示个人不能理解为什么要修改所有文件系统?!)。同样,符号连接方法也没有得到亲们的好评:Layton解释了为什么不能用符号连接,而Trond Myklebust则清楚地认为符号连接的主意是too ugly和hacky,并且也限制了referral的信息只能保存在一个页面上,所以如果要在一个referral上支持多个协议恐怕很困难。Myklebust总结道,符号连接,Samba自己玩玩尚可,推而广之还是不要了。可惜的是,究竟怎么统一格式还没有定论。

后面开始小话题满天飞了,我摘要一下:

  • 用户空间文件服务器需要内核协助的地方蛮多的,其中之一是file-private locks,Layton已经提供了一个patch,Lieb希望它可以进入3.15合并窗口。然后,就是让glibc支持这种新型锁。
  • inotify事件的过滤机制,例如,Ganesha文件系统服务器需要监听底层文件系统的事件,但不需要其自身的事件,这用于Ganesha的缓存管理机制。同样也有补丁了,等待合并中。
  • Ganesha开发者希望可以用更快的方法处理user credentials,现在每次操作都需要数次(概念上需要7次)系统调用。最后的结果是大家认为Ganesha需要一种user credentials缓存,而Tso觉得他有个招儿可以解决问题,转私下交流了。
  • readdir()的增强版提议再次提上日程,看起来依然无望。
  • 最后也是一个没有结论的话题:目前有两种ACL:POSIX ACL和NFS ACL,各有一批使用者,两者的语义也不完全相同。AI Viro认为同者支持两者过于复杂,而虽然NFS ACL的语义更丰富,但大家对NFS ACL是否能够模拟POSIX ACL也没有定论。所以,也就没法确定内核应该支持哪个了。

Lots of new perf features

在2014年的Linux Foundation Collaboration Summit大会上,Red Hat的Jiri Olsa和LG的Namhyung Kim为我们介绍了Perf的一些新特性。

Olsa的介绍如下:

1)Perf利用了libtraceevent库来分析Tracepoint的格式。

Libtracepoint由Steven Rostedt开发,是ftrace前端的一部分。我们知道,每个kernel的Tracepoint在它的format文件里面都有格式信息,用以描述该Tracepoint输出信息的格式。Libraceevent可以帮助用户空间的程序更方便地获得这些格式信息。

2)对Intel处理器新特性的支持

2.1) 支持了RAPL(Running Average Power Limit 详见注[1])
在RAPL的支持下,管理能能够设置并监控系统中各种硬件域(Hardware Domain,见注[1])的能耗上限。基于RAPl,Stephane Eranian做了相应的支持,可以对不同的硬件域进行能耗相关的采样和分析:如所有的物理Core,CPU芯片,DRAM或者内建的GPU。
2.2) 访存行为的Profiling

对内存访问行为的Profiling是很多开发者的共性需求,以往,perf在这方面做的并不好,主要因为CPU没有提供相关的接口。目前,在最新的Intel CPU的支持下,我们能够观察到对内存的加载与存储操作。而且这些事件还附带了丰富的信息:指令地址、数据地址、访问目标(L1 Cache、本地RAM、远端Cache…)、TLB的访问状况(命中、丢失…)等。同时,我们还能拿到每次访问的开销(weight,即此次访存操作消耗了多少个CPU cycles)。不过,这个开销的数值在现阶段还不是很精确。但在未来的CPU中,访存开销的精度会得到持续改善。

Perf仅能够记录内存访问信息,并不去尝试分析它。另外一个工具“c2c(cache to cache)”能够监测到CPU间共享的cache line。在指定地址之后,c2c能够报告每一条cache line的状态,比如:访问类型(加载 or 存储)、访问偏移以及触发访问的指令。有了这些信息,开发者便能够对每条cache line都了如指掌。

3) 对调用链功能的改善

在利用ftrace进行profiling时,大家都希望能看到相关的函数调用链。目前,我们使用了frame pointer实现了这个功能。如果frame pointer未被使用时(GCC往往会将该寄存器优化为普通寄存器),则利用libdw,在DWARF调试信息的支持下,unwind应用程序的stack。Perf已经能够记录用户栈与相关的寄存器,而libdw可以完成后续的工作。Libdw比libunwind要快很多。[扁鹊下一步也将考虑使用libunwind替换掉libunwind]。

另外一种快速生成调用链的方法是使用LBR(Last Branches Record)信息。LBR机制在能够存储被采到的那条指令所触发的分支列表。同时包括分支的源地址与目标地址。利用这些信息,也能够获得调用链。

4) 未来工作

4.1) 添加对CTF(Common Trace Format)数据格式的支持。
 CTF格式当前被LTTng(Linux Trace toolkit next generation)用于数据存储格式。
4.2) 将perf record多线程化。
4.3) 事件的相互触发机制

允许一个事件打开或关闭另外一些事件,从而减少被记录的数据量。原始代码由Frédéric Weisbecker开发,但目前尚没有合适的用户接口。

4.4) 事件组

目前,perf需要在每个CPU上为每个打开的事件申请一个文件描述符。如果CPU很多,打开的事件数也很多。那么文件描述符很可能不够用。目前,已经有计划使得每个“事件组”共享同一个文件描述符。当然,大家仅仅讨论过这个想法,还没有代码。

4.5) Perf代码的通用化

Perf 的一些核心代码被移到了tools/lib中,其它的工具也可以使用这些代码了。

4.6) 测试用例

Perf现在共有26个测试用例。每个commit都需要跑一遍这些测试。测试集还会日益丰富。
Namhyung Kim的介绍如下:

1) Perf report 的--children参数

展示完整的callchain,以及callchain中的每个函数对测试结果的贡献。在perf report的输出结果中,增加了“children”与“self”列,用以展示Callchain中每个函数的贡献。

2)--field参数

用以指定需要展示的信息(dso,thread comm等等)。该参数可以与“--sort”配合使用。

3)Ftrace的支持

在perf中支持ftrace是一个新特性。目前,仅仅支持function与function_graph。该特性利用了libtraceevents的kbuffer API来访问事件。为ftrace事件提供类似perf的行为是未来的目标。

我们可以这样使用perf的ftrace功能:

perf ftrace record

perf ftrace report 或 perf ftrace show

4)在uprobe中增加了DWARF的支持

Perf目前能够利用符号名与行号动态设置tracepoint。该工作由Masami Hiramatsu贡献,并已经被收入upstream了。通过“--line”与“--var”便可以利用Binary中的debug信息创建uprobe检查点。之后,perf可以对这些进行采样。

5)SDT(Statically Defined Tracepoints)的支持

SDT类似于内核的Tracepoint,面向应用程序。在支持SDT后,perf能够访问SDT并对SDT进行采样。目前能够给出应用中支持的SDT的列表,并利用uprobe访问它们。不久之后,我们可以像访问perf的事件那样来访问SDT。

注: [1] RAPL: Running Average Power Limit。IntelCPU自Sandy Bridge之后引入的新接口。通过该接口,软件能够监测、控制芯片的能耗,并可以获得能耗相关的通知。RAPL为了实现细粒度的能耗控制,将硬件划分成了不同的域,如:芯片、DRAM控制器、CPU core、Graphic uncore等。
Ref:http://lwn.net/Articles/545745/

Sealed files

通过共享内存来进行进程间通信的常用方法。这个方式简单高效,但有一个陷阱:所有交互进程必须相互信任。也就是操作共享空间的进程必须假设对方不会做坏事,比如在你的读的时候偷偷修改了内容还告诉你没有修改,在不恰当的时机把空间的后端文件大小变了导致进程的读写操作触发致命的信号中止程序等。当然现实中,我们可以通过拷贝共享空间的数据到其它地方来避免这些问题。但这样做十分低效和笨重。

开发者最近一直在讨论内核层面上的解决方案。三月份David Herrmann同学提交了file sealing补丁,保证一方在操作共享空间的时候对方不会做坏事。

这个方案通过fcntl()操作共享空间的文件描述符引入了三个标志位

  1. SEAL_SHRINK 阻止文件被变小。
  2. SEAL_GROW 阻止文件变大。
  3. SEAL_WRITE 阻止任何写操作(resize除外)

如果这三个标志同时被设置,这个共享空间就变成了不可变(immutable)的空间。同时这些标志不能作用于有可写映射的空间,除非通过munmap()来把可写的映射去掉。一旦空间被密封后,一方可以通过调用fcntl,传入SHMEM_GET_SEALS来获取对方设置的标志。这将避免一些潜在的安全和未知因素。

目前文件密封(file seal)操作只能作用于shmfs,通过修改write(), truncate(), mmap()路径上的代码实现。

有人指出引入了文件密封机制也会引入安全问题。比如说通过频繁的去密封一个普通文件形成拒绝服务攻击。目前文件密封机制只做作用于shmfs,所以不会给普通文件带来这个问题。
如果不想挂载shmfs来显示的操作文件,这个补丁引入了一个新的系统调用:

int memfd_create(const char *name, u64 size, u64 flags);

这个系统调用将返回一个支持文件密封的句柄,然后用户可以通过mmap()映射到共享空间。

大多数开发者都觉得这套补丁不错,但是也有一些人觉得实现和语义有点问题。Linus大神不满意所有人都能去密封文件,不管是不是自己创建的。建议只有创建者才能密封文件。David同学没有反对这个建议,说如果这样做还能简化实现。他觉得还可以添加一个标志(MFD_ALLOW_SEALING)来控制是否允许别人密封。

Ted Ts'o提到文件密封可以扩大到更大的范围,应该做vfs层中去,然后所有的文件系统都可以支持这个操作。David同学回答说,到目前为止他没有见到任何通用文件系统对这个特性的需求用例。这个问题的讨论以没有结果而告终。

目前这个补丁已经有了一些潜在用户,kdbus和显卡已经Android系统都有可能用到,所以这个机制被合并的前景很好。

Avoiding memory-allocation deadlocks

避免申请内存死锁 有句谚语"要挣钱必须先花钱", 尽管这个悖论很容易通过创业贷款和坚持收支平衡的原则来解决。 一个相似的逻辑适用于管理像linux这样的操作系统的内存: 有时候,你为了释放内存,需要先申请内存。 这里,同样需要坚持原则, 尽管不小心的结果不是破产,而是死锁。

作为linux开发历程的一个缩影,内核如何保持其收支的平衡是很有趣的,并有助于理解如何去处理未来发生的死锁。 让我们从介绍1998年早期的Linux 2.1.80pre3里的__GFP_IO作为一个美好起点。

__GFP_IO in 2.1.80pre3

内核里的任何内存申请都包含一个gfp_t的参数,这是一组标志用来指导get_free_page()函数如何去定位一个空闲页。 从2.1.80pre3,这个参数的类型从一组简单的枚举类型,改变为标志位。每个标志里的含义跟以前的一致,但是 这是他们第一次被明确的标示。

—GFP_IO 是这些新标志中的一个。当它被设置时, get_free_pages() 可以调用shm_swap() 把一些页写到交换分区。 如果shm_swap()为了完成写操作,需要申请一些buffer_head结构体,要注意不能再设置—GFP_IO标志了。 否则,很容易就发生死循环,进而很快耗光栈,并导致内核崩溃。

今天,内核里的—GFP_IO是2.1.80内核引入的,原先的—GFP_IO在 2.1.116内核已被删除了。尽管名字一样,但是他们是不同的标志了。

PF_MEMALLOC in 2.1.116

很久以前(1998年8月),我们还没有像今天这样好的修改日志,所以需要一个操作系统考古学家去猜测之前哪些修改的原因。 我们确认之前get_free_page()使用的(每请求一个的)—GFP_IO已经没有了,并出现了一个新的(每进程一个的)标志PF_MEMALLOC, 来取代它避免递归的作用。这次改动的一个明显的好处是它更专注于解决一个问题:递归是per-process的问题,因此一个per-process标志更合适。 之前,很多申请内存的地方会不必要的避免使用—GFP_IO。现在,这些地方不用再担心这个递归的问题了。

下面的代码注释强调了内存分配的一个重要方面:
*"PF_MEMALLOC" 标志使我们避免递归

* 如果我们做换出的时候工作时,需要更多的内存,我们只需要返回"success",来告诉内存分配器接受申请。

当条件满足时, get_free_page()就会从空闲链里面拿出一页并尽快返回这一页。当条件不满足时,不会仅仅释放一页来满足当前请求, 而是多释放一些页,以节省下次的时间。这样,就相当于重新补充了创业贷款。PF_MEMALLOC 的一个特别的结果是内存分配器不需要 特别努力地去得到大量的页。他尽量根据它已有的去设法应对。

这意味着带有PF_MEMALLOC的进程有可能适用最后仅有的很少的空闲内存,相比之下,其他的进程则必须换出并释放大量内存后才能够使用。 在当前内核里PF_MEMALLOC的这种特性还有并且正式些。在内存分配器里有个“水位线”的概念,如果空闲的内存总数低于特定的水位线, 分配器就会尽量释放更多的内存,而不是返回现有的内存。跟—GFP能够选择不同的(最小,低,高)水位线不同,PF_MEMALLOC会忽略所有的水位线, 只要有可用的内存,它就会返回这些内存。

PF_MEMALLOC实际上相当于说“现在要停止省钱,开始消费,否则我们将会没有产品可卖”。结果现在PF_MEMALLOC被广泛的使用, 而不仅仅是为了避免递归(尽管他有这个作用)。像nbd,网络块设备,iscsi_tcp和MMC卡控制器用到的几个线程都设置了PF_MEMALLOC, 任何调用他们去写出一页内存(以释放它)的时候,他们肯定能够得到内存。

与此相比,在2.6.33,MTD驱动(它管理NANDflash并对MMC卡驱动有类似的作用)停止使用PF_MEMALLOC标志, 并在注释里说是这是一种不正确的用法。关于内核里其他使用的地方是否合理的问题已经超出了我们这篇文章要讨论的深度了。

__GFP_IO in 2.2.0pre6

当—GFP_IO再次出现在内核时,跟最初的目的相比,它有一个类似的目的,但是是为了一个很重要的不同的原因。为了理解那个原因, 很有必要看看代码里的一个注释:
/* 如果我们不能够做IO,不要深入到换出的东西里,*/

这里的关注还要涉及到递归,但也会涉及到锁,如per-inode mutex, page lock和其他各种锁。 为了写出一页而调用到文件系统里可能需要锁。如果分配内存时,某一个锁被阻住了,那么避免任何可能调用到文件系统并取得相同锁 就很重要。这些情况下,代码千万不要使用—GFP_IO;其他情况下,参数里包含这个标志是相当安全的。

PF_MEMALLOC避免了get_free_page()又递归调用了get_free_page(), 而—GFP_IO更通用,并且避免了 一个持有锁的进程通过get_free_page()调用到其他也需要这个锁的函数。这里的风险不是像PF_MEMALLOC那样的耗光栈, 而是死锁.

考虑到先前—GFP_IO 的经验并不成功,有人可能疑惑为啥使用一个GFP标志而不是一个进程标志, 进程标志可以明确地说“我持有一个文件系统的锁”。 像许多软件设计一样,这可能“在那时看来是个好主意”

__GFP_FS in 2.4.5.8

这个标志最初出现在2.4.5.1里,名字是—GFP_BUFFER ,但直到2.4.5.8里被命名为—GFP_FS前,没有真正正常工作。 显然在原始设计里有个 thinko, 这要求不仅改变一些代码,还有改了一个新的名字。

实际上—GFP_FS从—GFP_IO里分走了一部分功能,所以过去是一个标志,现在又两个标志。 只有三种组合可能是正确的:全没有,全有,新的可能仅—GFP_IO被设置。 这将允许我们已经准备的buffers被写出,但会禁止调用到文件系统里去准备那些buffers。 I/O行为被允许,文件系统行为被禁止。

推测,—GFP_IO先前有这么宽泛的功能,对性能有影响,在一些可能有I/O行为的地方也被排除(禁止使用)了。 重新提炼规则,增加一个新的标志位,从而更加灵活,对性能影响更小。

PF_FSTRANS in 2.5.36

在2002年底XFS被合并进内核时,这个新的进程标志出现。 它被用来指明一个文件系统事务正在被准备,也就是所有的写文件系统都会被阻塞,直到这个事务被处理完毕。 这个标志的作用是,当申请内存时,并设置了PF_FSTRANS时,—GFP_FS 就会被剔出,至少XFS的代码产生的请求是这样的。 其他代码里的请求不会受影响,但是当这个标志设置时,其他代码里的内存申请很少被调到。

在2.1.116内核里,由于用的地方少,而且修改也很容易理解,删除一个像—GFP_IO 的标志位是很简单的事情。 在2.5.26,这样的操作就困难的多了。 3.6rc1稍后的版本,NFS开始使用PF_FSTRANS 标志。PF_FSTRANS 不是被使用在事务里,而是被NFS用在创建并传输一个RPC请求到网络上 所以,现在名字有一点不太确切。这个标志在NFS中的作用,不是用来清除—GFP_FS标志,而是避免在nfs_release_page里发送一个COMMIT请求, 这在没有设置—GFP_FS标志是应该避免的。这样的使用方法跟XFS的使用方法有很大的不同。也许把它重命名为PF_MEMALLOC_NOFS,并删除GFP_FS, 从而使这个标志具有全局的属性的想法,是个不错的主意。

set_gfp_allowed_mask() in 2.6.34

首次出现在2.6.31,但在2.6.34里变得更加有意思。 gfp_allowed_mask 是一个全局的变量,它包含了一系列的GFP标志。特别是—GFP_FS,—GFP_IO, and—GFP_WAIT (这个标志位允许get_free_page()函数等待其他进程释放更多的内存)有时候会通过这种机制失效。 它能够影响更多的进程并使更多的标志失效,这有点像PF_FSTRANS。

gfp_allowed_mask能够在启动的早期阶段给kmalloc()函数更多的支持。在启动的早期阶段,中断时被禁止的。任何试图通过—GFP_WAIT或者其他标志去申请内存,都会触发来自lockdep checker的一个警告信息。在启动阶段,如果内存如此紧张,以至于让 分配者不需要要去等待,那将是让人感到很吃惊的。 所以gfp_allowed_mask 在初始化的时候去除了前面提到的三个标志, 并在启动完成后,把他们再加回来。

我们这些年得到的一个经验是启动不是像我们想得那么特殊:无论是挂起和恢复,还是硬件的热拔插。 在2.6.34里这个掩码被扩展以覆盖挂起和恢复。

对于内存分配引起的死锁,刮起比启动更有代表性。在启动阶段有大量的空闲内存,而刮起时则不是这样,可能碰巧我们也很缺乏内存。 那么就不是弹出个警告信息,而是真正的死锁。挂起和重启是顺序化的进程,如设备顺序进入休眠,而反向的顺序醒来。 所以对块设备来说,尽量避免使用—GFP_IO是远远不够的。一些写请求设计到的块设备,可能在顺序操作中已经休眠,因而 在这个请求完成前,不可能恢复过来。

使用一个系统级别的设置来禁止这些标志位,这看起来有些小题大做。因为,只要考虑那些顺序刮起所涉及的进程就足够了。 但这是一个简单可靠的修复办法。不会影响正在运行的系统。

PF_MEMALLOC_NOIO in 3.9-rc1

就像挂起和恢复告诉我们,启动不是一个很特殊的场景。进而,运行时的电源管理搞所我们,刮起也不是很特殊的场景。 如何一个块设备为了节省电源,在系统运行期间挂起,显然直到他或者他依赖的设备(如USB控制器,PCI总线)恢复过来前, 任何将一个脏页写出的请求都不会得到处理。所以任何一个这样的设备执行带有—GFP_IO的内存请求都是不安全的。

当一个设备刮起或者恢复的时候,我们使用set_gfp_allowed_mask()来确保这一点。但是如果有多个这样的设备同时 挂起或者恢复,我们很难处理什么时候恢复正确的mask。所以有个patch引入了一个进程的标志位,就像PF_FSTRANS标志一样, 只禁止了—GFP_IO标志位而不是—GFP_FS标志位。在设置标志时,先记录老的值,等完成时,再恢复回去。每个设备增加了一个 memalloc_noio标志,来指明什么时候去设置这个标志。他还可以被传递到设备树里的父节点。当一个带有memalloc_noio的设备进入 电源管理的代码的时候,PF_MEMALLOC_NOIO就会被设置。

尽管启动的早期阶段和设备的挂起和恢复大多是单线程(或者指定的线程),设置在这些线程上设置PF_MEMALLOC_NOIO和PF_FSTRANS 标志位能够替代set_gfp_allowed_mask。 但是,这样的变化没有明显的好处,而且能否正常工作并安全并不是很明确,所以目前还是保持不变。

Patterns that emerge

节下来有两个地方要做进一步的修改: 1。如何进一步避免“避免递归”。最初是个枚举类型的值,然后是—GFP_IO,之后PF_MEMALLOC。下一步是第二版的—GFP_IO.最后会分成 两个独立的标志位。 2。一个GFP标志位是不够的, 不是一个单独的申请要被控制,而是给定进程的所有内存申请都要被控制。 我们使用一个进程标志来禁止—GFP_WAIT或者一个每进程gfp_allowed_mask只是一个时间问题吗?

在3.6rc1,增加了一个新的标志—GFP_MEMALLOC,以支持swap-over-NFS。 这个标志在忽略低水位线并访问保留内存方面跟PF_MEMALLOC类似。 这个标志位和每socket的sk_allocation mask, 使得某些tcp sock(nfs使用他们进行swap over)能够访问保留内存,从而保证swap-out成功。 显然,GFP标志和进程标志,以及每设备和每socket标志都要改。

这篇文章没完,下周还要结合具体配置进行讲解。

Ktap or BPF?

过去几年内核一直在考虑增加一个动态的跟踪机制. ktap和bpf是近两年出现的两种内核动态跟踪的机制。 这里的动态跟踪机制,我个人的理解就是把报文过滤的虚拟机从用户态转移到内核态去做, 这样可以提高效率。例如,减少不必要的报文copy(内核态到用户态)。

BFP就是"Berkeley packet filter"。在各种报文分类的场合,它被广泛的使用,如tcpdump就支持这种格式。 2011年,BPF采用了一种新的编译器从而大大提高了速度。3.15内核也采用了这个特性。 一个重写的BPF引擎可以为用户空间提供一样的虚拟指令集,在内核里这些指令还可以被转换为一种更 接近硬件指令的格式。跟旧的格式相比,新的格式有很多优势,包括使用10个而不是2个64位的寄存器, 更高效的跳转指令,一种允许从BPF程序里调用内核函数的机制。 显然,在BPF争取成为内核动态跟踪设施的虚拟引擎时,这些附加的能力进一步增强了它的优势。

目前,如果ktap要想被内核接受,必须先和BPF虚拟引擎融合。 KTAP的作者Jovi Zhangwei(这个兄弟目前在华为)也表达了做出这样改变的愿望, 但同时他也指出了一些BPF的需要解决的缺陷, BPF不支持一些KTAP需要的特性,如访问全局变量,timer-limited looping等。 Jovi反复的抱怨BPF的跟踪机制,它是基于“把脚本附加到一个特定的跟踪点上”, 而Jovi需要一个更加弹性的机制"把一个脚本附加到多个跟踪点上“。 只要两个作者通力合作,这些都不是很困难的事情。但是两个人沟通出现的问题,彼此误解。 目前也没有其它人加入进来,事情就被搁在这儿了。

Scripting languages

Ktap是基于lua语言的,lua的一些属性在设置动态跟踪时很有用,如associative arrays。 然而,还有一些人,更喜欢一种C风格的语言。理由是内核使用的C语言,开发者对C风格语言更容易接受。 BPF一开始使用一种受限的C语言版本,Alexei已经提供把GCC和LLVM(?)转换成BPF虚拟引擎需要的后端。

而Jovi并不赞同上述观点,他认为ktap工作起来更简单。他举了个例子:

   void dropmon(struct bpf_context *ctx) {
       void *loc;
       uint64_t *drop_cnt;
       loc = (void *)ctx->arg2;
       drop_cnt = bpf_table_lookup(ctx, 0, &loc);
       if (drop_cnt) {
           __sync_fetch_and_add(drop_cnt, 1);
       } else {
           uint64_t init = 0;
           bpf_table_update(ctx, 0, &loc, &init);
       }
   }

而KTAP只需要:

   var s ={}
   trace skb:kfree_skb {
       s[arg2] += 1
   }

ALexei承认KTAP语法使用起来更简洁。同时, 他还建议一旦确定使用哪个虚拟引擎, 用户空间要有一些脚本语言的支持。 现在情况是,ktap里有一些有意思的函数,作者希望能有个开发者能够作些工作, 以便于把这些代码合入到内核中。

Changing the default shared memory limits

@泰来

Linux内核的System V共享内存从Unix年代开始就有一个固定的限制值。虽然用户可调大这个值,但是随着硬件的飞速发展,应用程序期待越来越大的内存,这个值显然太小了。现在除了调大这个默认值,开发者开始讨论我们是否需要这个限制了。但是为了兼容老的程序和避免一些不可预料的后果,简单粗暴的去掉这个限制也不可能,所以当大家都觉得这个值太小了的时候,修复确是一件不容易的事情。

System V风格的共享内存广泛用于进程间的通信,比如在多进程的数据库中就有广泛的应用。对于空间大小系统有两种限制,一种是每个空间可以允许的最大值(SHMMAX),一种是所有共享空间加起来的最大值(SHMALL)。在Linux系统中,SHMMAX默认是32MB,SHALL的计算公式是:

 #define SHMALL (SHMMAX/getpagesize()*(SHMMNI/16))

其中SHMMNI是系统支持最大的空间个数--默认是4096个。SHAMAX和SHMALL可以通过sysctl调节。实际是系统还有一个宏SHMMNI,表示一个空间的最小值,默认是1个字节,这个值不能被修改。

近年来管理员一上来就调大SHMMAX几乎成了标配的动作,因为32MB确实太小了。很多文章就建议跑一些受欢迎的程序之前,需要惯例性的调大这个值。
所以,在一些开发者当中认为调大这个值是当务之急。在3月31号的时候,Davidlohr Bueso就提交了一个补丁,把这个值修改成128MB。当然,128只是一个随机值,背后没有任何意义可言。

鼎鼎大名的Andrew Morton随后指出,这个补丁没有解决真正的问题。这个值还是需要很多用户去手动的调大默认值。他呼吁大家想得更远一点:如何才能永远的把这个问题彻底根治掉?
一个办法就是更本不提供SHMMAX这个值给用户,但是有人指出有时候管理员确实想设置一些限制来避免内存被滥用。Motohiro Kosaki建议看是否可以把默认值设置为0,然后用来表示无最大限制。但是后面Spraul指出,有用例把SHMALL设置为0来表示彻底关掉共享空间这个机制。所以用0来表示无限制,不能完全走通。

后来Spraul同学搞了一个自己的版本,就是把SHMALL和SHMMAX设置为ULONG_MAX。有人指出这其实又引入了潜在的问题:如果有用户通过N + 1的方式来设置这两个值,那么+1后这两个值直接溢出了,完全变成了相反的效果。后面针对这个方式,做了一些小的改进,但都不如人意。

默认把限制去掉,有人觉得SHM空间会有机会耗光内存。如果这个发生,将是灾难性的,因为OOM机制无法释放SHM占用的内存。解决方式是开启shm_rmid_forced选项(也就是让强制SHM空间和一个进程联系在一起,所以可以通过杀死进程来释放内存)或者选择手动设置限制(这不是又回到了原点么?这个补丁的目的就是避免用户的手工设置)

即使是回到了原点,删除古老的32MB限制也算得上一个不小的进步。

Loopback NFS: theory and practice

Linux NFS开发者一直以来就知道将一个NFS文件系统挂载到导出它的相同主机上,将会导致死锁(有时候被称作回环或者本地NFS挂载)。除了10年前发布的一个补丁外,有很少的努力来解决这个场景的问题因为没有可信的用例出现。NFS实现的测试肯定能从回环挂载中受益;这很可能触发上面提到的补丁。打上这个补丁,其余的死锁的触发需要做出一些努力,因此对测试者的建议本质上是“小心,你应该是安全的”。

对于其他的测试用例,似乎使用“bind”挂载提供了与回环NFS挂载类似的效果。简言之:假如当你使用回环NFS挂载受伤,你不要简单的那么做。然而,最近一个可信的测试用例出现了,激发了关于这个问题更多的思考。它导致作者写了一篇关于文件系统与内存管理间交互的教育性文章,并且产生了近期发布的补丁集(取代之前的尝试)来消除大部分,甚至所有的死锁。

一个简单的集群文件系统

那个用例涉及使用NFS作为一个高可用性集群中的文件系统,在那,所有的主机拥有共享的对存储访问权限。对于集群中所有能平等地访问存储的节点来说,你需要某种集群文件系统,像OCFS2, Ceph, 或者GlusterFS.假如集群不需要特定高级的吞吐量并且假如系统管理员偏爱继续使用已有技术,NFS能提供一个简单的临时替代方案。

理论遇到了实践

这个场景触发的死锁通常涉及一系列的事件像:1)NFS服务器试着分配内存,2)内存分配器试着通过把一些内存页经过NFS客户端写到文件系统,3)NFS客户端等待NFS服务器有些进展。我的假设是这个死锁是不可避免的因为同一个内存管理器正试着服务2个不同但竞争的用户: NFS客户端和NFS服务器。

一种可能的修复方法可能是在一个虚拟机中运行NFS服务器,并且给它以一个固定且被锁住内存分配,这样将没有任何竞争。这将工作,但对于我们的管理员,它很难是一个简单解决方案,很可能将会带来为最佳性能而计算VM大小的挑战。

似乎对__GFP_FS和PF_FSTRANS标识的操作应该能解决死锁问题。假如我们把nfsd看作是NFS文件系统的底层,死锁涉及到分配内存的文件系统的底层,并且触发回写到相同的文件系统。这准确是__GFP_FS被设计来阻止的死锁。实际上,在所有nfsd线程中设置PF_FSTRANS标识的确修复之前很容易就碰到的死锁。

使用__GFP_FS框架,要么直接,要么通过PF_FSTRANS, 最终证明作为回环NFS挂载问题的解决方案,要么是不充分,要么是不可信赖的。

一个关键的补丁

Mel Gorman在Linux 3.2中提交了一个对于这个问题很关键的补丁。这个补丁集改变了内存回收和文件系统回写间的交互。

在3.2之前,内存回收通常发起它能发现的文件脏页的回写。在页能被释放前,明显需要写这个脏页的内存到永久存储上,因此似乎当寻找可释放页时做这些是有意义的。不幸的是,它有一些严重的负面影响。

一个负面影响是被使用的内存栈空间的数目。第二个是页可能以不可测顺序被写出。
因此,Linux 3.2中从直接回收中删除了回写,把它留给kswapd或者各种文件系统回写线程。

等待回写

在这个补丁被应用后,直接回收将不再回写脏的文件页,这个延迟将也不再发生。此时,假如所有看到的脏页正在等待拥塞的设备,我们将得到一个明显的小延迟。这个导致了回环NFS挂载一些问题。与Linux 3.2之前存在的隐含延迟相反,清除__GFP_FS标识不能避免这个延迟。这就是为什么使用__GFP_FS或者PF_FSTRANS标识不够充分的原因。

从历史中学习

这个问题是类似于被介绍中提到的那个补丁修复的10前就出现的问题。在那个场景中,问题是正在弄脏页面的进程将会慢下来直到一定量的脏页被写出。当这发生的时候,nfsd最终被阻塞直到它写出一些页面,因此导致死锁。在我们当前的场景中,延迟发生是当回收内存的时候而不是弄脏内存,并且延迟有100ms的上限,除此之外,它是一个类似的问题。

解决方案是添加一个每进程标识 PF_LESS_THROTTLE, 它仅为nfsd线程设置。在这门限的时候,进程将会慢下来,这个标识将增加门限,因此解决了死锁。在那个补丁中,我们能看到2个重要的想法:使用一个每进程标识,和不要完全删除限流,仅增大门限来避免死锁。当弄脏页面的时候nfsd根本不限流,它将导致其他问题。

一个去,一个留

随着在控制下的livelock,不仅对于回环NFS挂载,而且同样潜在的对于loop块设备,我们仅需要处理一个遗留的死锁。当我们发现第一个问题的时候,实际要求的改变是相当小的。下周将发出的对这个改变的理解和解释将是更加本质的。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章