1.Linux 文件系统的工作原理
1.索引节点和目录项
在 Linux 中一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。
为了方便管理,Linux 文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。
- 索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。
- 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。
2.虚拟文件系统
虚拟文件系统VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。
下面这张图是Linux 文件系统的架构图,帮你更好地理解系统调用、VFS、缓存、文件系统以及块存储之间的关系。
通过这张图,你可以看到,在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。
- 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。
- 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。
- 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等。
3.文件系统 I/O
文件读写方式的各种差异,导致 I/O 的分类多种多样。最常见的有,缓冲与非缓冲 I/O、直接与非直接 I/O、阻塞与非阻塞 I/O、同步与异步 I/O 等。 接下来,我们就详细看这四种分类。
第一种,根据是否利用标准库缓存,可以把文件 I/O 分为缓冲 I/O 与非缓冲 I/O。
- 缓冲 I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。
- 非缓冲 I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。
无论缓冲 I/O 还是非缓冲 I/O,它们最终还是要经过系统调用来访问文件。而根据上一节内容,我们知道,系统调用后,还会通过页缓存,来减少磁盘的 I/O 操作。
第二,根据是否利用操作系统的页缓存,可以把文件 I/O 分为直接 I/O 与非直接 I/O。
- 直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。
- 非直接 I/O 正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。
想要实现直接 I/O,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接 I/O。
不过要注意,直接 I/O、非直接 I/O,本质上还是和文件系统交互。如果是在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/O。
第三,根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O:
- 所谓阻塞 I/O,是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。
- 所谓非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。
比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。
第四,根据是否等待响应结果,可以把文件 I/O 分为同步和异步 I/O:
- 所谓同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得 I/O 响应。
- 所谓异步 I/O,是指应用程序执行 I/O 操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O 完成后,响应会用事件通知的方式,告诉应用程序。
4.性能观测
容量
对文件系统来说,最常见的一个问题就是空间不足。当然,你可能本身就知道,用 df 命令,就能查看文件系统的磁盘空间使用情况。比如:
df -h /dev/sda1
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 20G
6
.4G 13G
35
% /
|
不过有时候,明明你碰到了空间不足的问题,可是用 df 查看磁盘空间后,却发现剩余空间还有很多。这是怎么回事呢?
#给 df 命令加上 -i 参数,查看索引节点的使用情况
df -i /dev/sda1
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1
1310720
167343
1143377
13
% /
|
索引节点的容量,(也就是 Inode 个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。所以,一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题。
缓存
在前面 Cache 案例中,我已经介绍过,可以用 free 或 vmstat,来观察页缓存的大小。free 输出的 Cache,是页缓存和可回收 Slab 缓存的和,你可以从 /proc/meminfo ,直接得到它们的大小:
cat /proc/meminfo |grep -E
"SReclaimable|Cached"
Cached:
431680
kB
SwapCached:
27780
kB
SReclaimable:
88540
kB
|
文件系统中的目录项和索引节点缓存,又该如何观察呢?
比如,运行下面的命令,你就可以得到,所有目录项和各种文件系统索引节点的缓存情况:
cat /proc/slabinfo | grep -E
'^#|dentry|inode'
# name : tunables : slabdata
ovl_inode
282
282
688
47
8
: tunables
0
0
0
: slabdata
6
6
0
mqueue_inode_cache
68
68
960
34
8
: tunables
0
0
0
: slabdata
2
2
0
fuse_inode
78
78
832
39
8
: tunables
0
0
0
: slabdata
2
2
0
ecryptfs_inode_cache
0
0
1024
32
8
: tunables
0
0
0
: slabdata
0
0
0
fat_inode_cache
0
0
744
44
8
: tunables
0
0
0
: slabdata
0
0
0
squashfs_inode_cache
11523
11638
704
46
8
: tunables
0
0
0
: slabdata
253
253
0
ext4_inode_cache
23845
27600
1088
30
8
: tunables
0
0
0
: slabdata
920
920
0
hugetlbfs_inode_cache
102
102
632
51
8
: tunables
0
0
0
: slabdata
2
2
0
sock_inode_cache
3990
4278
704
46
8
: tunables
0
0
0
: slabdata
93
93
0
shmem_inode_cache
2307
2668
712
46
8
: tunables
0
0
0
: slabdata
58
58
0
proc_inode_cache
4852
5232
680
48
8
: tunables
0
0
0
: slabdata
109
109
0
inode_cache
22067
23956
608
53
8
: tunables
0
0
0
: slabdata
452
452
0
dentry
68912
88074
192
42
2
: tunables
0
0
0
: slabdata
2097
2097
0
|
这个界面中,dentry 行表示目录项缓存,inode_cache 行,表示 VFS 索引节点缓存,其余的则是各种文件系统的索引节点缓存。
在实际性能分析中,我们更常使用 slabtop ,来找到占用内存最多的缓存类型。
slabtop
|
2.Linux 磁盘 IO 的工作原理
1.磁盘
磁盘是可以持久化存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘。
- 第一类,机械磁盘,也称为硬盘驱动器(Hard Disk Driver),通常缩写为 HDD。机械磁盘主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。
- 第二类,固态磁盘(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 性能。
2.通用块层
通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能 。
- 第一个功能跟虚拟文件系统的功能类似。向上,为文件系统和应用程序,提供访问块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。
- 第二个功能,通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。
3.I/O 栈
我们可以把 Linux 存储系统的 I/O 栈,由上到下分为三个层次,分别是文件系统层、通用块层和设备层。
存储系统 I/O 的工作原理:
- 文件系统层,包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过通用块层,来存储和管理磁盘数据。
- 通用块层,包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。
- 设备层,包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作。
4.磁盘性能指标
说到磁盘性能的衡量标准,必须要提到五个常见指标,也就是我们经常用到的,使用率、饱和度、IOPS、吞吐量以及响应时间等。这五个指标,是衡量磁盘性能的基本指标。
- 使用率,是指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈。
- 饱和度,是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求。
- IOPS(Input/Output Per Second),是指每秒的 I/O 请求数。
- 吞吐量,是指每秒的 I/O 请求大小。
- 响应时间,是指 I/O 请求从发出到收到响应的间隔时间。
这里要注意的是,使用率只考虑有没有 I/O,而不考虑 I/O 的大小。换句话说,当使用率是 100% 的时候,磁盘依然有可能接受新的 I/O 请求。
4.1磁盘 I/O 观测
第一个要观测的,是每块磁盘的使用情况。
iostat 的输出界面如下。
-d -x 表示显示所有磁盘 I/O 的指标
$ iostat -d -x
1
Linux
4.18
.
0
-
15
-generic (ubuntu)
02
/
26
/
2019
_x86_64_ (
2
CPU)
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
loop0
0.00
0.00
0.01
0.00
0.00
0.00
0.00
0.00
1.63
0.00
0.00
3.73
0.00
1.85
0.00
loop1
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.33
0.00
0.00
2.53
0.00
0.57
0.00
loop2
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
3.12
0.00
0.00
1.91
0.00
2.83
0.00
loop3
0.05
0.00
0.07
0.00
0.00
0.00
0.00
0.00
0.71
0.00
0.00
1.50
0.00
0.24
0.00
loop4
0.02
0.00
0.03
0.00
0.00
0.00
0.00
0.00
0.18
0.00
0.00
1.35
0.00
0.26
0.00
loop5
0.02
0.00
0.04
0.00
0.00
0.00
0.00
0.00
0.24
0.00
0.00
2.23
0.00
0.29
0.00
loop6
0.00
0.00
0.01
0.00
0.00
0.00
0.00
0.00
1.21
0.00
0.00
4.07
0.00
1.58
0.00
loop7
0.68
0.00
0.70
0.00
0.00
0.00
0.00
0.00
0.69
0.00
0.00
1.03
0.00
0.05
0.00
sda
74.81
2.35
53938.75
142.55
4.59
10.91
5.78
82.30
7.01
34.69
0.69
721.03
60.77
0.26
2.04
loop8
0.01
0.00
0.03
0.00
0.00
0.00
0.00
0.00
0.20
0.00
0.00
3.06
0.00
0.21
0.00
loop9
0.01
0.00
0.02
0.00
0.00
0.00
0.00
0.00
0.09
0.00
0.00
1.69
0.00
0.13
0.00
|
iostat指标解读:
这些指标中,你要注意:
- %util ,就是我们前面提到的磁盘 I/O 使用率;
- r/s+ w/s ,就是 IOPS;
- rkB/s+wkB/s ,就是吞吐量;
- r_await+w_await ,就是响应时间。
在观测指标时,也别忘了结合请求的大小( rareq-sz 和 wareq-sz)一起分析。
4.2.进程 I/O 观测
上面提到的 iostat 只提供磁盘整体的 I/O 性能数据,缺点在于,并不能知道具体是哪些进程在进行磁盘读写。要观察进程的 I/O 情况,你还可以使用 pidstat 和 iotop 这两个工具。
$ pidstat -d
1
13
:
39
:
51
UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
13
:
39
:
52
102
916
0.00
4.00
0.00
0
rsyslogd
|
从 pidstat 的输出你能看到,它可以实时查看每个进程的 I/O 情况,包括下面这些内容。
- 用户 ID(UID)和进程 ID(PID) 。
- 每秒读取的数据大小(kB_rd/s) ,单位是 KB。
- 每秒发出的写请求数据大小(kB_wr/s) ,单位是 KB。
- 每秒取消的写请求数据大小(kB_ccwr/s) ,单位是 KB。
- 块 I/O 延迟(iodelay),包括等待同步块 I/O 和换入块 I/O 结束的时间,单位是时钟周期。
除了可以用 pidstat 实时查看,根据 I/O 大小对进程排序,也是性能分析中一个常用的方法。这一点,我推荐另一个工具, iotop。它是一个类似于 top 的工具,你可以按照 I/O 大小对进程排序,然后找到 I/O 较大的那些进程。
iotop 的输出如下所示:
从这个输出,你可以看到,前两行分别表示,进程的磁盘读写大小总数和磁盘真实的读写大小总数。因为缓存、缓冲区、I/O 合并等因素的影响,它们可能并不相等。