基于inotify实现配置文件热更新

在上一篇文章《Go配置文件热加载 - 发送系统信号》中给你们介绍了在Go语言中 利用发送系统信号更新配置文件 其核心思想就是:新起一个协程,监听linux 的用户自定义信号 USR1 , 当收到该信号类型时,主动更新当前配置文件。html

那么接下来,咱们将继续完成上一篇文章提到的第二种实现配置文件热更新方式:利用linux提供的inotify 接口实现配置文件自动更新。node

1. 关于inotify

首先在咱们实操以前,让咱们先来了解下什么是 inotifylinux

Linux 内核 2.6.13 (June 18, 2005)版本以后,Linux 内核新增了一批文件系统的扩展接口(API),其中之一就是inotifyinotify 提供了一种基于 inode 的监控文件系统事件的机制,能够监控文件系统的变化如文件修改、新增、删除等,并能够将相应的事件通知给应用程序。git

inotify 既能够监控文件,也能够监控目录。当监控目录时,它能够同时监控目录自己以及目录中的各文件的变化。此外,inotify 使用文件描述符做为接口,于是可使用一般的文件I/O操做 selectpollepoll 来监视文件系统的变化。github

总之,简单来讲就是:inotify 为咱们从系统层面提供了一种能够监控文件变化的接口,咱们能够利用它来监控文件或目录的变化。golang

inotify经常使用监控事件

inotify 提供经常使用的监控事件以下:编程

IN_ACCESSjson

文件被访问时触发事件,例如一个文件正在被read时。windows

IN_ATTRIBapi

文件属性(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_WRITEIN_CLOSE_NOWRITE 的事件总和。

IN_MOVE

涉及全部的移动事件,包括 IN_MOVED_FROMIN_MOVED_TO

如上即是inotify提供给咱们的经常使用监听事件,咱们能够在本身的项目中监听如上的一个或多个事件来实现特定的需求,若是想查阅更多事件细节,请参考此处:inotify doc

但须要说明的是,inotify 并不是是跨平台的,因此在macOSwindows下则没法使用,但在macOS也提供相似的实现:FSEvents ,以及 windows下的 FindFirstChangeNotificationA,这里咱们再也不展开跨平台实现讨论,读者要是有兴趣能够查阅相关资料,或者使用文末推荐的开源库。

2. 代码实现

接下来,咱们开始实现项目的配置文件更新监控功能(实操)。在GO语言中,咱们使用 golang.org/x/sys/unix 这个包来调用底层操做系统的一些封装功能,inotify相关接口也包含在此包中,使用时只须要导入此包便可:

import "golang.org/x/sys/unix"

最简单的使用inotify大体分为三个步骤:

  1. inotify初始化。
  2. 添加文件监听,设置须要监听的一个事件或多个事件。
  3. 获取监听到的事件。

咱们将按照这三个步骤来实现一个简单的 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中来实现还算比较简单,接下来咱们须要总结一下:

  1. inotifyLinux 是内核系统提供的监控系统,使用它作热更新,其实和语言无关,因此你能够熟悉的语言来开发。
  2. inotify须要内核版本为2.6.13以上,不支持macOSWindows系统,若是但愿实现跨平台文件监控那么可使用以下第三点的 fsnotify 库。
  3. 若是不想重复早轮子,那么咱们能够站在巨人的肩上,推荐两个文件监听库:

(360技术原创内容,转载请务必保留文末二维码,谢谢~)

关于360技术

360技术是360技术团队打造的技术分享公众号,天天推送技术干货内容

更多技术信息欢迎关注“360技术”微信公众号

相关文章
相关标签/搜索