问题发现

最近在测试遇到一个问题,容器日志过大导致系统磁盘爆满,造成的影响就是该服务器的一些服务挂掉了。日志过大,解决方法就是删啊,直接定位到容器日志位置发现高达5G,三下五除二就rm了,但是仍然显示磁盘空间已满,但是du -sh命令也显示文件夹下为空。经过同事百度得知,容器日志不能直接rm,要通过cat /dev/null > {log文件}方式将日志删除。因为该文件被进程所引用,直接删除并不能擦除磁盘上的文件block信息,解决方式就是停止进程。今天就来复现一下并探究一下底层原理。

复现方式

由于容器也属于一种进程,引用文件的方式并没有不同于其他普通进程,所以就使用一个简单的demo复现。

首先进入一个目录,以/tmp/testfile目录为例,可以看到我的服务器还有2G的剩余空间

[centos@guozhao testfile]$ cd /tmp/testfile
[centos@guozhao testfile]$ df -h .
文件系统        容量  已用  可用 已用% 挂载点
/dev/vda1        50G   49G  2.0G   97% /

然后生成一个随机文件,再来查看剩余空间,看到还剩余1014M空间

[centos@guozhao testfile]$ dd if=/dev/urandom of=/tmp/testfile/delfiletest bs=1M count=1000
记录了1000+0 的读入
记录了1000+0 的写出
1048576000字节(1.0 GB)已复制,8.31491 秒,126 MB/秒
[centos@guozhao testfile]$ 
[centos@guozhao testfile]$ df -h .
文件系统        容量  已用  可用 已用% 挂载点
/dev/vda1        50G   49G 1014M   99% /

启动一个程序,引用该文件,不退出进程

func main() {
    file, err := os.Open("/tmp/testfile/delfiletest")
    defer file.Close()
    if err != nil{
        fmt.Println("open err :",err.Error())
        return
    }
    time.Sleep(100*time.Minute)
} 

接下来删除该文件文件,查看磁盘占用情况,看到虽然文件删除,但是磁盘空间并没有释放。

[centos@guozhao testfile]$ rm -rf delfiletest
[centos@guozhao testfile]$ df -h .
文件系统        容量  已用  可用 已用% 挂载点
/dev/vda1        50G   49G 1015M   99% /

解决方法,找到引用该文件的进程,并停止该进程,可以Ctrl+C停止,也可以kill命令停止。

[centos@guozhao testfile]$ lsof|grep deleted|grep delfile
lsof: WARNING: can't stat() proc file system /run/docker/netns/default
      Output information may be incomplete.
......
testdelet 29604 29608  centos    3r      REG  253,1 1048576000 58925156 /tmp/testfile/delfiletest (deleted)
......
[centos@guozhao testfile]$ kill 29604

停止后再次查看磁盘空间,发现磁盘已经释放。

[centos@guozhao-170 testfile]$ df -h .
文件系统        容量  已用  可用 已用% 挂载点
/dev/vda1        50G   49G  2.0G   97% /

重做一次上面的步骤,这次我们使用正确的方式释放磁盘的空间,可以看到磁盘空间腾出来了。

[centos@guozhao-170 testfile]$ cat /dev/null > /tmp/testfile/delfiletest 
[centos@guozhao-170 testfile]$ 
[centos@guozhao-170 testfile]$ df -h .
文件系统        容量  已用  可用 已用% 挂载点
/dev/vda1        50G   49G  2.0G   97% /

原因探究

继续深挖一下造成这种结果的原因。

在Linux上,每个文件都有一个自己对应的索引节点即inode,在这个inode里记录了文件在磁盘的块信息,以及链接数量等信息,一个文件在是否要被真正删除释放空间,取决于两个值,一个是i_count,代表引用计数;一个是i_nlink,代表硬链接数量,只有当两个都为0,文件才会真正释放。

struct inode{
    atomic_t i_count;
    unsignet int i_nlink;
    ......
};

当有进程使用该文件时候,i_count就会加1,当进程不在引用或进程结束,就会减一。

硬链接也是如此,当为文件创建一个硬链接时,i_nlink就会加一,删除就会减一,当减少为0时候,就会删除文件,释放空间。

在Linux中,硬链接指的是文件名与inode的链接,通常创建一个文件对应一个硬链接,我们可以手动通过ln命令或者程序触发link系统调用为一个文件创建一个硬链接,相当于两个文件名对应了同一个磁盘文件,两个都删除才会删除磁盘文件(没有进程引用的前提下)。而Linux的rm命令相当于执行了unlink系统调用,会使得i_nlink数量减一。

当然,由于文件并没有被真正删除,所以该文件是可以恢复的,只需要找到进程的pid,并进入/proc/{pid}/fd中,找到对应的文件描述符,执行cp命令复制即可找回文件。


郭朝
24 声望7 粉丝