V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
shipinyun2016
V2EX  ›  问与答

网易视频云: DirectIO 时的 IO 放大探究

  •  
  •   shipinyun2016 · 2016-07-08 10:53:49 +08:00 · 1252 次点击
    这是一个创建于 3090 天前的主题,其中的信息可能已经有所发展或是发生改变。

    网易视频云是网易倾力打造的一款基于云计算的分布式多媒体处理集群和专业音视频技术,为客户提供稳定流畅、低时延、高并发的视频直播、录制、存储、转码及点播等音视频的 PaaS 服务。在线教育、远程医疗、娱乐秀场、在线金融等各行业及企业用户只需经过简单的开发即可打造在线音视频平台。现在,网易视频云与大家分享一下 DirectIO 时的 IO 放大探究。

    前段时间在对我们自己开发的文件系统测试过程中发现一个有趣的现象: IO 放大。测试现象描述如下:

    · 现象 1 : iostat – x /dev/sdh1 ,观察发现每秒完成的读请求次数 100+,而测试程序统计的实际 IOPS 为 50 ,约为 iostat 统计数据的一半;

    · 现象 2 : cat /sys/block/sdh1/stat 发现测试程序运行过程中,该设备被读的数据量为 77292KB ,而测试程序实际读数据量为 40000KB 。

    我们会在下面的两个测试以及原理分析中揭示这些额外 IO 来自何方。

    测试 1 :不同 offset 读文件测试

    测试目的

    测试 ext3 文件系统元数据(索引块)对文件读性能影响

    测试方法

    顺序读数据目录(500 个 1GB 大小文件)所有文件,每个文件读一次,每轮测试中读文件 offset 一致,一共执行五轮, offset 分别为 32KB , 48KB , 64KB , 1GB-40KB ,每轮测试之前清空缓存( echo 3 > /proc/sys/vm/drop_caches ),使用 O_DIRECT 方式读,每次读 4KB 。

    测试结果

    1

    结论

    上面的测试较为有力地解释了 ext3 文件系统元数据(主要指索引块)对杜性能影响。在元数据缓存未命中情况下,读文件 offset 越大,产生额外 IO 越多, IOPS 越低。

    测试 2 :元数据缓存性能测试

    测试目的

    测试 ext3 文件系统元数据(主要指索引块)缓存与否对文件读性能影响

    测试方法

    顺序读数据目录下所有文件(500 个,每个大小为 1GB),每个文件只读一次, offset 为 1GB-40KB(根据计算, ext3 此时使用二级间接索引)。测试共执行两轮,第一轮测试之前清 cache ,第二轮测试之前不清理 cache ,使用 O_DIRECT 方式读,读大小 4KB 。

    测试结果

    1

    结论

    该测试对比了 ext3 文件系统元数据缓存与否对读性能影响,元数据未缓存情况下,产生的额外 IO 越多, IOPS 越低。而且,因为每次读偏移位于二级索引,需要两次额外的元数据 IO ,每个索引块大小为 4KB ,所以实际读出数据量应该为期望读数据量的三倍,与测试结果较吻合。

    原理分析

    为了进一步弄清楚这个问题,我们本着打破砂锅问到底的精神,翻阅了 Linux 内核代码,看看文件系统 direct 方式读的实现,需要说明的是我们使用的内核代码版本为 3.12 。

    在代码分析之前,先来普及一个概念:文件系统和底层块设备都有自己的块大小设置,而且,这两者可以不相同。块设备的默认块大小为 512 字节,有兴趣的可以自己查看下 hd_init()这个函数。文件系统,如 ext3 ,默认块大小是 1024 字节(可以自己挂载文件系统的时候设置,但最小 1024 字节,最大为 PAGE_SIZE ),有兴趣的同学可以自己查看下 ext3_fill_super()这个函数。因此一般说来,文件系统块大小是块设备默认大小的整数倍,明白这个对理解后面的实现比较重要。

    另外,我们看看 direct 方式读接口参数需要注意的几点, read 接口形式为:

    read(fd, buffer, size)

    注意:

    1. buffer 需要对齐,对齐的单位是块大小,按照文件块大小或者设备块大小对齐均可,可通过函数 posix_memalign()来分配按照一定粒度对齐的缓冲区;
    2. 读偏移 offset 必须以文件或者设备块大小对齐;
    3. 读大小 size 必须以文件或者设备块大小对齐;
    4. 上述只要有一点不满足,调用失败,返回错误信息为” Invalid argument ”。 同样,我们通过情境分析的方式来分析实现,要不显得实在是枯燥,假如我们读的调用形式为 read(fd, buffer, 4096)

    其中, buffer 以 1024 字节对齐,分配的 buffer 地址为 0X804a400 , offset 从 0 开始。

    跟着代码深入,文件系统的 read 最终会进入函数 generic_file_aio_read()。因为本文只讨论 direct IO 方式的读,因此我们只关注以下部分代码(mm/filemap.c):

    if (filp->f_flags & O_DIRECT) {

    ……

    if (!retval) {

    retval = mapping->a_ops->direct_IO(READ, iocb, iov, pos, nr_segs);

    }

    ……

    }

    看看代码,发现核心是调用了具体文件系统的 directIO 实现,对于 ext3 文件系统来说,是 ext3_direct_IO()。接下来,以此为起点,我们一路向西,跟踪调用流程:

    ext3_direct_IO —>blockdev_direct_IO —>__blockdev_direct_IO —>do_blockdev_direct_IO 。我们从 do_blockdev_direct_IO 开始分析,忽略前面几者是因为他们只是对下一层的简单封装,不值得浪费笔墨。由于代码较长,我们不堆砌代码,只是列举若干注意事项:

    · 检查参数对齐与否。 offset 、 buffer_addr 、 size 必须全部过一遍。首先会判断是否与文件系统块大小对齐,如果否,则判断是否和块设备的块大小对齐,如果均不满足,那可以直接报错了。另外,如果参数不以文件块大小对齐却以设备快大小对齐,代码中会记录差异因子是 1 ( 10 – 9 ); · 计算总共需读出的页面数。这里需要注意一点:页面数必须以 PAGE_SIZE 对齐,并非简单的 size/PAGE_SIZE(size 为应用程序想要读出的总的字节数)。拿本例来说,虽然 size 为 4096 字节,但计算需要读出的页面数却是 2 ,如下图所示: 1

    之所以这样是因为内核的文件 IO 均以 PAGE_SIZE 对齐。所以,如果应用程序分配的内存并非以 PAGE_SIZE 对齐(如我们分配的空间 0X804a400 就是以 1024 字节对齐),则 IO 可能会跨越多个 PAGE 。

    · 在 directIO 实现中,有两个结构体扮演了非常重要的角色, struct dio 和 struct dio_submit 。一些关键成员变量的含义如下:

    o dio_submit.pages_in_io :本次 IO 共有多少 PAGE ,本情境中为 2 ;

    o dio_submit.first_block_in_page : IO 操作的第一个块在页面中的偏移,本情境中为 1 ;

    o dio_submit.final_block_in_request :本次 IO 请求的最后一个块号,本情境中为 4 ;

    o dio_submit.total_pages :本次 IO 请求共有多少个 PAGE ,本情境中为 2 ;

    · 该函数实现上是一个循环:对应用程序传入的每个 seg (应用程序一次课发多个 IO segment ),初始化 dio_submit 的某些参数,然后调用 do_direct_IO()。

    接下来让我们忘掉该死的 do_blockdev_direct_IO(),进入更可恶的 do_direct_IO()。来看看是如何执行 directIO 的。因为篇幅的关系我们同样不罗列代码,只是看看该函数的实现有哪些关键点。

    · 上例中,应用程序提供的缓冲区跨越两个页面( page 32842 的 1024~4096 以及 page 32843 的 0~1024 )。但再次请读者注意的是:我们底层的 IO 一般以 page 为单位组织,所以我们需要根据用户态的缓冲区地址找到 page 结构,这便是该函数第一个需要注意的地方 page = dio_get_page(dio, sdio);将用户态缓冲区地址转化为 page

    · 接下来,我们的主要任务就是挽起袖子读数据了,不过等等,好像我们还忘记了一件事:我们这里读的全是逻辑块号,我们从底层读之前还需要将逻辑块号转化成物理块号,映射的方法因文件系统而异了,在这里很可能会产生额外的 IO ,在这里我们不关心具体文件系统的映射方法了,有兴趣的朋友可移步这里,有 ext3 映射方法实现的详细描述。需要的是:内核会尽可能多地进行映射,但底层返回的映射结果只能映射从起始逻辑块号连续的若干磁盘块,这很有可能会小于内核的需求。再拿本例来说,内核肯定是想一下把所有的想读的逻辑块地址全映射了,但实际上文件系统在实现的时候不一定能保证这些逻辑块在磁盘上完全连续,假如本例中的映射结果可能如下图所示:

    1

    这部分对应了该函数中第二个关键点: get_more_blocks(),如果映射的结果真的如我们上面设想的那样,那第一次我们只映射了 page1 中的两个逻辑块,因此接下来我们只能读这两个 block ,第二次再映射的时候,又映射了两个 block ,第一个 block 位于 page1 ,第二个 block 位于 page2 。

    · 映射完成后(且每次映射完成均)开始进行 IO ,这里的 IO 操作也是比较有趣的,对应了代码第三个关键点: submit_page_section()。一般来说,文件系统要进行 IO 主要就是调用块设备层的 submit_bio 接口,我们只需设置好参数即可。 submit_page_section()为了效率上的考虑,实现了 deferIO ,核心思想是:不立即发起 IO 操作,而是等等看,看看后面的 IO 请求是否连续(所谓的连续 IO 是指同一个页面内物理块号连续的 IO )。如果有,我们将它合并进来;否则将本次 IO 先发下去(调用 submit_bio )在我们的情境中,第一次的 IO 请求位于 page1 的 block 0 和 block 1(物理块号为 100 和 101),第二次 IO 请求其实是 page0 的 block 2(物理块号 300),第三次的 IO 请求其实是 page1 的 block 3 ,因此会产生 3 次 IO 请求。

    结论

    至此,我们不仅进行了测试,更进一步从源头上分析了测试结果的原因,形成如下有效结论:

    1.ext3 采用的文件索引方式在读偏移较大的时候会产生额外 IO ,而且偏移越大,额外 IO 次数越多;

    2.Linux 采用了 buffer cache 缓存 ext3 的索引块,查找数据块位置会首先在缓存中查找索引块,缓存未命中需要从磁盘读取索引块;

    3.DirectIO 时 Linux 内核发出的 IO 请求次数实际上与以下因素相关: a).逻辑块在物理磁盘上的连续性; b).应用程序缓冲区对齐粒度,编程中尽量做到以 PAGE_SIZE 对齐。

    更多技术分享,请关注网易视频云官方网站( http://vcloud.163.com/)

    或者网易视频云官方微信( vcloud163 )进行交流与咨询

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2723 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 15:14 · PVG 23:14 · LAX 07:14 · JFK 10:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.