zoukankan      html  css  js  c++  java
  • 配置文件热加载的go语言实现

    通常我们更新应用程序的配置文件,都需要手动重启程序或手动重新加载配置。假设一组服务部署在10台机器上,你需要借助批量运维工具执行重启命令,而且10台同时重启可能还会造成服务短暂不可用。要是更新配置后,服务自动刷新配置多好...今天我们就用go实现配置文件热加载的小功能,以后更新配置再也不用手动重启了...

    1 基本思路

    通常应用程序启动的流程:加载配置,然后run()。我们怎么做到热加载呢?我们的思路是这样的:

    【1】在加载配置文件之后,启动一个线程

    【2】该线程定时监听这个配置文件是否有改动

    【3】如果配置文件有变动,就重新加载一下

    【4】重新加载之后通知需要使用这些配置的应用程序(进程或线程),实际上就是刷新内存中配置

    2 加载配置

    首先我们要实现加载配置功能。假设配置文件是k=v格式的,如下:

    那我们得写一个解析配置的包了...让我们一起面向对象:

    type Config struct{
    	filename string
    	data map[string]string
    	lastModifyTime int64
    	rwLock sync.RWMutex
    	notifyList []Notifyer
    }

    filename string 配置文件名称

    data map[string]string  将配置文件中的k/v解析存放到map中

    lastModifyTime int64   记录配置文件上一次更改时间

    rwLock sync.RWMutex   读写锁,处理这样一种竞争情况:更新这个结构体时其他线程正在读取改结构体中的内容,后续用到的时候会讲

    notifyList []Notifyer  存放所有观察者,此处我们用到了观察者模式,也就是需要用到这个配置的对象,我们就把它加到这个切片。当配置更新之后,通知切片中的对象配置更新了。

    接下来我们可以给这个结构体添加一些方法了:

    2.1 构造函数

    func NewConfig(file string)(conf *Config, err error){
    	conf = &Config{
    		filename: file,
    		data: make(map[string]string, 1024),
    	}
    
    	m, err := conf.parse()
    	if err != nil {
    		fmt.Printf("parse conf error:%v
    ", err)
    		return
    	}
    
    	// 将解析配置文件后的数据更新到结构体的map中,写锁
    	conf.rwLock.Lock()
    	conf.data = m
    	conf.rwLock.Unlock()
    
    	// 启一个后台线程去检测配置文件是否更改
    	go conf.reload()
    	return
    }

    构造函数做了三件事:【1】初始化Config 【2】调用parse()函数,解析配置文件,并把解析后的map更新到Config 【3】启动一个线程,准确说是启动一个goroutine,即reload() 

    注意此处更新map时加了写锁了,目的在于不影响拥有读锁的线程读取数据。

    2.2 parse()

    解析函数比较简单,主要是读取配置文件,一行行解析,数据存放在map中。

    func (c *Config) parse() (m map[string]string, err error) {
    	// 如果在parse()中定义一个map,这样就是一个新的map不用加锁
    	m = make(map[string]string, 1024)
    
    	f, err := os.Open(c.filename)
    	if err != nil {
    		return
    	}
    	defer f.Close()
    
    	reader := bufio.NewReader(f)
    	// 声明一个变量存放读取行数
    	var lineNo int
    	for {
    		line, errRet := reader.ReadString('
    ')
    		if errRet == io.EOF {
    			// 这里有一个坑,最后一行如果不是
    结尾会漏读
    			lineParse(&lineNo, &line, &m)
    			break
    		}
    		if errRet != nil {
    			err = errRet
    			return
    		}
    		
    		lineParse(&lineNo, &line, &m)
    	}
    
    	return 
    }
    
    func lineParse(lineNo *int, line *string, m *map[string]string) {
    		*lineNo++
    
    		l := strings.TrimSpace(*line)
    		// 如果空行 或者 是注释 跳过
    		if len(l) == 0 || l[0] =='
    ' || l[0]=='#' || l[0]==';' {
    			return
    		}
    
    		itemSlice := strings.Split(l, "=")
    		// =
    		if len(itemSlice) == 0 {
    			fmt.Printf("invalid config, line:%d", lineNo)
    			return
    		}
    
    		key := strings.TrimSpace(itemSlice[0])
    		if len(key) == 0 {
    			fmt.Printf("invalid config, line:%d", lineNo)
    			return
    		}
    		if len(key) == 1 {
    			(*m)[key] = ""
    			return
    		}
    
    		value := strings.TrimSpace(itemSlice[1])
    		(*m)[key] = value	
    
    		return
    }

    这里我写了两个函数。lineParse()是解析每一行配置的。parse()就是解析的主函数,在parse()中我调用了两次lineParse()。原因是在使用bufio按行读取配置文件的时候,有时候会出现这样的情况:有的人在写配置文件的时候,最后一行没有换行,也就是没有‘ ’,然后我们就直接读到io.EOF了,这时候如果直接break就会导致最后一行丢失。所以这种情况下我们应该在break之前调用lineParse()把最后一行处理了。

    3 封装接口

    上面我们已经实现了读取配置文件,并放到一个Config示例中,我们需要为这个Config封装一些接口,方便用户通过接口访问Config的内容。这步比较简单:

    func (c *Config) GetInt(key string)(value int, err error){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	str, ok := c.data[key]
    	if !ok {
    		err = fmt.Errorf("key [%s] not found", key)
    	}
    	value, err = strconv.Atoi(str)
    	return
    }
    
    func (c *Config) GetIntDefault(key string, defaultInt int)(value int){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	str, ok := c.data[key]
    	if !ok {
    		value = defaultInt
    		return
    	}
    	value, err := strconv.Atoi(str)
    	if err != nil {
    		value = defaultInt
    	}
    	return
    }
    
    func (c *Config) GetString(key string)(value string, err error){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	value, ok := c.data[key]
    	if !ok {
    		err = fmt.Errorf("key [%s] not found", key)
    	}
    	return
    }
    
    func (c *Config) GetIStringDefault(key string, defaultStr string)(value string){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	value, ok := c.data[key]
    	if !ok {
    		value = defaultStr
    		return
    	}
    	return
    }

    如上,一共封装了4个接口:

    GetInt(key string)(value int, err error)   通过key获取value,并将value转成int类型

    GetIntDefault(key string, defaultInt int)(value int)    通过key获取value,并将value转成int类型;如果获取失败,使用默认值

    GetString(key string)(value string, err error)    通过key获取value,默认value为string类型

    GetIStringDefault(key string, defaultStr string)(value string)   通过key获取value,默认value为string类型;如果获取失败,使用默认值

    注意:四个接口都用了读锁

    4 reload()

    上面我们已经实现了解析,加载配置文件,并为Config封装了比较友好的接口。接下来,我们可以仔细看一下我们之前启动的goroutine了,即reload()方法。

    func (c *Config) reload(){
    	// 定时器
    	ticker := time.NewTicker(time.Second * 5) 
    	for _ = range ticker.C {
    		// 打开文件
    		// 为什么使用匿名函数? 当匿名函数退出时可用defer去关闭文件
    		// 如果不用匿名函数,在循环中不好关闭文件,一不小心就内存泄露
    		func (){
    			f, err := os.Open(c.filename)
    			if err != nil {
    				fmt.Printf("open file error:%s
    ", err)
    				return
    			}
    			defer f.Close()
    
    			fileInfo, err := f.Stat()
    			if err != nil {
    				fmt.Printf("stat file error:%s
    ", err)
    				return
    			}
    			// 或取当前文件修改时间
    			curModifyTime := fileInfo.ModTime().Unix() 
    			if curModifyTime > c.lastModifyTime {
    				// 重新解析时,要考虑应用程序正在读取这个配置因此应该加锁
    				m, err := c.parse()
    				if err != nil {
    					fmt.Printf("parse config error:%v
    ", err)
    					return
    				}
    
    				c.rwLock.Lock()
    				c.data = m
    				c.rwLock.Unlock()
    
    				c.lastModifyTime = curModifyTime
    
    				// 配置更新通知所有观察者
    				for _, n := range c.notifyList {
    					n.Callback(c)
    				}
    			}
    		}()
    	}
    }

    reload()函数中做了这几件事:

    【1】用time.NewTicker每隔5秒去检查一下配置文件

    【2】如果配置文件的修改时间比上一次修改时间大,我们认为配置文件更新了。那么我们调用parse()解析配置文件,并更新conf实例中的数据。并且更新配置文件的修改时间。

    【3】通知所有观察者,即通知所有使用配置文件的程序、进程或实例,配置更新了。

    5 观察者模式

    我们反复提到观察者,反复提到通知所有观察者配置文件更新。那么我们就要实现这个观察者:

    type Notifyer  interface {
    	Callback(*Config)
    }  

    定义这样一个Notifyer接口,只要实现了Callback方法的对象,就都实现了这个Notifyer接口。实现了这个接口的对象,如果都需要被通知配置文件更新,那这些对象都可以加入到前面定义的notifyList []Notifyer这个切片中,等待被通知配置文件更新。

    好了,此处我们是否少写了添加观察者的方法呢??是的,马上写:

    // 添加观察者
    func (c *Config) AddObserver(n Notifyer) {
    	c.notifyList = append(c.notifyList, n)
    }
    

    6 测试

    经过上面一番折腾,咱们的热加载就快实现了,我们来测一测:

    通常我们在应用程序中怎么使用配置文件?【1】加载配置文件,加载之后数据放在一个全局结构体中 【2】run()

    也就是run()中我们要使用全局的结构体,但是这个全局结构体会因为配置文件的更改被更新。此时又存在需要加锁的情况了。我擦,是不是很麻烦。。不用担心,我们用一个原子操作搞定。

    假设我们的配置文件中存放的是hostname/port/kafkaAddr/kafkaPort这几个字段。。

    type AppConfig struct {
    	hostname string
    	port int
    	kafkaAddr string
    	kafkaPort int
    }  

    接下来我们要用原子操作保证数据一致性了:

    // reload()协程写 和 for循环的读,都是对Appconfig对象,因此有读写冲突
    type AppConfigMgr struct {
    	config atomic.Value
    }
    
    // 初始化结构体
    var appConfigMgr = &AppConfigMgr{}  

    atomic.Value能保证存放数据和读取出数据不会有冲突。所以当我们更新数据时存放到atomic.Value中,我们使用数据从atomic.Value加载出来,这样不用加锁就能保证数据的一致性了。完美~~

    我们需要AppConfigMgr实现Callback方法,即实现Notifyer接口,这样才能被通知配置更新:

    func (a *AppConfigMgr)Callback(conf *reconf.Config) {
    	appConfig := &AppConfig{}
    	hostname, err := conf.GetString("hostname")
    	if err != nil {
    		fmt.Printf("get hostname err: %v
    ", err)
    		return
    	}
    	appConfig.hostname = hostname
    
    	kafkaPort, err := conf.GetInt("kafkaPort")
    	if err != nil {
    		fmt.Printf("get kafkaPort err: %v
    ", err)
    		return
    	}
    	appConfig.kafkaPort = kafkaPort
    
    	appConfigMgr.config.Store(appConfig)
    
    }

    这个Callback实现功能是:当被通知配置更新时,马上读取更新的数据并存放到config   atomic.Value 中。

    好了,我们要写主函数了。

    func initConfig(file string) {
    	// [1] 打开配置文件
    	conf, err := reconf.NewConfig(file)
    	if err != nil {
    		fmt.Printf("read config file err: %v
    ", err)
    		return
    	}
    
    	// 添加观察者
    	conf.AddObserver(appConfigMgr)
    
    	// [2]第一次读取配置文件
    	var appConfig AppConfig
    	appConfig.hostname, err = conf.GetString("hostname")
    	if err != nil {
    		fmt.Printf("get hostname err: %v
    ", err)
    		return
    	}
    	fmt.Println("Hostname:", appConfig.hostname)
    
    	appConfig.kafkaPort, err = conf.GetInt("kafkaPort")
    	if err != nil {
    		fmt.Printf("get kafkaPort err: %v
    ", err)
    		return
    	}
    	fmt.Println("kafkaPort:", appConfig.kafkaPort)
    
    	// [3] 把读取到的配置文件数据存储到atomic.Value
    	appConfigMgr.config.Store(&appConfig)
    	fmt.Println("first load sucess.")
    
    }
    
    func run(){
    	for {
    		appConfig := appConfigMgr.config.Load().(*AppConfig)
    
    		fmt.Println("Hostname:", appConfig.hostname)
    		fmt.Println("kafkaPort:", appConfig.kafkaPort)
    		fmt.Printf("%v
    ", "--------------------")
    		time.Sleep(5 * time.Second)
    	}
    }
    
    func main() { 
    	confFile := "../parseConfig/test.cfg"
    	initConfig(confFile)
    	// 应用程序 很多配置已经不是存在文件中而是etcd
    	run() 
    }

    主函数中调用了initConfig()和run()。

    initConfig()中:reconf.NewConfig(file)的时候我们已经第一次解析配置,并启动线程不断更新配置了。此外initConfig()还做了一些事,就是通过Config提供的接口,将配置文件中的数据读取到appConfig 中,然后再将appConfig 存储到 atomic.Value中。

    run()就是模拟应用程序在运行过程中使用配置的过程:run()中获取配置信息就是从 atomic.Value加载出来,这样保证数据一致性。

    编译运行,然后不断更改配置文件中kafkaAddr,测试结果如下:

    这样配置文集热加载就实现了。

    附一下所有代码:

    reconf/reconf.go:

    package reconf
    
    // 实现一个解析配置文件的包
    import (
    	"time"
    	"io"
    	"fmt"
    	"os"
    	"bufio"
    	"strings"
    	"strconv"
    	"sync"
    )
    
    type Config struct{
    	filename string
    	data map[string]string
    	lastModifyTime int64
    	rwLock sync.RWMutex
    	notifyList []Notifyer
    }
    
    func NewConfig(file string)(conf *Config, err error){
    	conf = &Config{
    		filename: file,
    		data: make(map[string]string, 1024),
    	}
    
    	m, err := conf.parse()
    	if err != nil {
    		fmt.Printf("parse conf error:%v
    ", err)
    		return
    	}
    
    	// 将解析配置文件后的数据更新到结构体的map中,写锁
    	conf.rwLock.Lock()
    	conf.data = m
    	conf.rwLock.Unlock()
    
    	// 启一个后台线程去检测配置文件是否更改
    	go conf.reload()
    	return
    }
    
    // 添加观察者
    func (c *Config) AddObserver(n Notifyer) {
    	c.notifyList = append(c.notifyList, n)
    }
    
    func (c *Config) reload(){
    	// 定时器
    	ticker := time.NewTicker(time.Second * 5) 
    	for _ = range ticker.C {
    		// 打开文件
    		// 为什么使用匿名函数? 当匿名函数退出时可用defer去关闭文件
    		// 如果不用匿名函数,在循环中不好关闭文件,一不小心就内存泄露
    		func (){
    			f, err := os.Open(c.filename)
    			if err != nil {
    				fmt.Printf("open file error:%s
    ", err)
    				return
    			}
    			defer f.Close()
    
    			fileInfo, err := f.Stat()
    			if err != nil {
    				fmt.Printf("stat file error:%s
    ", err)
    				return
    			}
    			// 或取当前文件修改时间
    			curModifyTime := fileInfo.ModTime().Unix() 
    			if curModifyTime > c.lastModifyTime {
    				// 重新解析时,要考虑应用程序正在读取这个配置因此应该加锁
    				m, err := c.parse()
    				if err != nil {
    					fmt.Printf("parse config error:%v
    ", err)
    					return
    				}
    
    				c.rwLock.Lock()
    				c.data = m
    				c.rwLock.Unlock()
    
    				c.lastModifyTime = curModifyTime
    
    				// 配置更新通知所有观察者
    				for _, n := range c.notifyList {
    					n.Callback(c)
    				}
    			}
    		}()
    	}
    }
    
    func (c *Config) parse() (m map[string]string, err error) {
    	// 如果在parse()中定义一个map,这样就是一个新的map不用加锁
    	m = make(map[string]string, 1024)
    
    	f, err := os.Open(c.filename)
    	if err != nil {
    		return
    	}
    	defer f.Close()
    
    	reader := bufio.NewReader(f)
    	// 声明一个变量存放读取行数
    	var lineNo int
    	for {
    		line, errRet := reader.ReadString('
    ')
    		if errRet == io.EOF {
    			// 这里有一个坑,最后一行如果不是
    结尾会漏读
    			lineParse(&lineNo, &line, &m)
    			break
    		}
    		if errRet != nil {
    			err = errRet
    			return
    		}
    		
    		lineParse(&lineNo, &line, &m)
    	}
    
    	return 
    }
    
    func lineParse(lineNo *int, line *string, m *map[string]string) {
    		*lineNo++
    
    		l := strings.TrimSpace(*line)
    		// 如果空行 或者 是注释 跳过
    		if len(l) == 0 || l[0] =='
    ' || l[0]=='#' || l[0]==';' {
    			return
    		}
    
    		itemSlice := strings.Split(l, "=")
    		// =
    		if len(itemSlice) == 0 {
    			fmt.Printf("invalid config, line:%d", lineNo)
    			return
    		}
    
    		key := strings.TrimSpace(itemSlice[0])
    		if len(key) == 0 {
    			fmt.Printf("invalid config, line:%d", lineNo)
    			return
    		}
    		if len(key) == 1 {
    			(*m)[key] = ""
    			return
    		}
    
    		value := strings.TrimSpace(itemSlice[1])
    		(*m)[key] = value	
    
    		return
    }
    
    
    func (c *Config) GetInt(key string)(value int, err error){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	str, ok := c.data[key]
    	if !ok {
    		err = fmt.Errorf("key [%s] not found", key)
    	}
    	value, err = strconv.Atoi(str)
    	return
    }
    
    func (c *Config) GetIntDefault(key string, defaultInt int)(value int){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	str, ok := c.data[key]
    	if !ok {
    		value = defaultInt
    		return
    	}
    	value, err := strconv.Atoi(str)
    	if err != nil {
    		value = defaultInt
    	}
    	return
    }
    
    func (c *Config) GetString(key string)(value string, err error){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	value, ok := c.data[key]
    	if !ok {
    		err = fmt.Errorf("key [%s] not found", key)
    	}
    	return
    }
    
    func (c *Config) GetIStringDefault(key string, defaultStr string)(value string){
    	c.rwLock.RLock()
    	defer c.rwLock.RUnlock()
    
    	value, ok := c.data[key]
    	if !ok {
    		value = defaultStr
    		return
    	}
    	return
    }
    

    reconf/notify.go:

    package reconf
    
    // 通知应用程序文件改变
    
    type Notifyer  interface {
    	Callback(*Config)
    }
    

    reconf_test/main.go:

    package main
    
    import (
    	"time"
    	"go_dev/s23_conf/reconf"
    	"sync/atomic"
    	"fmt"
    )
    
    type AppConfig struct {
    	hostname string
    	port int
    	kafkaAddr string
    	kafkaPort int
    }
    
    // reload()协程写 和 for循环的读,都是对Appconfig对象,因此有读写冲突
    type AppConfigMgr struct {
    	config atomic.Value
    }
    
    // 初始化结构体
    var appConfigMgr = &AppConfigMgr{}
    
    func (a *AppConfigMgr)Callback(conf *reconf.Config) {
    	appConfig := &AppConfig{}
    	hostname, err := conf.GetString("hostname")
    	if err != nil {
    		fmt.Printf("get hostname err: %v
    ", err)
    		return
    	}
    	appConfig.hostname = hostname
    
    	kafkaPort, err := conf.GetInt("kafkaPort")
    	if err != nil {
    		fmt.Printf("get kafkaPort err: %v
    ", err)
    		return
    	}
    	appConfig.kafkaPort = kafkaPort
    
    	appConfigMgr.config.Store(appConfig)
    
    }
    
    func initConfig(file string) {
    	// [1] 打开配置文件
    	conf, err := reconf.NewConfig(file)
    	if err != nil {
    		fmt.Printf("read config file err: %v
    ", err)
    		return
    	}
    
    	// 添加观察者
    	conf.AddObserver(appConfigMgr)
    
    	// [2]第一次读取配置文件
    	var appConfig AppConfig
    	appConfig.hostname, err = conf.GetString("hostname")
    	if err != nil {
    		fmt.Printf("get hostname err: %v
    ", err)
    		return
    	}
    	fmt.Println("Hostname:", appConfig.hostname)
    
    	appConfig.kafkaPort, err = conf.GetInt("kafkaPort")
    	if err != nil {
    		fmt.Printf("get kafkaPort err: %v
    ", err)
    		return
    	}
    	fmt.Println("kafkaPort:", appConfig.kafkaPort)
    
    	// [3] 把读取到的配置文件数据存储到atomic.Value
    	appConfigMgr.config.Store(&appConfig)
    	fmt.Println("first load sucess.")
    
    }
    
    func run(){
    	for {
    		appConfig := appConfigMgr.config.Load().(*AppConfig)
    
    		fmt.Println("Hostname:", appConfig.hostname)
    		fmt.Println("kafkaPort:", appConfig.kafkaPort)
    		fmt.Printf("%v
    ", "--------------------")
    		time.Sleep(5 * time.Second)
    	}
    }
    
    func main() { 
    	confFile := "../parseConfig/test.cfg"
    	initConfig(confFile)
    	// 应用程序 很多配置已经不是存在文件中而是etcd
    	run() 
    }  

    本篇所有代码都上传到github上了https://github.com/zingp/goclub/tree/master/go_dev/s23_conf。参见该目录下的reconf 和reconf_test两个目录。

  • 相关阅读:
    Search Insert Position——二分法
    Majority Element——算法课上的一道题(经典)
    Max Points on a Line——数学&&Map
    Valid Number——分情况讨论最经典的题(没细看)——这题必须静下心来好好看看
    Sqrt(x)——二分法,防越界
    Ubuntu系统---NVIDIA 驱动安装
    系统---《windows + ubuntu双系统》
    VS---《在VS2010中 使用C++创建和使用DLL》(003)
    VS---《在VS2010中 使用C++创建和使用DLL》(002)
    图像处理---《在图片上打印文字 windows+GDI+TrueType字体》
  • 原文地址:https://www.cnblogs.com/zingp/p/9330540.html
Copyright © 2011-2022 走看看