-1

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


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

4

2 回答 2

1

你 lockviperLock调用vipe.WatchConfig()并设置vipe.OnConfigChange 了一个函数,它也是lock viperLock

因为你已经打电话了vipe.WatchConfig(),然后它开始vipe.OnConfigChange在单独的 goroutine 中调用。它也尝试获取相同的锁。这就是为什么存在竞争条件。

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

它应该被纠正如下。

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
    }()
}
于 2021-08-25T04:20:27.743 回答
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() // 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)
}
于 2021-08-25T09:06:48.507 回答