大数据小视角5:探究SSD写放大的成因与解决思路
笔者目前开发运维的存储系统的服务器都跑在SSD之上,目前单机服务器最大的SSD容量有4T之多。(公司好有钱,以前在实验室都只有机械硬盘用的~~)但SSD本身的特性与机械硬盘差距较大,虽然说在性能上有诸多优势,但是如果使用的方式方法不对,反而会事倍功半。所以笔者花时间调研了一下固态硬盘的结构与特性,并且总结了一些避免SSD写放大性能下降的法则,希望对大家有所帮助~~
1.SSD的写放大
首先我们来看看什么是写放大,写放大(Write amplification)是2008年,由英特尔和SiliconSystems在论文之中首次提出:它表现为在SSD上实际写入的数据远远大于用户写入数据。
为什么会产生这样的现象呢,我们要回归到固态硬盘原本的特性。首先固态硬盘与传统的机械硬盘不同的点在于它不能够覆盖写。所以对于已经存在数据SSD来说,一次数据的写入分为2个动作:
- 1、擦除SSD上已有的数据。
- 2、写入新的数据。
写入放大的问题就出了这个部分,因为SSD每次写入的最小单位为Page,每个Page是4KB大小,而每次擦除的大小单位为Block,Block通常由64或128个Page组成。也就是说,正是由于SSD写入与擦除的单位大小不匹配,导致了写入放大。如果仅仅是写入单一Page的数据,而单个Block之中没有了空余的Page,则需要擦除一个Block的数据,之后再写入一个Block的数据。所以说,本身只需要4KB的写入,"放大"了64倍甚至是128倍!由于SSD的寿命取决于擦除次数,所以写入放大会大大影响到SSD的使用寿命
OK,了解了写入放大的现象之后,我们接下来怎么样会导致写入放大的现象呢?
- 写入量
这点应该很好理解,由上面的阐述可以看到,如果每次对SSD的写入都是很小的量,就会产生典型的写入放大。
- 剩余容量
通常我们使用的SSD都存在预留空间(OP)来用于给SSD的主控芯片来进行一些优化操作。其中预留空间最为核心的两项工作就是垃圾回收与损耗均衡,在这里笔者简要介绍一下垃圾回收和损耗均衡。
垃圾回收:
SSD中主流的垃圾回收算法与Java之中的标记清除的垃圾回收算法类似:
如上图所示,SSD首先在Block X之中写入A-D的Page页,之后继续写入到H,并且更新了A'-D',所以原先的A-D的page页成为了需要回收的垃圾。所以主控芯片会将可用数据移动到旁边的有空闲的Block Y,同时完整的擦除Block X。当剩余容量越小,垃圾回收越频发,则SSD的写入放大就越为严重。就是为什么许多手机在刚刚买来之后丝滑如顺,但是之后就越用越卡,这是因为容量越来越小了,SSD需要背锅!!(这也是笔者的64G的小米5剩余容量只有4个G了,日常使用卡如狗的部分原因~~)需要频繁进行垃圾回收的场景会导致写入放大的问题更为严重。
损耗均衡:
我们知道,SSD的使用寿命等同于Block的擦写次数,目前主流使用的MLC颗粒的编程/擦写次数一般在3000 ~ 5000次左右。如果反复地编程和擦写某个Block,该Block则会先于其他Blcok损坏,导致坏块大量出现。正是因为这样的原因,SSD的主控芯片会尽可能的让每个Block的擦写次数均匀。所以损耗均衡的操作需要移动并没有新数据写入的Block,这样同样也会导致写放大的上升。
2.写放大问题的一些解决思路
了解了SSD的写放大的成因之后,我们可以『对症下药』的尝试给出一些方案来减小写放大问题来对我们线上服务的影响。
-
批量写
这几乎是解决磁盘io问题的通用解决方案,同样适合于传统的机械硬盘与SSD。尽量在代码逻辑之中减少随机写的次数,来避免由少量写操作引发的写放大问题,同时可以考虑通过块对齐的方式来进一步减少写入产生的写放大问题。(当然这里所谓块对齐的思路在是与程序运行环境紧耦合的,程序的可移植性会大打折扣) -
预留空间,容量告警
这也是我们运维线上机器常用的思路,对于SSD的使用量要有一个阀值,超过我们的预设容量时,线上的程序需要给运维和开发人员告警。 -
写压缩
写压缩是依赖SSD的主控芯片的,部分SSD主控芯片支持写压缩。也就说接受到操作系统发送要写入20m数据,主控芯片可以通过一些流压缩或块压缩的算法压缩数据,在读取数据时,再重新进行解压。这种方式强依赖硬件,并且新的瓶颈可能会是主控芯片的压缩,解压的速度。 -
TRIM命令
TRIM是操作系统层级的命令。操作系统利用TRIM命令来标记SSD上某个Page的数据可以回收。一旦某个Page被SSD标记为可以回收,在SSD空闲的时候SSD的主控芯片会将这些被标记的Page数据收集到同一个Block,然后共同擦除。这样每次需要写数据时,就在已经有足够空闲的Page可以写入新的数据。
上述几个思路都是在实践中可以采取的措施,其实TRIM命令需要通过Linux设置开启,这里笔者在这里介绍一下如何在Linux下开启TRIM命令:
- 确认Linux的内核是否大于 2.6.28,笔者这里是4.9.0的内核。
2.调用hdparm -I /dev/sda1 命令确认SSD设备是否支持TRIM。
3.修改/etc/fstab文件,在挂载选项之中添加discard,重启之后就开启了TRIM
3.小结
到此为止,笔者聊了聊SSD写放大的成因,并且针对SSD写放大的成因,提出了一些解决的思路和方法,希望大家能有所收获,在生产环境之中能够更好的『调教好』SSD~~~
- 在Java EE7框架中使用MongoDB
- 用事实说话,成熟的ORM性能不是瓶颈,灵活性不是问题:EF5.0、PDF.NET5.0、Dapper原理分析与测试手记
- iPhone的Wi-Fi芯片漏洞利用POC公布,赶紧更新系统吧
- No.003 Longest Substring Without Repeating Characters
- 【Spark研究】极简 Spark 入门笔记——安装和第一个回归程序
- 通常Java开发人员如何进行数据排序?
- 消息服务框架使用案例之--大文件上传(断点续传)功能
- Java中三种Set类型用法、性能大比拼
- Android基础总结(5)——数据存储,持久化技术
- 如何突破Windows环境限制打开“命令提示符”
- 【Spark研究】Spark之工作原理
- Java中泛型使用的必要性
- “一切都是消息”--MSF(消息服务框架)之【发布-订阅】模式
- Android基础总结(4)——广播接收器
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Java自动化测试(Android app界面元素 33)
- 太厉害了,这款开源类库可以帮你简化每一行代码
- Linux ps和pstree命令知识点总结
- CentOS7上以rpm方式安装JDK8
- linux DMA接口知识点详解
- Linux中使用crond工具创建定时任务的方法
- Linux which命令的具体使用
- Linux安装Python3.8.1的教程详解
- linux压缩文件命令zip的实例用法
- centos下samba文件夹共享服务器配置详解
- Centos7安装FFmpeg音/视频工具简易文档
- Linux 进程通信之FIFO的实现
- Linux nl命令的使用方法
- Linux gcc命令的具体使用
- Linux dirname命令的具体使用