配置中心

概述

应用程序中的大多数配置都是静态配置或者从多个源加载的复杂逻辑。Config 是其变得简单,可插拔和可合并。

特性

  • 动态加载 - 在需要时从多个源加载配置. Config 管理在后台监听配置源,并自动合并和更新内存中的配置文件源。
  • 可插拔的源 - 从任意数量的源中进行选择以加载和合并配置。后端源被抽象为内部使用并通过编码器解码的标准格式。源可以是 env vars, flags, etcd, k8s configmap, 等。
  • 可合并配置 - 如果指定多个配置源,无论格式如何,它们都将合并并在单个视图中显示。这极大地简化了基于环境的优先级顺序加载和更改。
  • 改动监听 - 可选择监听配置,以监控特定值的更改。使用 Config 的监听程序热加载你的应用。不必临时关机重新加载其他任何内容,只需继续读取配置并监听需要通知的更改。
  • 安全恢复 - 如果配置负载严重或由于未知原因完全擦除,则可以在直接访问任何配置值时指定回退值。这可确保在发生问题时始终读取某些合理的默认值。

内部实现

下面介绍 Config 的实现,Config 有以下部分组成:

  • source: 配置的来源
  • encoder: 处理编码/解码源配置
  • reader: 将多个编码源合并为单一格式
  • loader:管理加载源

Source

Source 作为配置的读取来源。可以同时使用多个源。 支持以下源:

cli : 从 CLI 标志读取

func main() {
    // New Service
    service := vine.NewService(
        vine.Name("example"),
        vine.Flags(
            cli.StringFlag{
                Name: "database-address",
                Value: "127.0.0.1",
                Usage: "the db address",
            },
        ),
    )

    var clisrc source.Source

    service.Init(
        vine.Action(func(c *cli.Context) {
            clisrc = cli.NewSource(
                cli.Context(c),
	    )
            // Alternatively, just setup your config right here
        }),
    )
    
    // ... Load and use that source ...
    conf := config.NewConfig()
    conf.Load(clisrc)
}

env : 从环境变量中读取

src := env.NewSource(
	// optionally specify prefix
	env.WithPrefix("VINE"),
)

// Create new config
conf := config.NewConfig()

// Load env source
conf.Load(src)

file : 从文件中读取

func main() {
	data := []byte(`{"foo": "bar"}`)
	path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano()))
	fh, err := os.Create(path)
	if err != nil {
		t.Error(err)
	}
	defer func() {
		fh.Close()
		os.Remove(path)
	}()

	_, err = fh.Write(data)
	if err != nil {
		t.Error(err)
	}

	f := NewSource(WithPath(path))
	c, err := f.Read()
	if err != nil {
		t.Error(err)
	}
}

flag : 从标志中读取

flagSource := flag.NewSource(
	// optionally enable reading of unset flags and their default
	// values into config, defaults to false
	IncludeUnset(true)
)
// Create new config
conf := config.NewConfig()

// Load flag source
conf.Load(flagSource)

memory : 从内存中读取

memorySource := memory.NewSource(
	memory.WithJSON(data),
)
// Create new config
conf := config.NewConfig()
// Load memory source
conf.Load(memorySource)

ChangeSet

SourceConfig 作为一个 ChangeSet 返回,作为多个源的单一内部抽象。

// ChangeSet represents a set of changes from a source
type ChangeSet struct {
	Data      []byte
	CheckSum  string
	Format    string
	Source    string
	Timestamp time.Time
}

encoder

Encoder 处理编码和解码。后端源可能以许多不停的格式存储。编码器使我们能够处理任何格式。如果未指定编码器,则默认为 json。 支持以下编码格式:

  • json
  • yaml
  • toml
  • xml
  • hcl

reader

Reader 将多个 ChangeSet 表示为单个合并和查询的集合

// Reader is an interface for merging changesets
type Reader interface {
    // Merge multiple changeset into a single format
    Merge(...*source.ChangeSet) (*source.ChangeSet, error)
    // Return Go assertable values
    Values(*source.ChangeSet) (Values, error)
    // Name of the reader e.g json reader
	String() string
}

Reader 使用 EncoderChangeSet 解码为 map[string]Values,然后将它们合并到单个 ChangeSet 中。

// Values is returned by the reader
type Values interface {
	Bytes() []byte
	Get(path ...string) Value
	Set(val interface{}, path ...string)
	Del(path ...string)
	Map() map[string]interface{}
	Scan(v interface{}) error
}

Value 接口允许强制转换,类型断言,返回默认值

// Value represents a value of any type
type Value interface {
	Bool(def bool) bool
	Int(def int64) int64
	String(def string) string
	Float64(def float64) float64
	Duration(def time.Duration) time.Duration
	StringSlice(def []string) []string
	StringMap(def map[string]string) map[string]string
	Scan(val interface{}) error
	Bytes() []byte
}

Config

Config 管理所有配置,抽象 source,encoder,reader。 它从多个源读取,同步,监听,并将它们合并为单个可查询的源。

// Config is an interface abstraction for dynamic configuration
type Config interface {
	// provide the reader.Values interface
	reader.Values
	// Init the config
	Init(opts ...Option) error
	// Options in the config
	Options() Options
	// Stop the config loader/watcher
	Close() error
	// Load config sources
	Load(source ...source.Source) error
	// Force a source changeset sync
	Sync() error
	// Watch a value for changes
	Watch(path ...string) (Watcher, error)
}

使用

实例配置

只要我们有一个编码器来支持它,配置文件就可以是任务格式的。 json 配置实例:

{
    "hosts": {
        "database": {
            "address": "10.0.0.1",
            "port": 3306
        },
        "cache": {
            "address": "10.0.0.2",
            "port": 6379
        }
    }
}

创建 Config

创建一个新 Config

import "github.com/vine-io/vine/services/config"

conf := config.NewConfig()

加载文件

从文件加载配置,根据文件扩展名来确定配置格式。

import "github.com/vine-io/vine/services/config"

// 加载 json 配置文件
config.LoadFile("/tmp/config.json")

如果扩展不存在,可指定 encoder

package main

import (
	"github.com/vine-io/vine/service/config"
	"github.com/vine-io/vine/service/config/encoder/toml"
	"github.com/vine-io/vine/service/config/source"
	"github.com/vine-io/vine/service/config/source/file"
)

func main() {
	enc := toml.NewEncoder()
	
	// 通过编码器加载 toml 文件
	config.Load(
		file.NewSource(
			file.WithPath("/tmp/config"),
			source.WithEncoder(enc),
		),
	)
}

读取配置

conf := config.Map()

fmt.Println(conf["hosts"])

扫描配置到结构体

type Host struct {
	Address string `json:"address"`
	Port int `json:"port"`
}

type Config struct {
	Hosts map[string]Host `json:"hosts"`
}

func main() {
	pwd, _ := os.Getwd()
	err := config.LoadFile(pwd + "/config/" + "config.json")
	if err != nil {
		log.Fatal(err)
	}

	var cfg Config
	config.Scan(&cfg)

	fmt.Println(cfg.Hosts["database"].Address)
}

读取值

从配置扫描值到结构体

type Host struct {
    Address string  `json:"address"`
    Port    int     `json:"port"`
}

var host Host

config.Get("hosts", "database").Scan(&host)

// 10.0.0.1 3306
fmt.Println(host.Address, host.Port)

读取单个值作为 Go 类型

// Get address. Set default to localhost as fallback
address := config.Get("hosts", "database", "address").String("localhost")

// Get port. Set default to 3000 as fallback
port := config.Get("hosts", "database", "port").Int(3000)

监听配置

监听配置文件。当文件更改时,更新到新值:

w, err := config.Watch("hosts", "database")
if err != nil {
    // do something
}

// wait for next value
v, err := w.Next()
if err != nil {
    // do something
}

var host Host

v.Scan(&host)

多源

可以加载和合并多个源。合并优先级顺序相反:

config.Load(
    // base config from env
    env.NewSource()
    // override env with flags
    flag.NewSource()
    // override flags with file
    file.NewSource(file.WatchPath("/tmp/config.json"))
)

设置源编码器

源要求编码器对数据进行编码 / 解码并指定更改集格式.

默认的编码器是 json. 要更改编码器可调整对应的类型选项.

e := yaml.NewEncoder()

s := consul.NewSource(
    source.WithEncoder(e),
)

添加读取器编码器

读取器使用编码器从不同格式的源解码数据.

默认的读取器支持 json, yaml, xml, toml 和 hcl. 它将合并的配置表示为 json.

可以通过指定的选项来添加一个新的编码器.

e := yaml.NewEncoder()

r := json.NewReader(
    reader.WithEncoder(e),
)

最后修改 September 4, 2021: config (b079a56)