前两天,同事找我看一个问题,发现一台服务器上的磁盘空间快占满了,却找不到占用磁盘的文件。最后定位发现是服务器上有两个进程A和B,进程A写文件,进程B定时删除文件,但是进程B删除文件后,删除的文件实际上还在占用磁盘空间,这是为什么呢?
下面我在服务器上实际操作演示一下这个问题发生的过程。
1 问题复现
1)先使用df命令查看一下磁盘根目录的空间占用情况,当前占用55G:
2)使用dd命令在根目录下创建一个10G的大文件test.bin,并再次查看根目录的空间占用情况,发现根目录空间占用已经从55G变为了65G:
dd if=/dev/zero of=/test.bin bs=1000MB count=10
3)使用tail命令模拟进程A正在使用test.bin文件:
tail -f /test.bin
4)删除test.bin文件(模拟进程B删除文件),并查看磁盘空间占用情况,发现根目录的空间占用还是65G,并没有减少:
rm -rf /test.bin
2 问题定位及解决
2.1 问题定位
其实上面我们是已经知道了有tail命令在访问文件,所以导致删除的文件还在占用磁盘空间。但是如果是因为别的程序或者其它进程导致这个问题,我们该如何定位呢?
这里我们要借助lsof这个工具来帮助我们定位问题,一般Linux系统没有自带lsof工具,Ubuntu上可以用apt安装一下:
apt install lsof
man lsof看一下lsof命令的介绍,是列出所有打开的文件(list open files的缩写)
使用lsof查询一下刚刚删除的test.bin文件的情况:
lsof | grep deleted
看来确实是被tail这个ID为292709的进程占用了,使用ps命令看一下tail进程的详细信息:
看下进程ID为292709进程的描述符信息(Linux系统上进程打开一个文件时,都会为其分配一个描述符fd,通过/proc/进程ID/fd可以看到进程所有的打开文件信息)
看来确实是tail这个进程占用了test.bin,导致没能真正删除。
2.2 问题解决
解决这个问题有两种办法:
1)由于是tail进程占用了文件,所以直接kill掉tail进程即可,但是这里有一个问题,如果是生产环境,肯定不能随随便便就杀掉一个业务进程,因为这样可能会影响客户的业务使用。
2)直接清空文件
echo "" > /proc/292709/fd/3
再次查看一下根目录的空间占用,发现磁盘空间已经被释放了:
虽然这里通过清空文件暂时解决了磁盘未释放的问题,但是从根本上我们最好还是要避免这种问题的出现,比如协调好多进程间对于文件操作的时序问题,比如等到进程A结束了对文件的访问,进程B再去删除文件。
3 原因分析
下面我们从操作系统层面简单分析一下出现这种现象的原因:
在 Linux 文件系统中,一个文件由目录项、索引节点和数据块三部分组成。
- 目录项,也就是 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。
- 索引节点,也就是 inode,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间。
- 数据块,包含文件的具体内容,存储在硬盘上,硬盘的最小存储单位是扇区(Sector),然而扇区只有 512字节大小,如果每次都读写这么小的单位效率一定很低,所以当操作系统从硬盘读取数据的时候,不会一个个扇区的地去读取,而是一次性连续读取多个扇区,即一次性读取一个”块“(block) 由多个 sector 组成的 block ,是文件存取的最小单位 ,block 常见大小的是 4KB,即连续 8 个 sector 组成一个 block。
既然文件数据都存储在 block 中,那么我们指定读取某个文件数据的时候,操作系统就需要知道这个文件存储在哪个 block 上 ,而文件的数据存放位置信息被存放到了 inode 上,所以我们先要找到这个文件对应的inode号。使用stat命令可以查看inode信息:
以我们访问上面的test.bin文件为例,会经过以下步骤:
1)系统在目录项中根据文件名找到对应的 inode 号
2)通过 inode 号获取到 inode 信息
3)根据 inode 信息找到文件数据所在的 block ,读出数据
inode 跟数据块一样会消耗硬盘空间,所以硬盘格式化的时候,系统自动将硬盘分成2个区域:
- 一个是数据块,存放文件数据
- 另一个是 inode 区,存放 inode 所包含的信息,即文件元信息
了解了文件的存储结构之后,我们来看下对文件执 rm 行删除操作之后,这两个部分会发生什么。
文件被删除的时候,文件对应的 inode 就被删除掉了,而文件的数据部分在 inode 被清除掉之后,就会被覆盖并写入新的内容。但是如果文件在删除的时候是被打开的状态,比如有一个进程正在使用该文件,文件被进程锁定或者有进程一直在向这个文件写数据等,那么进程依旧可以读取该文件,系统就会认为该文件并未删除,导致磁盘空间一直被占用。