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
尊贵的董事大人
英文标题不为空时 视为本栏投稿
需要关键字 描述 英文标题