磁盘介绍:
磁盘是可以持久化存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘。
机械磁盘,也称为硬盘驱动器(hard disk driver),通常缩写为 hdd。机械磁盘主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。
显然,如果 i/o 请求刚好连续,那就不需要磁道寻址,自然可以获得最佳性能。这其实就是我们熟悉的,连续 i/o 的工作原理。与之相对应的,当然就是随机 i/o,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。
固态磁盘(solid state disk),通常缩写为 ssd,由固态电子元器件组成。固态磁盘不需要磁道寻址,所以,不管是连续 i/o,还是随机 i/o 的性能,都比机械磁盘要好得多。
其实,无论机械磁盘,还是固态磁盘,相同磁盘的随机 i/o 都要比连续 i/o 慢很多,原因也很明显。
对机械磁盘来说,我们刚刚提到过的,由于随机 i/o 需要更多的磁头寻道和盘片旋转,它的性能自然要比连续 i/o 慢。
而对固态磁盘来说,虽然它的随机性能比机械硬盘好很多,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机 i/o 的性能比起连续 i/o 来,也还是差了很多。
此外,连续 i/o 还可以通过预读的方式,来减少 i/o 请求的次数,这也是其性能优异的一个原因。很多性能优化的方案,也都会从这个角度出发,来优化 i/o 性能。
此外,机械磁盘和固态磁盘还分别有一个最小的读写单位。
机械磁盘的最小读写单位是扇区,一般大小为 512 字节。
而固态磁盘的最小读写单位是页,通常大小是 4kb、8kb 等。
如果每次都读写 512 字节这么小的单位的话,效率很低。所以,文件系统会把连续的扇区或页,组成逻辑块,然后以逻辑块作为最小单元来管理数据。常见的逻辑块的大小是 4kb,也就是说,连续 8 个扇区,或者单独的一个页,都可以组成一个逻辑块。
除了可以按照存储介质来分类,另一个常见的分类方法,是按照接口来分类,比如可以把硬盘分为 ide(integrated drive electronics)、scsi(small computer system interface) 、sas(serial attached scsi) 、sata(serial ata) 、fc(fibre channel) 等。
不同的接口,往往分配不同的设备名称。比如, ide 设备会分配一个 hd 前缀的设备名,scsi 和 sata 设备会分配一个 sd 前缀的设备名。如果是多块同类型的磁盘,就会按照 a、b、c 等的字母顺序来编号。
除了磁盘本身的分类外,当你把磁盘接入服务器后,按照不同的使用方式,又可以把它们划分为多种不同的架构。
最简单的,就是直接作为独立磁盘设备来使用。这些磁盘,往往还会根据需要,划分为不同的逻辑分区,每个分区再用数字编号。比如 /dev/sda ,还可以分成两个分区 /dev/sda1 和 /dev/sda2。
另一个比较常用的架构,是把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列,也就是 raid(redundant array of independent disks),从而可以提高数据访问的性能,并且增强数据存储的可靠性。
根据容量、性能和可靠性需求的不同,raid 一般可以划分为多个级别,如 raid0、raid1、raid5、raid10 等。
raid0 有最优的读写性能,但不提供数据冗余的功能。
而其他级别的 raid,在提供数据冗余的基础上,对读写性能也有一定程度的优化。具体大家自行百度吧,这里不再详细介绍。
最后一种架构,是把这些磁盘组合成一个网络存储集群,再通过 nfs、smb、iscsi 等网络存储协议,暴露给服务器使用。
其实在 linux 中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。
iostat 是最常用的磁盘 i/o 性能观测工具,它提供了每个磁盘的使用率、iops、吞吐量等各种常见的性能指标,当然,这些指标实际上来自 /proc/diskstats。
在io问题排查中,有时候用到iostat -x这命令,详细示例如下:
可以看到%idle(%idle小于70%说明io压力已经比较大了)和%util的值都处于非正常状态。
从这里你可以看到,iostat 提供了非常丰富的性能指标。
avg-cpu说明:
%user:在用户级别运行所使用的cpu的百分比。
%nice:带nice值(和进程优先级相关)的用户模式下运行所使用的cpu的百分比。
%system:在系统级别运行所使用cpu的百分比。
%iowait:cpu等待io完成的时间百分比。(单个iowait指标值偏高并不能说明磁盘存在io瓶颈,下面会有详述。)
%steal:管理程序维护另一个虚拟处理器时,虚拟cpu的无意识等待时间的百分比。
%idle:cpu空闲时间的百分比。(idle值高,表示cpu较空闲。)
device说明:
第一列的 device 表示磁盘设备的名字,其他各列指标,虽然数量较多,但是每个指标的含义都很重要。为了方便你理解,我把它们总结成了一个表格。
这些指标中,你要注意:
%util ,就是我们前面提到的磁盘 i/o 使用率;
r/s w/s ,就是 iops;
rkb/s wkb/s ,就是吞吐量;
r_await w_await ,就是响应时间。
在观测指标时,也别忘了结合请求的大小( rareq-sz 和 wareq-sz)一起分析。
正常情况下svctm应该是小于await值的,而svctm的大小和磁盘性能有关,cpu、内存的负荷也会对svctm值造成影响,过多的请求也会间接的导致svctm值的增加。await值的大小一般取决于svctm的值和io队列的长度以及io请求模式,如果scvtm比较接近await,说明io几乎没有等待时间;如果await远大于svctm,说明io请求队列太长,io响应太慢,则需要进行必要优化。
如果%util接近100%,说明产生的io请求太多,io系统已经满负荷,该磁盘可能存在瓶颈。
队列长度(avgqu-sz)也可作为衡量系统 i/o 负荷的指标,但由于 avgqu-sz 是按照单位时间的平均值,所以不能反映瞬间的 i/o 泛洪,如果avgqu-sz比较大,则说明有大量io在等待。
(可以看完下面一节再来回顾这段内容。)
对于await, svctm以及%util等,光从概念上来说,比较晦涩,可以通过下图的磁盘io流程来加深理解:
简要图:
详细图
(图片来自 linux storage stack diagram )
磁盘io场景
- 用户调用标准c库进行io操作,数据流为:应用程序buffer->c库标准iobuffer->文件系统page cache->通过具体文件系统到磁盘。
- 用户调用文件io,数据流为:应用程序buffer->文件系统page cache->通过具体文件系统到磁盘。
- 用户打开文件时使用o_direct,绕过page cache直接读写磁盘。
- 用户使用类似dd工具,并使用direct参数,绕过系统cache与文件系统直接写磁盘。
发起io请求请的步骤简析(以最长链路为例)
写操作:
- 用户调用fwrite把数据写c库标准iobuffer后就返回,即写操作通常是个异步操作。
- 数据到c库标准iobuffer后,不会立即刷新到磁盘,会将多次小数据量相邻写操作先缓存起来合并,最终调用write函数一次性写入(或者将大块数据分解多次write调用)page cache。
- 数据到page cache后也不会立即刷新到磁盘,内核有pdflush线程在不停的检测脏页,判断是否要写回到磁盘中,如果是则发起磁盘io请求。
读操作: - 用户调用fread到c库标准iobuffer读取数据,如果成功则返回,否则继续。
- 到page cache读取数据,如果成功则返回,否则继续。
- 发起io请求,读取到数据后缓存buffer和c库标准iobuffer并返回。可以看出,读操作是同步请求。
io请求处理
- 通用块层根据io请求构造一个或多个bio结构并提交给调度层。bio结构描述对一个磁盘扇区读/写操作。
- 调度器将bio结构进行排序和合并组织成队列且确保读写操作尽可能理想:将一个或多个进程的读操作合并到一起读,将一个或多个进程的写操作合并到一起写,尽可能变随机为顺序(因为随机读写比顺序读写要慢),读必须优先满足,而写也不能等太久。
linux的io调度器有时也称之为磁盘调度器,工作机制是控制块设备的请求队列,确定队列中那些io的优先级更高以及何时下发io到块设备,以此来减少磁盘寻到时间,从而提高系统的吞吐量。
目前linux共有如下几种io调度算法:
noop算法
noop算法的全写为no operation。该算法实现了最最简单的fifo队列,所有io请求大致按照先来后到的顺序进行操作。之所以说“大致”,原因是noop在fifo的基础上还做了相邻io请求的合并,并不是完完全全按照先进先出的规则满足io请求。
假设有如下的io请求序列:
100,500,101,10,56,1000
noop将会按照如下顺序满足:
100(101),500,10,56,1000
2、cfq
cfq算法的全写为completely fair queuing。该算法的特点是按照io请求的地址进行排序,而不是按照先来后到的顺序来进行响应。
假设有如下的io请求序列:
100,500,101,10,56,1000
cfq将会按照如下顺序满足:
100,101,500,1000,10,56
cfq是默认的磁盘调度算法,对于通用服务器来说最好的选择。它视图均匀地分布对io带宽的访问。cfq为每个进程单独创建一个队列来管理该进程所产生的请求,也就是说每个进程一个队列,各队列之间的调度使用时间片来调度,以此来保证每个进程都能被很好的分配到io带宽。io调度器每次执行一个进程的4次请求。在传统的sas盘上,磁盘寻道花去了绝大多数的io响应时间。cfq的出发点是对io地址进行排序,以尽量少的磁盘旋转次数来满足尽可能多的io请求。在cfq算法下,sas盘的吞吐量大大提高了。但是相比于noop的缺点是,先来的io请求并不一定能被满足,可能会出现饿死的情况。
3、deadline
deadline在cfq的基础上,解决了io请求饿死的极端情况。除了cfq本身具有的io排序队列之外,deadline额外分别为读io和写io提供了fifo队列。读fifo队列的最大等待时间为500ms,写fifo队列的最大等待时间为5s。fifo队列内的io请求优先级要比cfq队列中的高,而读fifo队列的优先级又比写fifo队列的优先级高。优先级可以表示如下:
fifo(read) > fifo(write) > cfq
4、anticipatory
cfq和deadline考虑的焦点在于满足零散io请求上。对于连续的io请求,比如顺序读,并没有做优化。为了满足随机io和顺序io混合的场景,linux还支持anticipatory调度算法。anticipatory的在deadline的基础上,为每个读io都设置了6ms的等待时间窗口。如果在这6ms内os收到了相邻位置的读io请求,就可以立即满足。 anticipatory 算法通过增加等待时间来获得更高的性能,假设一个块设备只有一个物理查找磁头(例如一个单独的sata硬盘),将多个随机的小写入流合并成一个大写入流(相当于给随机读写变顺序读写),使用这个原理来使用读取写入的延时换取最大的读取写入吞吐量.适用于大多数环境,特别是读取写入较多的环境。
不同的磁盘调度算法(以及相应的io优化手段)对kafka这类依赖磁盘运转的应用的影响很大,建议根据不同的业务需求来测试选择合适的磁盘调度算法(以后的文章中会有相关的测试介绍)。
查看设备当前的io调度器:cat /sys/block/{device-name}/queue/scheduler。其中{device-name}指的是磁盘设备的名称,即文章开头iostat -x中device下方的vda,vdb等。
举例:
[root@hidden ~]# cat /sys/block/vda/queue/scheduler
noop anticipatory deadline [cfq]
修改当前的io调度器: echo {scheduler-name} > /sys/block/{device-name}/queue/scheduler。其中{scheduler-name}取值为noop、anticipatory、deadline、cfq其中之一。
举例:
[root@hidden ~]# echo noop > /sys/block/vda/queue/scheduler
[root@hidden ~]# cat /sys/block/vda/queue/scheduler
[noop] anticipatory deadline cfq
以上设置重启之后会失效,如果要想重启后配置仍然生效,需要在内核启动参数中将elevator={scheduler-name}写入/boot/grub/menu.lst文件中。在修改这个文件之前最好先备份一份,然后将elevator={scheduler-name}添加到文件末尾即可。