繁体   English   中英

viper 动态加载配置文件有数据竞争

[英]viper dynamically loading config file has data race

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


配置文件

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 的值以测试配置文件是否正在动态加载。此数据竞争仅在第一次动态重新加载配置时发生。 对于随后的时间,它工作得很好。

您锁定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
    }()
}

可能是 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)
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM