警惕fsnotify文件监控时的资源占用

正确使用inode、inotify资源

Posted by JeeShao on July 4, 2021

我们在使用fsnotify监控Linux文件资源时,涉及到对系统inode及inotify资源的申请、占用和释放,如果在程序中不警惕使用,便可能会造成系统资源耗尽,导致程序崩溃。

fsnotify介绍

fsnotify是Golang中一个实现文件监控的开源库,项目地址 github.com/fsnotify/fsnotify,其对外提供的接口比较少,使用也非常简单。

package main

import (
	"log"

	"github.com/fsnotify/fsnotify"
)

func main() {
	//创建一个监控对象
	watch, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watch.Close()
	//添加要监控的对象,文件或文件夹
	err = watch.Add("/home/jee/GoPath/src/demo/aa")
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		for {
			select {
			case ev := <-watch.Events:
				{
					//事件类型:
					// Create 创建
					// Write 写入
					// Remove 删除
					// Rename 重命名
					// Chmod 修改权限
					if ev.Op&fsnotify.Create == fsnotify.Create {
						log.Println("创建文件 : ", ev.Name)
					}
					if ev.Op&fsnotify.Write == fsnotify.Write {
						log.Println("写入文件 : ", ev.Name)
					}
					if ev.Op&fsnotify.Remove == fsnotify.Remove {
						log.Println("删除文件 : ", ev.Name)
					}
					if ev.Op&fsnotify.Rename == fsnotify.Rename {
						log.Println("重命名文件 : ", ev.Name)
					}
					if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
						log.Println("修改权限 : ", ev.Name)
					}
				}
			case err := <-watch.Errors:
				{
					log.Println("error : ", err)
					return
				}
			}
		}
	}()

	//循环
	select {}
}

其中通过fsnotify.NewWatcher()创建一个监控实例,然后Add添加要监控的对象(文件或目录),可以多次Add添加多个对象同时监控,watch.Events为监控的事件,一共5种事件:Create、Write 、Remove、 Rename及Chmod,我们可以针对不同的监控事件进行相应的操作。

警惕inode、inotify资源耗尽

Linux中系统通过inode标识每个文件,而非文件名,文件名只是提供给用户区分文件使用,inode包含很多的文件元信息,但不包含文件名,例如:字节数、属主UserID、属组GroupID、读写执行权限、时间戳等。硬盘分区的inode总数在格式化后就已经固定,而每个文件必须有一个inode,因此inode资源存在耗尽的风险。

inotify是Linux下一种异步文件系统事件监控机制,允许监控程序打开一个独立文件描述符,并针对事件集监控一个或者多个文件,例如打开、关闭、移动/重命名、删除、创建或者改变属性。在系统中通过以下三个文件配置了系统inotify资源参数:

$ ll /proc/sys/fs/inotify
-rw-r--r-- 1 root root 0 Jul  2 00:16 max_queued_events
-rw-r--r-- 1 root root 0 Jul  1 23:16 max_user_instances
-rw-r--r-- 1 root root 0 Jul  2 00:13 max_user_watches
  • max_queued_evnets 表示调用inotify_init时分配给inotify instance中可排队的event最大值,超出该值的event将被丢弃并触发IN_Q_OVERFLOW事件。
  • max_user_instances 表示每一个real user ID可创建的inotify instatnces的数量上限。
  • max_user_watches 表示每个inotify instatnces可监控的最大文件数量(inode数量)。如果监控的文件数目巨大,需要适当增加此值的大小。 查看系统种该参数默认值:
    $ sysctl -a | grep inotify
    fs.inotify.max_queued_events = 16384
    fs.inotify.max_user_instances = 1024
    fs.inotify.max_user_watches = 8192
    

    修改参数值:

    sysctl -n -w fs.inotify.{参数}={}
    

1. fsnotify对inotify资源的使用

在每次fsnotify.NewWatcher()创建监控实例都会申请一个inotify资源,并在watch.Close()关闭监控实例时释放inotify资源,即参数max_user_instances定义的资源。若对监控实例资源占用超过该参数,程序会返回如下error信息:

$ go run fsnotify.go 
2021/07/04 22:30:09 too many open files
exit status 1

为了防止程序未及时释放inotify资源导致资源耗尽,切记要对fsnotify.NewWatcher()创建的监控实例执行Close()释放inotify资源

2.fsnotify对inode资源的使用

在每次watch.Add()添加监控对象时申请使用inode资源所以如果在程序中没有Remove监控对象或者忘记Close监控实例,会持续占用甚至增长式占用inode资源,即参数max_user_watches定义的资源。若inode资源占用超过该参数,程序会返回如下error信息:

$ go run fsnotify.go 
2021/07/04 22:37:46 no space left on device
exit status 1

fsnotify中通过map结构的watches存放监控对象,其中key为被监控文件的路径 image-20210704231330592 系统实际是通过inode标识被监控文件,所以当文件被重命名后,依然是同一个inode资源,并依然可以监控其事件信息,但是因为watches中key不会改变,所以重命名后事件名称(event.Name)依然是重命名前的文件名。 image-20210704232826411

如果我们需要在文件被重命名时重新创建文件并重新监控新创建的文件,这里需要注意,此时一定要显式Remove释放被重命名后的监控对象,否则该文件将会一直被监控并占用inode资源。

if ev.Op&fsnotify.Rename == fsnotify.Rename {
	log.Println("重命名文件 : ", ev.Name)
	if err := watch.Remove(ev.Name); err != nil {
		log.Fatal(err)
	}
	if _, err := os.Create(file); err != nil {
		log.Fatal(err)
	}
	if err := watch.Add(file); err != nil {
		log.Fatal(err)
	}
}

而在监控到文件被删除时,fsnotify便会从watches中自动删除该监控对象,同时资源也被释放。

总结

在使用fsnotify时,程序会申请占用系统inode及inotify资源,并且使用不当可能会造成资源耗尽。在使用fsnotify.NewWatcher()创建监控实例并使用结束后要Close()释放inotify资源;当不再需要监控某个文件资源时(如文件被Rename后),及时Remove()释放inode资源。

本文参考 Linux inode 详解 LINUX下INOTIFY的学习和使用

版权声明:博主文章,可以不经博主随意转载,引用本文时请标明来源。