最近比较懒, 还是加班写点东西吧, 不然过段时间又把这些整理的东西弄丢了.
写什么呢? 写一些跟工作相关的吧! 因为笔者从事多媒体录像相关的开发工作, 因此常常涉及到优化写卡策略, 提升写卡性能相关的方面的事情. 此话怎讲呢? 如行车记录仪类的录像产品, 录像可能持续多日, 越往后写卡速度会越来越慢, 直观感受是取出视频文件进行回放时, 时间约往后的视频文件卡顿越来越严重.
怎样解决呢? 一种方案从硬件解决, 换一张好卡! 但是这不能一劳永逸解决问题, 因为录着录着写卡速度又掉下来了. 另外一种方案从软件层面解决, 就是卡速变慢了后, 将卡格式化, 但是这种方案对于用户来讲不太友好(有些用户可能不知道这个功能, 或者文件删除前备份不方便). 还有一种方案, 也是从软件层面解决问题, 就是优化写卡策略. 优化写卡策略, 有一些可行的方案, 例如文件预分配, 待写数据进行缓冲写, 编码与封装解耦, 直写(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 的空间浪费. 但是, 使用预分配对于行车记录仪类产品是个较优的选择, 因为文件切换是定时切换的, 如果编码器输出码率是相对稳定的, 就可以预估最终文件大小, 预分配的大小再留些余量就可以了.
来源: https://www.cnblogs.com/Dreaming-in-Gottingen/p/11980542.html