最近比较懒,还是加班写点东西吧,不然过段时间又把这些整理的东西弄丢了。
写什么呢?写一些跟工作相关的吧!因为笔者从事多媒体录像相关的开发工作,因此常常涉及到优化写卡策略、提升写卡性能相关的方面的事情。此话怎讲呢?如行车记录仪类的录像产品,录像可能持续多日,越往后写卡速度会越来越慢,直观感受是取出视频文件进行回放时,时间约往后的视频文件卡顿越来越严重。
怎样解决呢?一种方案从硬件解决,换一张好卡!但是这不能一劳永逸解决问题,因为录着录着写卡速度又掉下来了。另外一种方案从软件层面解决,就是卡速变慢了后,将卡格式化,但是这种方案对于用户来讲不太友好(有些用户可能不知道这个功能,或者文件删除前备份不方便)。还有一种方案,也是从软件层面解决问题,就是优化写卡策略。优化写卡策略,有一些可行的方案,例如文件预分配、待写数据进行缓冲写、编码与封装解耦,直写(DirectIO)。下面内容介绍预分配的内容。
1. fallocate介绍
linux man手册说明:
fallocate即预分配,英文为preallocate。什么意思呢?还往文件中没写数据,但是已经给文件分配了足额的物理空间来存储数据。创建了文件,再调用这个接口预分配了一定量的空间后,后续就可以往这个文件中写数据了。
另外一点需要注意,这个接口需要文件系统的支持。常用TF卡录像,而卡的文件系统类型一般为fat32,就需要fat32文件系统相关的实现才能使用该功能。
再有,这是一个不可移植的linux专用系统调用,用于确保文件空间被提前分配,成功执行后,可以确保写卡速度较快,也能保证不会因为磁盘空间不足而出现写失败。
2 . 接口声明
函数原型 |
int fallocate(int fd, int mode, off_t offset, off_t len); |
fd |
文件句柄 |
mode |
创建模式 |
offset |
偏移 |
len |
文件大小 |
其中,在创建了文件后和写数据前,需要调用该接口进行预分配,第二个参数mode一般设置为1,第三个参数设置为0,第四个参数填上期望预分配值。
3. 应用场景及目标
应用场景:持续写卡场景,例如行车记录仪、运动相机。
目标:减少磁盘碎片化,提高写卡速度。
其他说明:录像设备的瓶颈常常是写卡,因为要随时将视频文件记录下来。并且,对持续写卡速度要求较高,因为录像设备工作周期可能是以day为单位,不仅要求录像刚启动时写卡正常,而且要求工作了几天写卡速度也不能掉太多。至于每秒钟写入的数据量,视编码器输出码率和几路录像而定,对于单路1080p录制,视频码率设置为10mbps,那么卡速至少要保证2MB/s,这里面还不包括写log以及录像中拍照所用的。
虽然目前时间节点上(2019年末),市面上卡都是C10(10MB/s)及其以上,但是如果写策略不合理或卡中太多零碎文件,写速度可能很低。很常见的一个例子,拷贝一个视频文件到T卡的速度,要远远大于拷贝同样大小的源文件包。另一个例子是,一个刚格式化的T卡与一个内部已经存在了很多文件的T卡(卡品牌、容量、速度等参数都一样),拷贝同样大小的文件,刚格式化的那张卡速度更快。
4. 实现原理
TF卡(TransCard)和SSD(SolidStateDisk)作为常见的存储设备,内部组成非常类似,都主要由controler和nand flash组成。对于任何存储设备,我们都最关心三个参数:容量、读/写速度、寿命。
“容量”这个参数勿用介绍,“读速度”也不介绍,下面主要说下“寿命”和“写速度”这两个参数。介绍这两个参数后,再来介绍预分配。
4.1 寿命相关:
寿命主要由存储介质决定,即nand flash这种介质的可擦写次数,nand flash介质类型的发展经历了slc、mlc、tlc、qlc(目前市面上还较少)几个阶段,单位面积的容量也越来越大,因为介质类型反映了存储密度。小小的TF卡,就目前2019年末的这个时间节点上,市面上已经出现了512GB容量的TF卡,存储多个图书馆书籍的文字信息应该毫无压力!但是,凡事有利有弊,随着容量的提升,TF内部的最小存储单元的可擦写次数也越来越少。
SLC(SingleLevelCell)出现最早,可擦写次数10多万次;后来出现的MLC(MultiLevelCell)可擦写次数3000-10000次左右,目前主流的TLC的可擦写次数在500-1000次左右。在某东上随便查看了lexar的某款500GB 容量的SSD,其参数如下:
从中看到闪存类型为TLC,还有TBW=250T这个参数,这个是什么以及怎么得来的呢?
TBW,即TeraBytesWritten,以TB为单位的写入的数据量。这个值这样算:总容量*可写次数,即500GB*500 = 250TB。其中的500代表平均可写次数为500,是根据闪存类型TLC来估算的。一般企业级的用的sdd,价格较民用的高不少,例如编译/数据库服务器,相同容量的TBW值通常是以PBW(=1024TBW)为单位的,不太追求读写速度,但非常看重寿命和可靠性,毕竟数据是无价的。
4.2 速度相关:
写速度是个比较玄乎的东西,由许多因素综合导致,例如,闪存类型、主控算法(固件磨损平衡算法)、文件系统写策略、卡的碎片化程度、卡的文件系统类型和block大小、内部是否带Cache以及其大小,等等诸多因素。
但是,针对确定下来的一张卡,我们需要找到一些方法,来提高写卡速度。其中一种方法就是预分配——fallocate。
接下来先介绍文件存储相关的内容后,再来介绍这个预分配接口的作用。
对于fat32的文件系统,存储设备中的某文件,其内容主要包括两部分:一部分是属性信息metadata(创建/修改时间、文件名称、文件大小等),另一部分是真正的数据内容。常用的fdatasync操作只会强制将真正的数据内容刷新到存储设备中,而fsync会将两部分内容都刷新到设备中。对于真正的数据内容那部分,有一个链表来管理各个块内容所在的SectorId,即以sector链表的形式来完整表述数据内容。因此,某文件的存储物理地址可能是某连续sector区所在的一整片区域,也可能分布于多个不连续的物理区域。
存储设备的碎片化与内存碎片化非常类似,即某文件希望尽可能利用连续的物理存储空间来存储数据,但是由于卡已处于高度碎片化状态,当真正写入完这个文件时,这个文件在物理空间上是“支离破碎”的。即使是一个刚刚格式化的卡,当两个线程同时分别写两个不同文件时,在物理空间上(内部连续的物理block或sector),这两个文件可能处于交织状态(交错),英文为interleave。做过音频开发的同事也可以回想一下alsa-lib在打开设备进行参数配置时,针对双声道pcm数据采集,有interleave和non-interleave的配置,这个选择决定了左右声道pcm数据在一个period内如何排列,类似对比,卡中存储的多个文件,对于物理block就是这个意思。
设想一种写文件场景,使用正常fopen-fwrite-fclose的操作流程,只写一路,当每次将kernel cache中的数据刷到卡中前,需要现场去找(类似于写磁盘时的寻道)哪个物理sector是available的,当发现某个block中的某个sector是可用的,但是其他sector是其他文件占用的,那么接下来的策略就是copy-modify-write,即出现了“写放大”(WriteAmplification)。
为什么出现这个状况,需要了解闪存的基本组成:页page(也称sector,大小4KB) -> 块block(通常64或128个page组成一个block) -> 面plane(多个block组成) –> die(plane就是一个die) -> 闪存片(多个die组成) –> SSD或TF(多颗闪存片组成)。
下面描述下写放大过程:先把整个block中的数据完全拷贝到ddr,再将某个sector中的数据修改为期望写入的数据,擦掉ssd中这个block的内容,然后再整体将ddr中的已修改好的数据写入到ssd中这个block位置。为什么要这样做?因为写入是按block为最基本单位进行的。所以写入一笔数据,涉及了多次基本操作,不仅减慢了写速度,而且减少了寿命。然而,当进行了预分配后,提前为某文件划分了“势力范围”,标定某些位置已经被占用,可以减少后续的写放大和寻找可用空间的过程。
4.3 预分配原理:
介绍了文件存储结构的相关内容后,对于预分配的功能我们就有了大致的猜测!fallocate这个接口,其要实现的目的,就是在数据内容还未写入到设备前,提前为文件分配好若干大小的空间,并且使这个空间尽可能是物理连续的,这样可以减少后续写放大的出现频率,以及不需在写入过程中寻找可用空间,更不会出现写数据时磁盘空间不足的问题!
5. 其他问题
使用预分配一个最大的问题是——磁盘空间利用率不高!这个如何说起?文件刚创建还未写入数据,我们就抢先为文件设置了文件的大小并占用了固定大小的物理空间,但通常可能未写入那么大size的数据量就fclose了这个文件,那么这个文件内未写入的空间就不能被其他文件利用了。一个文件预分配了100MB,即使只写入1MB就关闭,那么就有99MB的空间浪费。但是,使用预分配对于行车记录仪类产品是个较优的选择,因为文件切换是定时切换的,如果编码器输出码率是相对稳定的,就可以预估最终文件大小,预分配的大小再留些余量就可以了。