【问题标题】:viper dynamically loading config file has data raceviper 动态加载配置文件有数据竞争
【发布时间】:2021-10-25 04:22:02
【问题描述】:

我想动态加载配置文件而不是重新启动我的 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"

去.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 文件中,func loadConfigDynamically,我将 OnConfigChange 设置为读取数据竞争。并且在同一行的同一函数中是先前的写入数据竞争。我用

运行上面的包
go run -race reload_config

并更改 config.yml 中 env 的值以测试配置文件是否动态加载。这种数据竞争仅在第一次动态重新加载配置时发生。对于以后的时间,它工作得很好。

【问题讨论】:

  • 如果我没记错的话,您想在从应用程序外部(手动或以编程方式)修改配置文件时重新加载配置?如果是,推荐的方法是监听文件系统事件并在写入事件时重新加载配置。你可以探索这个模块github.com/fsnotify/fsnotify
  • 我最近读到这个github.com/spf13/viper/issues/378,如果有人知道解决方案,请帮助我。

标签: go concurrency viper-go


【解决方案1】:

你锁定 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() //this starting call vipe.OnConfigChange
    }()
}

【讨论】:

  • 这有帮助!谢谢!
  • 这是我的荣幸 :-)
  • 嗨 Nipuna,现在我正在尝试动态加载多个配置文件。我已经更新了上述问题中的代码。我现在面临的问题是,对于 config.yml,没有打印更新。对于 config_base.yml,更新后的值会被打印出来。你能帮我看看我的错误吗?
  • 如果我使用单独的 viper 对象,从多个文件加载动态配置就可以了。这是推荐的解决方案吗?更新上面的代码。
  • 嗨@nipuna 如果我想从多个 goroutine 中了解配置文件中的更改,因此在配置文件更改后使用应用程序中的最新值,我正在使用通道传递新的价值。我已经更新了代码来做同样的事情。你能告诉我这是否是一个好方法吗?如果有更好的方法来实现这一点,也请告诉我。
【解决方案2】:

可能是 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() // read places
            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() // write places
    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)
}

【讨论】:

  • 感谢您对此进行调查。是的,我遇到过类似的事情。我在内部使用的一个库函数称为另一个创建冲突的库函数。 nipuna 的上述建议奏效了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-04-08
  • 1970-01-01
  • 2017-05-25
  • 1970-01-01
  • 2017-12-02
  • 2021-05-04
  • 1970-01-01
相关资源
最近更新 更多