viper动态加载配置文件存在数据竞争问题。

huangapple go评论89阅读模式
英文:

viper dynamically loading config file has data race

问题

我想动态加载配置文件而不重新启动我的Go应用程序。我编写了下面的文件,它可以运行,但存在数据竞争。

config.go

package main

import (
	"github.com/fsnotify/fsnotify"
	"github.com/spf13/viper"
	"log"
	"sync"
	"time"
)

var (
	reloadConfig  = make(chan string)
	reloadConfig2 = make(chan string)
	viperLock1    sync.Mutex
	viperLock2    sync.Mutex
)

func setUpConfig(file string, merge bool, v *viper.Viper) {
	v.AddConfigPath("./")
	v.SetConfigName(file)
	v.SetConfigType("yml")
	if merge {
		err1 := v.MergeInConfig()
		checkForFatalError("fatal error occurred while reading config file!", err1)
	} else {
		err := v.ReadInConfig()
		checkForFatalError("fatal error occurred while reading config file!", err)
	}
	log.Println("Initial config value: ", v.GetString("env"))
}

func loadConfigDynamically(configChannel chan string, viperLock *sync.Mutex, vipe *viper.Viper) {
	viperLock.Lock()
	vipe.OnConfigChange(func(e fsnotify.Event) {
		viperLock.Lock()
		log.Println("config file changed", e.Name)
		environment := vipe.GetString("env")
		configChannel <- environment
		viperLock.Unlock()
	})
	viperLock.Unlock()
	vipe.WatchConfig()
}

func loadMultipleConfigsDynamically() {
	go func() {
		time.Sleep(time.Millisecond * 50)
		vipe2 := viper.New()
		setUpConfig("config_base", false, vipe2)
		loadConfigDynamically(reloadConfig2, &viperLock2, vipe2)

		time.Sleep(time.Millisecond * 50)
		vipe1 := viper.New()
		setUpConfig("config", false, vipe1)
		loadConfigDynamically(reloadConfig, &viperLock1, vipe1)
	}()
}

main.go

package main

import (
	log "github.com/sirupsen/logrus"
	"os"
	"os/signal"
	"syscall"
)

var reloadConfigNow = make(chan bool)
var reloadConfigAgain = make(chan bool)
var newConfigValue string

func main() {
	loadMultipleConfigsDynamically()
	go printUpdatedValueOnly()
	go justAnotherGoroutine()
	go yetAnotherGoroutine()
	shutdownAppGracefully()
}

func printUpdatedValueOnly() {
	for {
		select {
		case updatedValue := <-reloadConfig:
			newConfigValue = updatedValue
			log.Println("dynamically loaded config value: ", updatedValue)
			reloadConfigNow <- true
			reloadConfigAgain <- true
		case updatedValue1 := <-reloadConfig2:
			newConfigValue = updatedValue1
			log.Println("dynamically loaded config value: ", updatedValue1)
			reloadConfigNow <- true
			reloadConfigAgain <- true
		default:
		}
	}
}

func justAnotherGoroutine() {
	existingConfigValue := ""
	for {
		select {
		case <-reloadConfigNow:
			existingConfigValue = newConfigValue
			log.Println("justAnotherGoroutine: ", existingConfigValue)
		default:
		}
	}
}

func yetAnotherGoroutine() {
	existingConfigValue := ""
	for {
		select {
		case <-reloadConfigAgain:
			existingConfigValue = newConfigValue
			log.Println("yetAnotherGoroutine: ", existingConfigValue)
		default:
		}
	}
}

func checkForFatalError(errorMsg string, err error) {
	if err != nil {
		log.Fatal(errorMsg, err)
	}
}

func shutdownAppGracefully() {
	killSignal := make(chan os.Signal, 1)
	signal.Notify(killSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
	k := <-killSignal
	log.Info("OS Interrupt Signal received, application is shutting down!")
	logSystemInterruptType(k)
}

func logSystemInterruptType(osInterrupt os.Signal) {
	switch osInterrupt {
	case syscall.SIGHUP:
		log.Info("SIGHUP")
	case syscall.SIGINT:
		log.Info("SIGINT")
	case syscall.SIGTERM:
		log.Info("SIGTERM")
	case syscall.SIGQUIT:
		log.Info("SIGQUIT")
	default:
		log.Info("Unknown OS Interrupt")
	}
}

config.yml

env : "LOCAL"

config_base.yml

env : "dev15"

go.mod

module reload_config

go 1.16

require (
	github.com/fsnotify/fsnotify v1.4.9
	github.com/spf13/viper v1.8.1
)

我最近了解到viper不是线程安全的,因此我需要用互斥锁包装它。我尝试做了同样的事情。在config.go文件的loadConfigDynamically函数中,我设置OnConfigChange是读取的数据竞争。在同一函数的同一行是先前的写入数据竞争。我使用以下命令运行上面的包:

go run -race reload_config

并更改config.yml中的env值以测试是否动态加载配置文件。这个数据竞争只会在第一次动态重新加载配置时发生。对于后续的重新加载,它可以正常工作。

英文:

I would like to dynamically load config file and not restart my Go app. I wrote the below files, which runs but has data race.

config.go

package main
import (
&quot;github.com/fsnotify/fsnotify&quot;
&quot;github.com/spf13/viper&quot;
&quot;log&quot;
&quot;sync&quot;
&quot;time&quot;
)
var (
reloadConfig  = make(chan string)
reloadConfig2 = make(chan string)
viperLock1    sync.Mutex
viperLock2    sync.Mutex
)
func setUpConfig(file string, merge bool, v *viper.Viper) {
v.AddConfigPath(&quot;./&quot;)
v.SetConfigName(file)
v.SetConfigType(&quot;yml&quot;)
if merge {
err1 := v.MergeInConfig()
checkForFatalError(&quot;fatal error occurred while reading config file!&quot;, err1)
} else {
err := v.ReadInConfig()
checkForFatalError(&quot;fatal error occurred while reading config file!&quot;, err)
}
log.Println(&quot;Initial config value: &quot;, v.GetString(&quot;env&quot;))
}
func loadConfigDynamically(configChannel chan string, viperLock *sync.Mutex, vipe *viper.Viper) {
viperLock.Lock()
vipe.OnConfigChange(func(e fsnotify.Event) {
viperLock.Lock()
log.Println(&quot;config file changed&quot;, e.Name)
environment := vipe.GetString(&quot;env&quot;)
configChannel &lt;- environment
viperLock.Unlock()
})
viperLock.Unlock()
vipe.WatchConfig()
}
func loadMultipleConfigsDynamically() {
go func() {
time.Sleep(time.Millisecond * 50)
vipe2 := viper.New()
setUpConfig(&quot;config_base&quot;, false, vipe2)
loadConfigDynamically(reloadConfig2, &amp;viperLock2, vipe2)
time.Sleep(time.Millisecond * 50)
vipe1 := viper.New()
setUpConfig(&quot;config&quot;, false, vipe1)
loadConfigDynamically(reloadConfig, &amp;viperLock1, vipe1)
}()
}

main.go

package main
import (
log &quot;github.com/sirupsen/logrus&quot;
&quot;os&quot;
&quot;os/signal&quot;
&quot;syscall&quot;
)
var reloadConfigNow = make(chan bool)
var reloadConfigAgain = make(chan bool)
var newConfigValue string
func main() {
loadMultipleConfigsDynamically()
go printUpdatedValueOnly()
go justAnotherGoroutine()
go yetAnotherGoroutine()
shutdownAppGracefully()
}
func printUpdatedValueOnly()  {
for {
select {
case updatedValue := &lt;-reloadConfig:
newConfigValue = updatedValue
log.Println(&quot;dynamically loaded config value: &quot;, updatedValue)
reloadConfigNow &lt;-true
reloadConfigAgain &lt;-true
case updatedValue1 := &lt;-reloadConfig2:
newConfigValue = updatedValue1
log.Println(&quot;dynamically loaded config value: &quot;, updatedValue1)
reloadConfigNow &lt;-true
reloadConfigAgain &lt;-true
default:
}
}
}
func justAnotherGoroutine(){
existingConfigValue := &quot;&quot;
for {
select {
case &lt;-reloadConfigNow:
existingConfigValue = newConfigValue
log.Println(&quot;justAnotherGoroutine: &quot;, existingConfigValue)
default:
}
}
}
func yetAnotherGoroutine()  {
existingConfigValue := &quot;&quot;
for {
select {
case &lt;-reloadConfigAgain:
existingConfigValue = newConfigValue
log.Println(&quot;yetAnotherGoroutine: &quot;, existingConfigValue)
default:
}
}
}
func checkForFatalError(errorMsg string, err error) {
if err != nil {
log.Fatal(errorMsg, err)
}
}
func shutdownAppGracefully() {
killSignal := make(chan os.Signal, 1)
signal.Notify(killSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
k := &lt;-killSignal
log.Info(&quot;OS Interrupt Signal received, application is shutting down!&quot;)
logSystemInterruptType(k)
}
func logSystemInterruptType(osInterrupt os.Signal) {
switch osInterrupt {
case syscall.SIGHUP:
log.Info(&quot;SIGHUP&quot;)
case syscall.SIGINT:
log.Info(&quot;SIGINT&quot;)
case syscall.SIGTERM:
log.Info(&quot;SIGTERM&quot;)
case syscall.SIGQUIT:
log.Info(&quot;SIGQUIT&quot;)
default:
log.Info(&quot;Unknown OS Interrupt&quot;)
}
}

config.yml

env : &quot;LOCAL&quot;

config_base.yml

env : &quot;dev15&quot;

go.mod

module reload_config
go 1.16
require (
github.com/fsnotify/fsnotify v1.4.9
github.com/spf13/viper v1.8.1
)

I learned recently that viper is not thread safe and hence I need to wrap it with mutex. I tried to do the same. In config.go file, func loadConfigDynamically, where I set OnConfigChange is the data race for read. And in the same function at the same line is previous write data race. I run the above package with

go run -race reload_config

And change the value of env in the config.yml to test if the config file is loading dynamically.This data race only occurs for the very first time config reloading dynamically. For subsequent times, it works just fine.

答案1

得分: 3

你锁定了名为viperLock的锁,并使用一个函数vipe.WatchConfig()来设置vipe.OnConfigChange,该函数也会锁定viperLock

因为你已经调用了vipe.WatchConfig(),然后它开始在单独的go例程中调用vipe.OnConfigChange,它也试图获取相同的锁。这就是为什么会出现竞争条件。

在设置vipe.OnConfigChange并释放锁之后,调用vipe.WatchConfig()

应该按照以下方式进行更正。

func loadConfigDynamically() {
	go func() {
		time.Sleep(time.Second)
		viperLock.Lock()
		vipe.OnConfigChange(func(e fsnotify.Event) {
			viperLock.Lock()
			log.Println("config file changed", e.Name)
			environment := vipe.GetString("env")
			reloadConfig <- environment
			viperLock.Unlock()
		})
		viperLock.Unlock()
		vipe.WatchConfig() // 这个调用会启动 vipe.OnConfigChange
	}()
}
英文:

You lock viperLock called vipe.WatchConfig() and set vipe.OnConfigChange with a function it is also locking viperLock.

Because you already called vipe.WatchConfig() and then it started to call vipe.OnConfigChange in separate go routine. it is also try to acquire the same lock. That's why there is a race condition.

Call vipe.WatchConfig() after setting the vipe.OnConfigChange and after release the lock.

It should be corrected as below.

func loadConfigDynamically() {
go func() {
time.Sleep(time.Second)
viperLock.Lock()
vipe.OnConfigChange(func(e fsnotify.Event) {
viperLock.Lock()
log.Println(&quot;config file changed&quot;, e.Name)
environment := vipe.GetString(&quot;env&quot;)
reloadConfig &lt;- environment
viperLock.Unlock()
})
viperLock.Unlock()
vipe.WatchConfig() //this starting call vipe.OnConfigChange
}()
}

答案2

得分: 0

可能是因为Go语言认为一个变量被两个goroutine同时修改和访问,并且在修改和访问的位置没有加锁。以下是一个类似的示例:

package main

import (
	"time"
)

type Foo struct {
	f func(string)
}

func (f *Foo) Watch() {
	go func() {
		for {
			time.Sleep(time.Second * 2)
			if f.f != nil {
				f.f("hello world")
			}
		}
	}()
}

func (f *Foo) SetF(fun func(string)) {
	f.f = fun
}

func main() {
	f := Foo{}

	f.Watch()
	f.SetF(func(s string) {
	})

	time.Sleep(time.Second * 5)
}

这段代码存在数据竞争。如果在修改和读取的位置都加上相同的锁,就不会出现数据竞争:

package main

import (
	"sync"
	"time"
)

var lock sync.Mutex

type Foo struct {
	f func(string)
}

func (f *Foo) Watch() {
	go func() {
		for {
			time.Sleep(time.Second * 2)
			lock.Lock() // 读取位置
			if f.f != nil {
				f.f("hello world")
			}
			lock.Unlock()
		}
	}()
}

func (f *Foo) SetF(fun func(string)) {
	f.f = fun
}

func main() {
	f := Foo{}

	f.Watch()
	lock.Lock() // 写入位置
	f.SetF(func(s string) {
	})
	lock.Unlock()

	time.Sleep(time.Second * 5)
}

或者,为了消除两个goroutine同时读取和写入的可能性,可以这样修改代码:

func main() {
	f := Foo{}

	f.SetF(func(s string) {
	})
	f.Watch()

	time.Sleep(time.Second * 5)
}
英文:

It could be that go thinks that a variable is being modified and accessed by two goroutines at the same time and that there is no lock on the modified and accessed places.
Something like the following example:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: go -->

package main
import (
&quot;time&quot;
)
type Foo struct {
f func(string)
}
func (f *Foo) Watch() {
go func() {
for {
time.Sleep(time.Second * 2)
if f.f != nil {
f.f(&quot;hello world&quot;)
}
}
}()
}
func (f *Foo) SetF(fun func(string)) {
f.f = fun
}
func main() {
f := Foo{}
f.Watch()
f.SetF(func(s string) {
})
time.Sleep(time.Second * 5)
}

<!-- end snippet -->

It has a data race. If I put the same lock on both the modified and read places there will be no data race:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: go -->

package main
import (
&quot;sync&quot;
&quot;time&quot;
)
var lock sync.Mutex
type Foo struct {
f func(string)
}
func (f *Foo) Watch() {
go func() {
for {
time.Sleep(time.Second * 2)
lock.Lock() // read places
if f.f != nil {
f.f(&quot;hello world&quot;)
}
lock.Unlock()
}
}()
}
func (f *Foo) SetF(fun func(string)) {
f.f = fun
}
func main() {
f := Foo{}
f.Watch()
lock.Lock() // write places
f.SetF(func(s string) {
})
lock.Unlock()
time.Sleep(time.Second * 5)
}

<!-- end snippet -->

Or to eliminate the possibility of two goroutines reading and writing at the same time would work fine:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: go -->

func main() {
f := Foo{}
f.SetF(func(s string) {
})
f.Watch()
time.Sleep(time.Second * 5)
}

<!-- end snippet -->

huangapple
  • 本文由 发表于 2021年8月25日 10:12:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/68915944.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定