在上一篇文章《Go配置文件热加载 - 发送系统信号》中给大家介绍了在Go语言中 利用发送系统信号更新配置文件 其核心思想就是:新起一个协程,监听linux
的用户自定义信号 USR1
, 当收到该信号类型时,主动更新当前配置文件。
那么接下来,我们将继续完成上一篇文章提到的第二种实现配置文件热更新方式:利用linux
提供的inotify
接口实现配置文件自动更新。
1. 关于inotify
首先在我们实操之前,让我们先来了解下什么是 inotify
。
在 Linux
内核 2.6.13
(June 18, 2005)版本之后,Linux
内核新增了一批文件系统的扩展接口(API),其中之一就是inotify
,inotify
提供了一种基于 inode
的监控文件系统事件的机制,可以监控文件系统的变化如文件修改、新增、删除等,并可以将相应的事件通知给应用程序。
inotify
既可以监控文件,也可以监控目录。当监控目录时,它可以同时监控目录本身以及目录中的各文件的变化。此外,inotify
使用文件描述符作为接口,因而可以使用通常的文件I/O操作 select
、poll
和 epoll
来监视文件系统的变化。
总之,简单来说就是:inotify
为我们从系统层面提供了一种可以监控文件变化的接口,我们可以利用它来监控文件或目录的变化。
inotify常用监控事件
inotify
提供常用的监控事件如下:
IN_ACCESS
文件被访问时触发事件,例如一个文件正在被read时。
IN_ATTRIB
文件属性(Metadata)发送变化触发的事件,例如文件权限发生变化(使用 chmod
修改),文件所属用户发生变化(使用chown
修改),文件时间戳发生变化等。
IN_CLOSE_WRITE
当一个文件写入操作结束文件被关闭时触发。
IN_CLOSE_NOWRITE
当一个文件或目录被打开没有任何写操作,当被关闭时触发。
IN_CREATE
当一个文件或目录被创建时触发。
IN_DELETE
文件或目录被删除时触发。
IN_DELETE_SELF
监控文件或目录本身被删除时触发,而且,如果一个文件或目录被移到其它地方,比如使用mv
命令,也会触发该事件,因为 mv
命令本质上是拷贝一份当前文件,然后删除当前文件的操作。
IN_MODIFY
文件被修改时触发,例如:有写操作( write
) 或者文件内容被清空(truncate
)操作。不过需要注意的是,IN_MODIFY
可能会连续触发多次。
IN_MOVE_SELF
所监控的文件或目录本身发生移动时触发。
IN_MOVED_FROM
文件或目录移除所监控目录。
IN_MOVED_TO
文件或目录移入所监控目录。
IN_ALL_EVENTS
监控所有事件。
IN_OPEN
文件被打开事件。
IN_CLOSE
文件被关闭事件,包括 IN_CLOSE_WRITE
和 IN_CLOSE_NOWRITE
的事件总和。
IN_MOVE
涉及所有的移动事件,包括 IN_MOVED_FROM
和 IN_MOVED_TO
。
如上便是inotify
提供给我们的常用监听事件,我们可以在自己的项目中监听如上的一个或多个事件来实现特定的需求,如果想查阅更多事件细节,请参考此处:inotify doc
但需要说明的是,inotify
并非是跨平台的,所以在macOS
或 windows
下则无法使用,但在macOS
也提供类似的实现:FSEvents ,以及 windows
下的 FindFirstChangeNotificationA,这里我们不再展开跨平台实现讨论,读者要是有兴趣可以查阅相关资料,或者使用文末推荐的开源库。
2. 代码实现
接下来,我们开始实现项目的配置文件更新监控功能(实操)。在GO
语言中,我们使用 golang.org/x/sys/unix 这个包来调用底层操作系统的一些封装功能,inotify
相关接口也包含在此包中,使用时只需要导入此包即可:
import "golang.org/x/sys/unix"
最简单的使用inotify
大致分为三个步骤:
inotify
初始化。- 添加文件监听,设置需要监听的一个事件或多个事件。
- 获取监听到的事件。
我们将按照这三个步骤来实现一个简单的 GO版
配置文件监控脚本 demo
,此处我们还是继续沿用 上一篇文章 的配置文件,当该文件发生变化时,我们需要通知Go
代码重新读取该文件内容,从而实现热更新的目的。
/tmp/env.json
2.1 初始化inotify
按照之前所说的步骤,第一步需要初始化inotify,初始化需要使用:InotifyInit()
函数,该函数会返回一个文件句柄和错误信息,之后的操作都是基于该文件句柄:
fd, err := unix.InotifyInit()
if err != nil {
log.Fatal(err)
}
2.2 添加文件监听
完成inotify
初始化后,接着我们需要添加我们需要监控的文件和以及想要监听的一个或多个事件,由于是项目的配置,此处的使用场景是:配置文件一般不会有删除的需求,而通常的操作是部署更新,因此此处我们选择的监听事件是:
IN_CLOSE_WRITE
如第一小节中提到的,当所监听的配置文件以写的方式被打开后,当此文件关闭时触发的事件,在这种情况下就可能发生文件的更新,正好此种场景正是我们想要的。
path := "/tmp/env.json"
watched, err := unix.InotifyAddWatch(fd, path, syscall.IN_CLOSE_WRITE)
if err != nil {
_ = unix.Close(fd)
log.Fatal(err)
}
如上代码中,文件监听使用了 InotifyAddWatch()
函数,第一个参数 fd
为第一步中的初始化文件句柄,第二个参数:path
为需要监听文件的路径,第三个参数为需要监听的事件。如果监听失败,较友好的方式是我们需要关闭当前的文件句柄。
2.3 获取监听事件
有了前两个步骤的准备,那么接下来我们只需要读取获取到监听事件即可:
events := make(chan uint32)
go func() {
var buf [unix.SizeofInotifyEvent * 4096]byte
for {
n, err := unix.Read(fd, buf[:])
if err != nil {
n = 0
continue
}
var offset uint32
for offset <= uint32(n - unix.SizeofInotifyEvent) {
raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask := uint32(raw.Mask)
nameLen := uint32(raw.Len)
events <- mask
offset += unix.SizeofInotifyEvent + nameLen
}
}
}()
在这里,我们新起了一个goroutine
, 因为接收事件通知是一个循环往复的过程。然后我们把文件句柄中的事件使用 unix.Read()
函数读取到一个 buffer
中,如果unix.Read()
读取不到任何事件,那么它就会处于阻塞状态。然后,我们循环遍历的方式,获取到 buffer
中的所接受到的所有事件通知,然后上报到 events
通道中。
那么现在一旦有新的监控事件通知,那么就会立即达到 events
通道中,接着我们需要做的便是从 events
通道中获取通知事件即可:
for {
select {
case event := <-events:
if event & syscall.IN_CLOSE_WRITE == syscall.IN_CLOSE_WRITE {
// 调用加载配置文件函数
loadConfig(path)
}
}
}
最终,整个代码大致如下:
func main() {
path := "/tmp/env.json"
// 初始化inotify文件监控
fd, err := unix.InotifyInit()
if err != nil {
log.Fatal(err)
}
watched, err := unix.InotifyAddWatch(fd, path, syscall.IN_CLOSE_WRITE)
if err != nil {
_ = unix.Close(fd)
log.Fatal(err)
}
defer func() {
_ = unix.Close(fd)
_ = unix.Close(watched)
}()
events := make(chan uint32)
go func() {
var (
buf [unix.SizeofInotifyEvent * 4096]byte
n int
)
for {
n, err = unix.Read(fd, buf[:])
if err != nil {
n = 0
fmt.Println(err)
continue
}
var offset uint32
for offset <= uint32(n - unix.SizeofInotifyEvent) {
raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask := uint32(raw.Mask)
nameLen := uint32(raw.Len)
// 塞到事件队列
events <- mask
offset += unix.SizeofInotifyEvent + nameLen
}
}
}()
// 获取监听事件
for {
select {
case event := <-events:
if event & syscall.IN_CLOSE_WRITE == syscall.IN_CLOSE_WRITE {
// 接收到事件,加载配置文件
loadConfig(path)
}
}
}
}
如上的代码,我们其实就完成了一个简单的配置文件监控的代码思路,但整体代码的质量是纯面条式的,因此有必要封装一下,在这里我打算把它们封装成一个 Watcher 类(其实Go语言没有类的概念,实质就是一个struct),代码内容请参考链接地址,这里不再此处展开,因为编程思想又是另一个话题,有了这个struct
之后,我们只需要直接使用即可:
func main() {
path := "/tmp/env.json"
notify, err := watcher.NewWatcher()
if err != nil {
log.Fatal(err)
}
err = notify.AddWatcher(path, syscall.IN_CLOSE_WRITE)
if err != nil {
log.Fatal(err)
}
done := make(chan bool, 1)
go func() {
for {
select {
case event := <-notify.Events:
if event & syscall.IN_CLOSE_WRITE == syscall.IN_CLOSE_WRITE {
fmt.Printf(" file changed \n")
// 加载配置文件函数, 配置文件代码参考上一篇文章内容
// loadConfig(path)
}
}
}
}()
<- done
}
总结
至此,我们完成了一个基于inofity
的配置文件热更新全部代码,在Go
中来实现还算比较简单,接下来我们需要总结一下:
-
inotify
是Linux
是内核系统提供的监控系统,使用它做热更新,其实和语言无关,所以你可以熟悉的语言来开发。 -
inotify
需要内核版本为2.6.13
以上,不支持macOS
和Windows
系统,如果希望实现跨平台文件监控那么可以使用如下第三点的fsnotify
库。 -
如果不想重复早轮子,那么我们可以站在巨人的肩上,推荐两个文件监听库:
-
https://github.com/goinbox/inotify (360大拿阿钢出品)
-
https://github.com/fsnotify/fsnotify (跨平台文件监控)
-
(360技术原创内容,转载请务必保留文末二维码,谢谢~)