基於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技術」微信公衆號

相關文章
相關標籤/搜索