大话存储系列21——存储系统内部IO 上

简介: 1、IT系统的IO结构图 2、应用程序层IO 应用层程序是计算机系统内主动发起IO请求的大户,但是要知道,计算机内不止有应用程序可以向底层存储设备主动发起IO请求,其他的,比如文件系统自身、卷管理层自身、适配器驱动层自身等,都可以主动发起IO。

1、IT系统的IO结构图




2、应用程序层IO

应用层程序是计算机系统内主动发起IO请求的大户,但是要知道,计算机内不止有应用程序可以向底层存储设备主动发起IO请求,其他的,比如文件系统自身、卷管理层自身、适配器驱动层自身等,都可以主动发起IO。当然,只有应用程序发起的IO才可以修改用户实体数据内容,而其他角色发起的IO一般只是对数据进行移动、重分布、校验、压缩、加密等动作,并不会修改用户层面的实际数据内容。

应用程序在读写数据的时候一般是直接调用操作系统所提供的文件系统API来完成文件数据的读写等操作,有的应用程序可以直接调用卷管理层或者适配器驱动层API从而直接操控底层的卷或者LUN,比如一些数据库程序,他们直接操控卷而不需要使用文件系统提供的功能,它们自己来管理数据在底层卷上的分布。

每个应用程序都会有自己的Buffer用来存取有待处理的数据。 应用程序向文件系统请求读数据之后,文件系统首先将对应的数据从底层卷或磁盘读入文件系统自身的Buffer,然后再将数据复制到对应程序的Buffer中。应用程序也可以选择不适用系统内核缓存,这时FS将IO请求透明的翻译并转发给底层处理,返回的数据将直接由OS放到应用程序Buffer。当应用程序向文件系统请求写入数据时,文件系统会先将应用程序Buffer中对应的数据复制到文件系统Buffer中,然后在适当的时刻将所有FS Buffer内的 Dirty Page写入硬盘。同样如果不使用系统内核缓存,则写入数据经过FS文件一块地址翻译后直接由OS提交给FS下层处理。文件系统的动作是可控的,稍后介绍。


同步IO与异步IO

接下来我们来看IO的概念,同步IO和异步IO这个比较简单;同步的话就是应用程序发出IO请求,然后等待操作系统下层返回给应用程序数据,然后继续执行;而所谓的异步IO,就是应用程序发出IO请求后,不必等待返回数据,直接进行下一步的代码运行。


IO请求发送到OS内核之后到内核将IO请求对应的数据读取或者写入完成这段时间会贡献为OS的IOWait(即为IO等待时间),IOWait指标一旦升高到高于60%左右的百分比,那么就需要考虑后端存储系统所提供的性能是否已经不能满足应用需求了。

同步IO请求如果不加任何参数的话,一般是操作提供的默认调用方式,也是一般的应用程序首选的IO调用方式。一般情况下,如果遇到数据链路速度或者存储介质速度很慢,比如通过低速网络进行IO(Ethernet上的NFS、CIFS等),或者使用低速Flash芯片等,使用异步IO方式是一个很好的选择。第一是因为异步IO调用可以接连发出多个IO请求,一通发向目标,目标在接收到这些IO请求之后可以一并处理,增加速率。比如SATA硬盘的NCQ功能,


要实现与单线程异步IO类似的结果,可以采用另一种方法,即生成多个线程或进程,每个线程或进程各自进行同步IO调用。然而,维护多个线程或进程需要耗费更多的系统资源,而采用单线程异步IO调用虽然需要复杂的代码来实现,其相比于多线程同步IO的方式来说来说仍然更加高效。


在Windows系统中,如果应用程序在打开文件进行读写操作时未指定特殊参数,则文件系统默认是使用自身缓存来加速数据读写操作的。并且,这种情况下异步调用多数情况下会自动变为同步调用,其结果就是IO发出后操作系统不会返回任何消息直到IO完成为止,这段时间内线程处于挂起状态,为什么会这样?有三个原因:

1、预读和Write Back:文件系统缓存的机制可以增加IO读操作的命中率,尤其是小块连续IO操作,命中率几乎是百分之百。在这种情况下,每个读IO操作的响应时间会在微秒级别,所以OS会自动将异步调用转变为同步调用以便节约异步IO所带来的系统开销。

2、尽量保持IO顺序:异步模式下,应用程序可以在单位时间内发出若干IO请求而等待OS批量返回结果。OS对于异步IO结果的返回顺序可能与IO请求所发出的顺序不同,在不使用文件系统缓存的情况下,OS不能缓存底层返回的IO结果以便重新对结果排序,只能够按照底层返回的实际顺序来将数据返回给应用程序,而底层设备比如磁盘在执行IO的时候不一定严格按照顺序执行,因为文件系统之下还有多处缓存,IO在这里可能会被重排,或者有些IO命中了,而有些还需要到存储介质中读取,命中的IO并不一定是先被发送的IO。而应用程序在打开文件的时候如果没有给出特殊参数,默认是使用文件系统缓存的,此时系统内核缓存(也就是文件系统的缓存)便会严格保持IO结果顺序的返回给应用程序,异步调用变为同步模式。

3、系统内核缓存机制和处理容量决定:文件系统一般使用Memory Mapping的方式来进行IO操作,将映射到缓存中一定数量的page,目标文件当需要的数据没有位于对应的page中,便会产生Page Fault,需要将数据从底层介质读入内存,这个过程OS自身会强行使用同步IO模式向下层存储发起IO。而OS内 存在一个专门负责处理page Fault情况的Worker线程池,当多个应用程序单位时间内使用异步IO向OS发送大量请求时,一开始OS还可以应付,接收一批IO,然后对其进行异步处理,随着IO大量到来,系统内核缓存命中率逐渐降低,越来越多的Page Fault将会发生,诸多的Worker线程将会处理Page Fault,线程池也会很快耗尽,此时OS只能将随后的IO变为同步操作,不再给其回应直到有Worker 线程空闲为止。


导致Windows将异步强行转变为同步的原因不只有系统内核缓存的原因,其他一些原因也可以导致其发生。在Windows系统中,访问NTFS自身压缩文件、访问NTFS自身加密文件、任何扩展文件长度的操作都会导致异步变同步。要实现真正的异步IO效果,最好在打开文件时给出相关参数,不使用内核文件系统缓存。不使用内核文件系统缓存的IO方式一般称为Direct IO或者DIO,异步IO模式又被称为AIO,即Asynchronous IO.

在使用iSCSI/NAS等基于TCP/IP的存储协议时,并且是随机IO环境下,利用Bypass文件系统缓存的纯异步IO模式会大大减少网络传输的开销,因为如果使用文件系统缓存,则OS强制变为同步IO模式,IO一个一个的执行,所以每个IO请求就需要被封装到单独的TCP包和以太网帧中,在IO量巨大的时候,这种浪费是非常惊人的。举个例子纯异步IO模式下抓包数据文件只有120KB,而如果使用FS缓存之后,同样的IO列表,抓包数据变为490KB,开销是前者的4倍,这是个绝对不容忽视的地方。


不管是Windows还是Linux都是:用户程序——OS内核——存储设备 这种架构,用户程序和OS内核之间存在一套IO接口。同样,OS内核与存储设备之间一样存在着IO接口,同样也有同步异步之分。在Windows系统下,OS内核——存储设备 和  用户进程——OS内核 这两个链接之间的IO行为对应,即上层为同步,则底层也同步。上层为异步底层也是异步(不使用FS缓存时;如果使用,有可能异步的转变为同步的)。 而Linux系统下,上层IO与底层IO不一定是对应关系,



NFS下的缓存和IO机制

同为NAS网络文件访问协议,NFS不管在数据包结构还是在交互逻辑上相比CIFS要简化许多,但是简化的结果就是不如CIFS强大,CIFS之所以复杂是因为CIFS协议中几乎可以透传本地文件系统的所有参数和属性,而NFS携带的信息很有限。简化同样也带来了高效,执行类似的操作,NFS交互的数据包在单个包的大小上和整体发包数量都相对CIFS有很大的降低。

与CIFS不同的是,NFS提供诸多更改的参数来控制操作系统内核底层IO行为。

Linux下的NFS比Windows下的CIFS优异,表现在,前者有明显预读能力,只有在特定情况下游读惩罚,写惩罚一点没。正因为NFS的缓存如此高效,所以在Linux2.6内核中,在mount时并没有提供Direct IO的选项(内核编译时被禁止),但是单个程序在Open()的时候依然可以指定O_DIRECT 参数来对单个文件使用DIO模式。与Windows下CIFS实现方式相同,如果选择使用了DIO模式,那么NFS层就会完全透传程序层的IO请求。


多进程访问下缓存一致性的解决办法:

在缓存一致性方面,NFS相比CIFS来讲要差一些。CIFS使用Oplook机制来充分保证文件的时序一致性;而对于NFS,除了使用字节锁或者干脆使用DIO模式之外,没有其他方法能够在使用缓存的情况下严格保证时序一致性,NFS只是提供了尽力而为的一致性保证,而且这种保证全部由客户端自行实现,NFS服务端在这个过程中不作为,下面我们看一下NFS提供的尽力而为的一致性保证机制。

比如,有两个客户端共同访问同一个NFS服务端上的文件,而且这两个客户端都是用本地NFS缓存,那么客户端A首先单开了文件并且做了预读,且A本地缓存内还有被缓存的写数据,此时客户端B也打开了这个文件,并且做了预读,如果被读入的数据部分恰好是A被缓存的尚未写入的部分,那么此时就发生了时序不一致。而这种情况在CIFS下是不会发生的,因为B打开时,服务端会强制让A来将自己的写入缓存Flush,然后才允许B打开,此时B读入的就是最新的数据。

再回到NFS来,B上某进程打开了这个文件之后,内核会将文件的属性缓存在本地,包括访问时间、创建时间、修改时间、文件长度等信息,任何需要读取文件数据的操作,都会Cache Hit直到这个Attribute Cache(ac)到达失效时间为止,如果ac达到了失效时间,那么内核NFS层会向服务端发起一个GETATTR请求来重新取回文件最新的属性信息并缓存在本地,ac失效计时器被置0,重新开始计时,往复执行这个过程,在ac缓存未超时之前,客户端不会像服务端发起GETATTR请求,除非收到了某个进程的Open()请求。其他诸如stat命令等读取文件属性的操作,不会触发GETATTR。

任何时刻,任何针对NFS文件的Open()操作,内核均会强制触发一个GETATTR请求被发送至服务端以便取回最新的属性数据。这样做是合理的,因为对于Open()操作来说,内核必须提供给这个进程最新的文件数据,所以必须查看最新属性以与ac缓存中的副本对比。如果新取回的属性信息中mtime相对于本地缓存的信息没有变化,则内核会擅自替代NFS服务端来响应程序的Open(),并且随后程序发起的读操作也都首先去碰缓存命中,不命中的话再将请求发给服务端,这一点类似于CIFS下的Batch Oplock;但是如果新取回的数据中对应的mtime比缓存副本晚,那么就证明有其他客户端的进程修改了这个文件,也就意味着本地的缓存不能体现当前最新的文件数据,全部作废,所以此时,内核NFS将会将这个Open()请求透传到服务器端,随后发生的读写数据的过程依旧先Cache Hit(由于之前缓存作废,所以第一次请求一定是不命中的),未命中则从服务端读取,随着缓存不断被填充以最新读入的数据,命中率越来越高,而且直到下次出现同样的过程之前这些缓存的文件属性副本很数据副本不会作废。




3、文件系统层IO

IO离开应用层之后,经由OS相关操作被下到了文件系统层进行处理,文件系统最大的任务就是负责在逻辑文件与底层卷或者磁盘之间做映射,并且维护和优化这些映射信息。文件系统还要向上层提供文件IO访问的API接口,比如打开、读、写、属性修改、裁剪、扩充、锁等文件操作。另外,还需要维护缓存,包括预读、Write Back、Write Through、Flush等操作;还需要维护数据一致性,比如Log、FSCK等机制;还需要维护文件权限、Quota等。

可以把一个文件系统分为上部、中部、下部三个部分。访问接口属于上部;缓存管理、文件管理等属于中部;文件映射、一致性保护、底层存储卷适配等属于下部。下面我们分别来看看各个部分的功能:

1、文件系统上部:文件系统对用户的表示层属于上部,比如Linux下的表示法“/root/a.txt”、"/dev/sda1"、“/mnt/nfs”或者Windows下的表示法“D:\a.txt”。。。文件系统表示层给用户提供了一种简洁直观的文件目录,用户无需关心路径对应的具体实体处于底层的哪个位置,处于网络的另一端,还是磁盘的某个磁道扇区。这种FS最顶层的抽象称为Vitrual File System(VFS)

文件系统访问接口层也位于上部。由于接口层直接接受上层IO,而IO又有14种,文件系统默默接受着这14中IO,包括什么随机、连续,大块,小块什么的读。文件系统一般会自己智能的、自适应的选择是否需要进行预读。以加快处理速度。

2、文件系统中部:缓存位于文件系统中部。预读和Write Back是文件系统的最基本功能,可以参考上文中的示例来理解文件系统预读机制。缓存预读对不通IO类型的优化效果也是不通的。


对于应用程序的写IO操作,文件系统使用Write Back模式提高写IO的响应速度。这种模式下,应用程序的写IO数据会在被复制到系统内核缓存之后而被通告为完成,而此时FS可能尚未将数据写入磁盘,所以此时如果系统当机,那么这块数据将会丢失,对应的应用程序可能并不知道数据已经丢失从而造成错误的逻辑,这种模式在关键领域的应用中是要绝对的杜绝的。比如NTFS文件系统提供了一个参数:FILE_FLAG_WRITE_THROUGH,就是给出这个参数之后,在写数据的时候,IO会先被复制到系统内核缓存,然后立即写入磁盘。最后向应用程序返回完成信号。注意这个参数与FILE_FLAG_NO_BUFFERING不同,后者表示不使用系统内核缓存了,而前者依然使用,只不过是全部写到磁盘后才返回成功信号。


文件系统还使用另外一种IO优化机制,叫做IO Combination。假设T1时刻有某个IO目标地址为LBA0--1023,被FS收到后暂存于IO Queue中;T2时刻,FS尚未处理这个IO,正好这个时候又有一个与前一个IO类型(读/写)的IO被收到,目标地址段位LBA1024--2047。FS将将这个IO追加到IO Queue末尾。T3时刻,FS准备处理Queue中的IO,FS会扫描Queue中一定数量的IO地址,此时FS发现这两个IO的地址是相邻的,并且都是读或者写类型,则FS会将这两个IO合并为一个目标地址为LBA0--2047的IO。并且向底层存存储发起这个IO。待数据返回之后,FS在将这个大IO的数据按照地址段拆分成两个IO结果并且分别返回给请求者。这样做的目的显而易见,节约后端IO资源,增加IOPS和带宽吞吐量。


3、文件系统下部:文件系统下部包括:文件——块 的映射、Flush机制、日志记录、FSCK以及与底层卷管理接口等相关操作。

位于FS下部的一个重要的机制是文件系统的Flush机制。在WB(write back)模式下,FS会暂存写IO实体数据与文件系统的Metadata。FS当然不会永久地暂存下去而不写入磁盘。文件系统会在适当的条件下将暂存的写IO数据写入磁盘,这个过程叫Flush。触发Flush的条件很多,时间点、应用触发、FS自身为实现某些功能(如快照)等等等都可以触发Flush。

文件系统是系统IO路径的之歌重要角色,文件在系统底层存储卷或者磁盘上的分布算法是重中之重,传统的文件系统只是将底层的卷当做一个连续的扇区空间,并不感知这个连续空间的物理承载设备的类型或者数量等,所以也就不知道自己的不同类型的IO行为会给性能带来身影响。如果我们再FS层对底层的RAID类型和磁盘类型等各种因素做出分析和判断,然后制定对应的策略,让文件能够按照预期的效果有针对性地在RAID组内进行分布。这样就会大大提高系统读写性能。比如,在格式化文件系统时,或者在程序调用时,给出显式参数:尽量保持每个文件存放在一个屋里硬盘中,那么当创建一个文件的时候,文件系统便会根据底层RAID的Stripe边界来计算将哪些扇区地址段分配给这个文件,从而让其物理地只分布到一个此磁盘中,这种文件分布方式在需要并行访问大龄文件时是非常有意义的,因为底层RAID的盲并发度很低,如果在FS层面手动地将每个文件只分布一个磁盘上,那么N个磁盘组成的TAID组理论上就可以并发N个针对文件的读操作,写操作并发值仍相对很低,但是至少可以保证为理论最大值。

总之、文件系统必须与底层完美配合才能获得最大性能。


相关文章
|
6月前
|
缓存 运维 Linux
Linux系统调优详解(十二)——IO调优之磁盘测速
Linux系统调优详解(十二)——IO调优之磁盘测速
94 1
|
2月前
crash —— 获取系统的磁盘IO统计数据
crash —— 获取系统的磁盘IO统计数据
|
6月前
|
运维 Linux
Linux系统调优详解(五)——磁盘IO状态查看命令
Linux系统调优详解(五)——磁盘IO状态查看命令
133 5
|
4月前
|
Linux 数据处理 C语言
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(下)
73 0
|
4月前
|
缓存 网络协议 算法
【Linux系统编程】深入剖析:四大IO模型机制与应用(阻塞、非阻塞、多路复用、信号驱动IO 全解读)
在Linux环境下,主要存在四种IO模型,它们分别是阻塞IO(Blocking IO)、非阻塞IO(Non-blocking IO)、IO多路复用(I/O Multiplexing)和异步IO(Asynchronous IO)。下面我将逐一介绍这些模型的定义:
189 1
|
4月前
|
Linux C语言 C++
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(上)
【Linux】基础IO----系统文件IO & 文件描述符fd & 重定向(上)
49 0
|
5月前
|
存储 缓存 NoSQL
Redis为什么速度快:数据结构、存储及IO网络原理总结
Redis为什么速度快:数据结构、存储及IO网络原理总结
|
5月前
使用字节输入流报错 java.io.FileNotFoundException: srcdruid.properties (系统找不到指定的文件。)
使用字节输入流报错 java.io.FileNotFoundException: srcdruid.properties (系统找不到指定的文件。)
41 0
|
6月前
|
存储 搜索推荐 文件存储
IO、存储、硬盘、文件系统相关常识
IO、存储、硬盘、文件系统相关常识
52 2
|
6月前
|
移动开发 前端开发 JavaScript
uniapp中IO模块(管理本地文件系统)的常用功能封装
uniapp中IO模块(管理本地文件系统)的常用功能封装
567 1