Go配置文件热加载

Wesley13
• 阅读 463

在日常项目的开发中,我们经常会使用配置文件来保存项目的基本元数据,配置文件的类型有很多,如:JSONxmlyaml、甚至可能是个纯文本格式的文件。不管是什么类型的配置数据,在某些场景下,我们可会有热更新当前配置文件内容的需求,比如:使用Go运行的一个常驻进程,运行了一个 Web Server 服务进程。

此时,如果配置文件发生变化,我们如何让当前程序重新读取新的配置文件内容呢?接下来,我们将使用如下两种方式实现配置文件的更新:

  1. 使用系统信号(手动式)。
  2. 使用inotify, 监听文件修改事件。

不管是哪一种方式,都会用到Go语言中 goroutine 的概念,我打算使用 goroutine 新起一个协程,新协程的目的是用来接收系统信号(signal)或者监听文件被修改的事件,如果你对 goroutine 的概念不是很了解,那么建议你先查阅相关资料。

手动式,使用系统信号。

我之所以称这种方式为手动式(Manual),是因为文件的更新是需要我们自己去手动告知当前依赖的运行程序:"嘿,哥们!配置文件更新啦,你得重新读一下配置内容!!",我们告知的方式就是向当前运行程序发送一个系统信号,因此程序的大概思路如下:

  1. 在Go主进程中,新起一个goroutine,用来接收信号。
  2. 新goroutine监听信号的发生,然后更新配置文件。

*nix 系统中规定,USR1USR2均属于用户自定义信号,至于USR1USR2 哪一个更合,维基百科 也没有给出权威的答案,所以在这里我按约定俗称的规矩,打算使用USR1:

如果你使用过Nginx或者Apache等Web Server,那么你对采用发送信号更新配置文件的策略肯定多少有点印象。

监听信号

Go语言中监听系统信号需要使用 signalNotify()方法,该方法至少需要两个参数,第一个参数要求是一个系统信号类型的通道,后续参数为一个或多个需要监听的系统信号:

import "os/signal"

Notify(c chan<- os.Signal, sig ...os.Signal)

因此,我们的代码大致如下:

package main

import (
    "os"
    "os/signal"
    "syscall"
)

func main() {
  // 声明一个容量为1的信号通道
    sig := make(chan os.Signal, 1)
  // 监听系统SIGUSR1发出的信号
    signal.Notify(sig, syscall.SIGUSR1)
}

在这里我们创建了一个信号容量大小为1的通道(channel),这表示,通道里最多能容纳下1个信号单元,如果当前通道里已经存在一个信号单元,此时又接收到另一个信号需要发送到通道中,那么在发送该信号的时候程序会被阻塞,直到通道里的信号被处理掉。

通过这种方式,我们可以一次精确的只处理一个信号,多个信号都需要排队的目的,这正是我想要的效果。

信号的处理

当系统信号被监听存入通道后(sig中),接下来我们需要处理接收到到信号,这里我们新起的协程(goroutine),使用协程的目的是希望后续的任务不阻塞主进程的运行,在 GO 语言中,另起一个协程是非常方便的,只需要调用关键字:go 即可:

go func(){
  // 新线程
}()

我们希望在新协程中永不停歇的获取通道中的系统信号,代码如下:

go func() {
        for {
            select {
            case <-sig:
                // 获取通道中的信号,处理信号            
            }
        }
}()

GO语言中的select 语句,其结构有点类似于其他语言的switch语句,但不同的是,select 只能被用来处理 goroutine 的通讯操作,而goroutine的通讯又是基于channel来实现的,所以直白点说:select 只能用来处理通道(channel)的操作。

当前的select一直会处于阻塞状态,直到它的某个case符合条件时才会执行该case条件下的语句。并且此处我们使用了for循环结构,让select语句处于一个无限循环当中,如果select 下的case接收到一个处理的信号后,当处理结束后;由于外层for循环的语句的作用,相当于重置了select的状态,在没有接收到新的信号时,select将再次被阻塞等待,循环往复。

如果你对select语句的阻塞有疑问,我们不妨考虑下面代码的运行情况:

for {
  select {
    case <-sig:
    // 获取通道中的信号,处理信号            
  }
  fmt.Println("select block test!")
}

在如上的select语句后,我们尝试输出一行字符串,那么请问:"这行fmt.Println() 函数会在for循环中立即运行吗?"

答案是肯定的:不会!select 会阻塞调,当程序运行起来时不会有任何输出,直到case匹配到。你不妨试试。

热加载配置

我们已经准备好了信号的监听,以及信号处理的简单工作,接下来我们需要细化信号处理阶段的代码,需要添加上加载配置文件的逻辑,我们将演示加载一份简单的json配置文件,文件的路径存放于/tmp/env.json,内容比较简单,仅一个test字段:

{
    "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
}

最终我们使用一张gif图片来演示最终效果:

Go配置文件热加载

最终的完整版代码,请在此处查看:github代码地址,并且需要说明的是,demo中的代码还有些小细节,例如:错误的处理,信号通道的关闭等,请自行处理。

预告:鉴于文章篇幅考虑,本文中我们只实现了第一种文件更新方式。下一篇文章中,我们将使用第二种方式:使用inotify监听配置文件的变化,以实现配置文件的自动更新,期待你的关注。

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

Go配置文件热加载

关于360技术

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

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

点赞
收藏
评论区
推荐文章
Easter79 Easter79
2年前
springcloud的配置文件的读取顺序
SpringBoot默认支持properties和YAML两种格式的配置文件。前者格式简单,但是只支持键值对。如果需要表达列表,最好使用YAML格式。SpringBoot支持自动加载约定名称的配置文件,例如application.yml。如果是自定义名称的配置文件,就要另找方法了。可惜的是,不像前者有@PropertySource这样方便的加载方式,
Stella981 Stella981
2年前
PHP配置优化:php
PHPFPM是一个PHPFastCGI管理器,phpfpm.conf配置文件用于控制PHPFPM管理进程的相关参数,比如工作子进程的数量、运行权限、监听端口、慢请求等等。我们在编译安装PHP的时,在./configure的时候带–enablefpm参数即可开启PHPFPM。PHPFPM配置文件为phpfpm.conf,其语法类似p
Stella981 Stella981
2年前
OpenStack配置解析库oslo.config的使用方法
 OpenStack的oslo项目旨在独立出系统中可重用的基础功能,oslo.config就是其中一个被广泛使用的库,该项工作的主要目的就是解析OpenStack中命令行(CLI)或配置文件(.conf)中的配置信息。  在本文的语境下,有这么几个概念:  配置文件:      用来配置OpenStack各个服务的ini风格的配置文件,通
Wesley13 Wesley13
2年前
Spring整合activiti配置processEngine
配置xml数据时,可以直接在配置文件中填写,也可以采用properties配置文件的方式加载。采用配置文件的方式需要使用到${参数}的方式获取。引用配置文件的方式:<context:propertyplaceholderlocation"classpath:properties文件目录"/applic
Wesley13 Wesley13
2年前
virt
        当使用virtmanager命令直接去安装一个很小的镜像文件(cirros操作系统的),此时会发生一个错误,该virtmanager会无法启动这个镜像文件,原因在于virtmanager有它自己的默认的配置文件,而当我们直接从界面上去安装的过程中,使用的就是这个配置文件。作为一个新手,现在先不管这个配置文件是什么鬼,为了快速地搞定它,可
Stella981 Stella981
2年前
Spring Cloud Alibaba Nacos Config 的使用
一、需求主要实现nacos作为配置中心的一些使用方法。二、实现功能1、加载productproviderdev.yaml配置文件2、实现配置的自动刷新3、实现加载多个配置文件
Stella981 Stella981
2年前
Skynet 进程启动
Skynet进程启动初始化配置skynet进程启动时需要指定配置文件,启动后读取配置文件中的内容并存储在内存中。配置文件格式是kv且k必须是字符串而v必须是字符串或者luaboolean类型。通过L读取配置,随后把配置存储在skynet_env.c模块中
Stella981 Stella981
2年前
Hibernate 中Datetime类型属性数据库默认值
在有些时间,我们在设置Hibernate的配置文件时希望POCO类的一个属性使用数据库中的默认值,这种情况出现在应用服务器和数据服务器分开设置的系统中,或者是有多个反向代理的Cache服务器中,如何设置才能让Hibernate依照我们的要求工作呢?以下以MSSql为例说明一下:我们只需在配置文件中设置属性为如下格式就行了:<property
Stella981 Stella981
2年前
BitCoinCore配置文件解读
bitcoin.conf配置文件除了datadir和conf以外的所有命令行参数都可以通过一个配置文件来设置,而所有配置文件中的选项也都可以在命令行中设置。命令行参数设置的值会覆盖配置文件中的设置。配置文件是“设置值”格式的一个列表,每行一个。您还可以使用符号来编写注释。配置文件
京东云开发者 京东云开发者
10个月前
Python自动化测试的配置层实现方式对标与落地 | 京东云技术团队
Python中什么是配置文件,配置文件如何使用,有哪些支持的配置文件等内容,话不多说,让我们一起看看吧~