Go 配置文件热加载


配置文件 热更新 是服务器程序的基本功能 不停机调整程序的配置 在生产环境 提供极大的便利
比如 动态调高日志等级 业务逻辑参数变化 甚至某个功能模块的开关等 动态调整
配置文件 保存项目 基本元数据
配置文件的类型有很多 JSON xml yaml  某些场景下需 热更新配置文件内容 不能停机

配置文件发生变化 如何让当前程序重新读取 配置文件内容

手动发系统信号
用 inotify 监听文件修改事件
用Go语言 goroutine 概念 用 goroutine 新起一个协程  新协程接收系统信号(signal) 或 监听修改文件的事件

手动式 使用系统信号
文件的更新 需要手动告知当前依赖的运行程序 "嘿 哥们!配置文件更新啦 你得重新读一下配置内容!"
告知的方式 是向当前运行程序发送一个系统信号

自动式
Go主进程 新起一个goroutine 用来接收信号
新goroutine监听信号的发生 然后更新配置
在 *nix 系统中规定 USR1和USR2均属于用户自定义信号 至于USR1 和 USR2 哪一个更合 没有给出权威的答案 这里 按约定俗称的规矩 使用USR1
Nginx或者Apache等Web Server 采用发送信号 更新配置文件的策略

监听信号
import "os/signal"
Notify(c chan<- os.Signal, sig ...os.Signal)//监听系统信号 用signal包 Notify()方法  至少两个参数
第一个参数 是系统信号类型的通道 后续参数为 需要监听的系统信号
package main
import (
"os"
"os/signal" //
"syscall"
)
func main() {// 声明一个容量为1的信号通道
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGUSR1) // 监听系统SIGUSR1发出的信号
}
创建了一个信号容量大小为1的通道 channel 这表示 通道里最多能容纳下1个信号单元
如果当前通道里已存在一个信号单元时 又接收到另一个信号需要发送到通道中 那么在发送该信号的时候程序会被阻塞 直到通道里的信号被处理掉
达到 一次处理一个信号 多个信号需要排队的目的

信号的处理
系统信号被监听 存入通道后 sig  接下来 需要处理接收到到信号  新起的协程 goroutine 使用协程的目的是希望后续的任务不阻塞主进程的运行
在 GO 语言中 另起一个协程是非常方便的 只需要调用关键字:go 即可:
希望在新协程中永不停歇的获取通道中的系统信号
go func() {
 for {
  select {
   case <-sig: // 获取通道中的信号 处理信号            
  }
 }
}()
GO语言 select 语句 只能被用来处理 goroutine通讯操作
而goroutine 通讯又是基于 channel 来实现的 所以 select 只能用来处理通道(channel) 操作
当前 select一直会处于阻塞状态 直到它的某个case符合条件时才会执行该case条件下的语句
且此处 使用了for循环结构 让select语句处于一个无限循环当中
如果select 下的case接收到一个处理的信号后 当处理结束后 由于外层for循环的语句的作用 相当于重置了select的状态 在没有接收到新的信号时 select 将再次被阻塞等待 循环往复
for {
 select {
  case <-sig: / 获取通道中的信号 处理信号            
 }
 fmt.Println("select block test!")
}
这行fmt.Println() 函数会在for循环中立即运行吗 不会! select 会阻塞调 当程序运行起来时不会有任何输出 直到case匹配

热加载配置
加载配置文件 配置文件 存放于/tmp/env.json 内容比较简单  {"test": "D"}
创建解析该json格式配套的数据结构
type configBean struct {
Test string
}

configBean 结构体 用来和 env.json 配置文件 字段一一映射  只要调用json.Unmarshal()函数  就可以把这份json文件内容 转为对应的Go语言结构体内容
还需要声明一个变量  存储这份结构体数据 供程序在其他地方调用
// 全局配置变量
var Config config
type config struct {
LastModify time.Time
Data       configBean
}

此处 没有直接把 configBean 解析的json数据赋值给全局变量 而是又包装了一层 额外声明了一个字段 LastModify 用来存储 当前文件的最后一次修改时间
好处在于 每收到一个需要更新配置文件的信号时  还需要比对当前文件的修改是否大于上一次的更新时间 当然这仅仅是一个配置优化加载的小技巧

新增了一个 loadConfig(path string) 函数 用于封装 加载配置文件的所有逻辑
// 全局配置变量
var Config *config
type configBean struct {
Test string
}
type config struct {
LastModify time.Time
Data       configBean // 配置内容存储字段
}

func loadConfig(path string) error {
  var locker = new(sync.RWMutex)// 读取配置文件内容
  data, err := ioutil.ReadFile(path)
  if err != nil {
  return err
  }
  // 读取文件属性
  fileInfo, err := os.Stat(path)
  if err != nil {
  return err
  }
  // 验证文件的修改时间
  if Config != nil && fileInfo.ModTime().Before(Config.LastModify) {
  return errors.New("no need update")
  }

  // 解析文件内容
  var configBean configBean
  err = json.Unmarshal(data, &configBean)
  if err != nil {
  return err
  }

  config := config{
  LastModify: fileInfo.ModTime(),
  Data:       configBean,
  }

  // 重新赋值更新配置文件
  locker.Lock()
  Config = config
  locker.Unlock()
  return nil
}

loadConfig()函数 虽然使用了锁 但是在文件读写并没使用锁 仅在赋值阶段使用 因为在这种场景下不存在 多个goroutine同时操作 同一个文件的需求 如果 所在的场景存在多个goroutine并发写操作 那么保险起见 建议你把文件的读写最好也加上锁机制 至此  完成了利用监听系统信号更新配置文件的所有所有逻辑  演示最终成果 演示之前 在main函数添加一点额外代码 模拟主进程成为一个常驻进程  使用通道 最后大致 代码
func main() {
configPath := "/tmp/env.json"
done := make(chan bool, 1)
sig  := make(chan os.Signal, 1) // 定义信号通道
signal.Notify(sig, syscall.SIGUSR1)
go func(path string) {
for {
  select {
  case <-sig: // 收到信号, 加载配置文件
    _ := loadConfig(path)
  }
}
}(configPath)

// 挂起进程 直到获取到一个信号
<-done
}
///// signal.go ///////
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
type configBean struct {
    Test string
}
type config struct {
    LastModify time.Time
    Data       configBean
}
var Config *config

func loadConfig(path string) error {
    var locker = new(sync.RWMutex)
    data, err := ioutil.ReadFile(path)
    if err != nil {
        return err
    }
    fileInfo, err := os.Stat(path)
    if err != nil {
        return err
    }

    if Config != nil && fileInfo.ModTime().Before(Config.LastModify) {
        return errors.New("no need update")
    }
    var configBean configBean
    err = json.Unmarshal(data, &configBean)
    if err != nil {
        return err
    }
    config := &config{
        LastModify: fileInfo.ModTime(),
        Data:       configBean,
    }
    locker.Lock()
    Config = config
    locker.Unlock()
    return nil
}

func main() {
    fmt.Println("start main process")
    configPath := "./tmp/env.json"
    done := make(chan bool, 1)
    _ = loadConfig(configPath)
    fmt.Printf("current config value is: %s \n", Config.Data.Test)
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGUSR1)
    go func(path string) {
        for {
            select {
            case <-sig:    // 收到信号, 加载配置文件
                err := loadConfig(path)
                if err != nil {
                    fmt.Println(err)
                }
                fmt.Println("received signal!")
                fmt.Printf("current config value is: %s \n", Config.Data.Test)
            }
        }
    }(configPath)

    // 挂起进程,直到获取到一个信号
    <-done
}
////////////////////
几个可用模块
viper       https://github.com/spf13/viper
go-config   https://github.com/micro/go-micro/tree/master/config
gozzo-config https://github.com/go-ozzo/ozzo-config
cconf       https://github.com/syyongx/cconf