MPI如何对Lustre/GPFS文件系统优化?

在文章“

并行IO性能优化究竟是怎么玩的呢?

”介绍了MPI-IO的“两阶段IO”优化技术,但实际上它在Lustre、GPFS等文件系统上的性能并不好。从1997年MPI-IO标准出台到2008年的十年间,有大量的论文探讨影响性能的因素并提出了多种优化方案。直到2008年超算大会上的论文[1]才发现文件系统的锁机制是影响MPI-IO性能的关键因素,接着设计了3种优化技术。这些技术进过打磨演进,被主流MPI软件包广泛使用。

前面文章已经介绍了三种文件读写操作模式:只用1个进程读写、多个进程分别读写、多个进程读写同一个文件(共享文件)。设计良好的MPI程序都选择共享文件模式,以便充分发挥并行存储的性能优势。理解MPI-IO背后的优化原理,打开黑盒,有助于设计更好的MPI程序。

ROMIO适配多种文件系统

MPI-IO的功能由软件包ROMIO实现(图1),包含多个驱动,对Lustre、GPFS、NFS等文件系统进行针对性优化。ROMIO会自动识别底层文件系统,并启动相应用的驱动,论文[1]中提出的技术就应用在Lustre驱动和GPFS驱动里。

图 1

为方便说明这三个优化技术的原理,先给出一个。

抽象文件系统模型

在超算机群中,计算服务器和并行存储设备(IO服务器)连接到同一个网络,见图2。图中的IO服务器都能对外提供文件数据访问(不是元数据)而且地位平等没有主从关系,本文不涉及的设备(如存储的硬盘柜、元数据服务器等)都省略了。

图 2

把图2进一步抽象为图3,C~C3代表运行在计算服务器上的存储客户端软件,S~S3代表IO服务器及其上运行的存储服务端软件。每台服务器上都有1个存储客户端,存储客户端必须先挂载文件系统,然后才能访问存储系统上的文件。不同文件系统的客户也不相同。NFS协议是工业标准,使用最广泛,NFS客户端有专业机构开发、发布。Lustre等非标准文件系统的客户端由只能随同文件系统一起发布。

图 3

集合IO与聚合器

MPI-IO里的集合IO(collective IO)是指若干进程一起读写文件。现在的计算服务器里都多个CPU核,通常每个核心上运行1个进程,这样以来1台服务器上会运行多个MPI进程。,如图4,挑选出一个进程统一执行读写操作能减少IO次数、扩大IO粒度,性能更好。实际上,MPI-IO的两阶段IO技术就是这样做的。挑选出来的进程代表称为聚合器(aggregator),聚合器与存储客户端是一对一的关系。

图 4

锁协议

许多并行文件系统都兼容POSIX协议,而POSIX要求读写操作必须满足数据一致性和原子性。数据一致性是指几个并发的读写操作的结果看起来跟按某种顺序串行执行的结果是一样的,原子性是指单次写操作的结果对其它读操作来说,要么完全可见,要么完全不可见,即不能只有部分数据写成功而部分数据写失败。

满足数据一致性和原子性的一个办法是对文件加锁。锁协议多种多样,Lustre和GPFS采用一种“基于范围的锁协议”(extent-based lockingprotocol,下文简称范围锁)。

范围锁的操作方法很简单:尽可能锁住最大的文件区域。如图6(a),聚合器Pi申请文件的锁,因为文件处于自由状态,所以聚合器Pi得到的锁覆盖整个文件,虽然只想写蓝色阴影标出的那一小段,但仍然拥有整个文件区域的独占写权限。

假如随后有另一个聚合器也Pj申请写红色区域那一段文件,如图6(b),那么就那将后部一大段的独占写权限都给Pj。如果Pj申请写头部的红写阴影区域,如图6(c),那么就将头部的一大段的独占写权限都给Pj。但是,如果Pj申请写权限的文件区域与Pi的写区域有重叠,就会申请失败,只能等待Pi写完之后再次申请。

图 6

范围锁的好处是能减少申请锁的次数:如果一个聚合器连续写同一个文件的不同区域,只需申请一次锁就可以了,不必每次写都申请。

提请注意,申请锁是有时间成本的。因为是聚合器,所以Pi和Pj就运行在不同的计算服务器上。两个聚合器的沟通、协商将产生多次网络通信,从而消耗时间。显然,对自由文件申请锁并且后续没有其它聚合器来协商的场景是最省时间的。

锁颗粒

知道了加锁机制,接着就来了一个问题:能够加锁的最小文件区域是多大?

POSIX规定的锁函数fcntl的最小颗粒是字节。但是,对文件系统来说,将锁颗粒设定为1个字节就会带来巨大的资源消耗:内存、CPU计算能力、存储空间等。并行文件系统通常将锁颗粒设定得与文件条块大小一样,Lustre和GPFS都是这么做的。

客户端加锁和服务端加锁

GPFS和Lustre都采用范围锁,而且都使用客户端缓存,但是它们的锁机制还不完全一样。

GPFS采用基于令牌的锁机制来保证客户端缓存的一致性,持有令牌的客户端就是文件的主人,后续的写请求都要向它申请锁;无论文件是存放在哪些IO服务器上的,客户都能够锁住整个文件。如图7,聚合器Pi第一个拿到文件的锁,随后Pj就得向Pi申请锁,而不是向锁管理器申请。进过协商,Pi给Pj一个锁。

图 7

对Lustre来说,所有IO服务器都是锁管理器,每个IO服务器只能对自已上面文件块加锁,锁不住其它IO服务器上的文件块。如图8,聚合器Pi要写的文件区域位于IO服务器S上,又是第一个申请者,因此,S为Pi锁住了它上面的所有蓝色文件块。若随后聚合器Pj访问蓝色文件块,Pj需要向S申请锁,而不是向Pi申请锁。

图 8

锁边界对齐剖分

“两阶段IO”技术会将聚合访问区域(aggregate access region)均匀剖分成等长的大段,并将每个大段的读写任务交给一个聚合器负责,大段与聚合器一一对应。

图1中,每个大段用不同的灰度表示,纵向蓝色虚线代表文件的锁边界(lock boundary),相邻两个锁边界之间文件长度就是锁粒度(lockgranularity)。

图 1

在图1的上部,聚合访问区域被均匀剖分成4个大段,并交由4个聚合器P-P3负责。聚合器P、P1要访问一个锁粒度内的数据,无法并行访问,只能串行访问,而且锁的争夺与协商也有一些时间开销,其它相邻的聚合器之间也有同样的问题。这样以来,在某些时间段内,只有一部分聚合器处于工作状态,其他离合器处于等待状态,无法达到最高性能。

解决这个锁竞争问题的办法也很简单:见图1下部,不要严格均匀剖分,大致均匀即可,保证内部大段(不包含头尾的两个大段)都对齐到锁边界,这个策略称为锁边界对齐剖分(Partitioning Aligned with Lock Boundaries),简称“锁界对齐”。

如果文件系统是GPFS,因为客户端加锁,所以每个聚合器只需要申请1次锁,而且每个锁粒度之内都没有访问竞争,所有的聚合器都能同时工作。

静态循环剖分

“锁界对齐”策略下的每个大段通常都会分布在多个IO服务器上。假设使用的文件系统是Lustre,因为是服务端加锁, 所以每个聚合器需要向多个IO服务器申请文件锁。假设聚合器的数量是n,IO服务器的数量是m,通常n远大于m,例如n=100,m=3。在最坏的情况下,每个聚合器都要向所有的IO服务器申请文件锁,即每个IO服务器要同时处理关于同一个文件的n个锁请求,时间开销很大,拖累性能。

为了解决这个问题,引入“静态循环剖分”(Static-cyclic Partitioning,简称静态循环) 技术,见图2。将整个文件剖分为等长的块,块的大小与文件系统锁粒度相同,并把这些块以轮循的方式分配给聚合器。块与聚合器的对应关系是静态的,在每次集合通信中的对应关系都是一样的,不改变。例如,聚合器共有n个,块i,i+n,i+2n,…分配给聚合器i。

提请注意,在均匀剖分和锁界对齐剖分中,每次集合IO时都要重新剖分一次(每次读写的文件位置通常都不一样)。而静态循环剖分中,只对文件区域剖分一次,此后的每次集合IO时不再重新剖分。如果锁颗粒与文件条块的大小一样,并且IO服务器数量m和聚合器数量n有一个公因子,那么每一个聚合器总是和相同的几个IO服务器通信(不是全部IO服务器)。

图 2

图2就是一个静态循环剖分的例子,聚合器P只与IO服务器S通信,聚合器P1只与IO服务器S1通信。第1次聚合IO时,P与IO服务器S建立信道,后续的聚合IO就不必再建信道,节省了网络开销。对IO服务器S来说,只有一部分聚合器来请求文件锁,处理起来较快。

分组循环剖分

静态循环剖分在某些场景下会有一些问题:对服务器端加锁的文件系统,在聚合器的数量(例如100)远大于IO服务器的数量(例如4)时,每个IO服务器要同时处理过多聚合器(100/4=25)的锁请求和数据请求。IO聚合器之间的锁竞争带来不少开销。不同IO聚合器请求的文件数据通常不相邻,跳跃读写数据也会带来不少硬盘寻道时间,从而拖累性能。

为了解决这个问题,对静态循环剖分做一些修改,得到“分组循环剖分”(Group-cyclic Partitioning)技术:将IO聚合器分组,每组中的IO聚合器数量与IO服务器的数量相同,每组内的IO聚合器的编号均相邻,一组内的所有IO操作都完成以后,才执行下一组聚合器的IO操作。

如图3,第0组中包含IO聚合器P7、P、P1、P2,第1组包含IO聚合器P3、P4、P5、P6。在每一组内,一个聚合器只向一个IO服务器请求读写数据,没有锁竞争和跳跃访问。

从图3可以看出,第0组执行IO操作时,P7与S3一一对应,P与S一一对应。对服务端加锁的文件系统而言,每个IO服务器上只有一个锁请求,不存在锁竞争。每个IO服务器都在执行IO操作,没有空闲,性能有望达到最高。

图 3

性能测试

前面介绍的三种优化技术性能怎样呢?拿一个权威例子测试一下,这个例子就是ROMIO自带的集合IO测试软件coll_perf,读写一个三维整数数组。该数组在三个维度块状剖分,子数组的读写任务分派给所有进程。测试结果如图4~图7。

图4

在图4中,静态循环和分组循环的写带宽高于均匀剖分和锁界对齐剖分,这是因为Lustre在IO服务器端加锁,均匀剖分和锁界对齐剖分会带来锁竞争。图4中间的一幅图“IO Phase percentage”表示IO操作在“两阶段IO”中占用的时间比例,因为没有锁竞争,所以静态循环和分组循环的IO时间比重小。

图4上部的一幅图中,在进程数量为16-256的时候,静态剖分和分组循环的性能相同,而进程数量为512和1024的时候,分组循环性能更高。

这是因为测试所用的计算服务器每台4个核心,跑4个MPI进程,256个进程对应64台服务器(每台服务器上1个IO聚合器),而Lustre IO服务也正好是64个,所以在512个进程的时候,在静态循环剖分下,每个IO服务器对应2个聚合器,锁竞争和跳跃访问导致性能下降。

读操作不存在锁竞争,均匀剖分和锁界对齐剖分的读带宽更高一些。分组循环的读带宽与静态循环的读带宽一致,图4中就省略了。

图5

图5中改变了子数组的大小,但4种剖分方法的相对性能与图4一致。

GPFS在客户端加锁,这点与Lustre不同。如图6所示,均匀剖分和锁界对齐剖分的写带宙斯远高于静态循环和分组循环(分组循环数据与静态循环数据一致,图中省略了)。这是因为在均匀剖分和锁界对齐剖分下,每个聚合器只需要为自己负责的文件区域申请1次锁;而在静态循环和分组循环下,每个聚合器要为自己负责的文件区域申请多次锁,存在锁竞争。

读操作不存在锁竞争,四种剖分方法的性能差不多。

图6

图7中改变了子数组的大小,但4种剖分方法的相对性能与图6一致(分组循环与静态循环性能一致,图中省略了)。

图 7

结语

对客户端加锁的文件系统,例如GPFS,均匀剖分和锁界对齐剖分的写性能好。

对服务端加锁的文件系统,例如Lustre,静态循环剖分和分组循环剖分的写性能好,分组循环剖分在计算服务器数量大于IO服务器数量时写带宽最好。

几种剖分方法下的读带宽相差不太大。

超算建设时,要优先选用MPI软件包做过针对优化的文件系统,否则并行IO性能可能会很低。

来源:HPC世界

参考文献:

[1] Wei-keng Liaoand Alok Choudhary, Dynamically Adapting File Domain Partitioning Methods forCollective I/O Based on Underlying Parallel File System Locking Protocols.

温馨提示:

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190331A0058N00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券