环境变量(配置)
Region、Zone、Cluster、Environment、Color、Discovery、AppID、Host,等之类的环境信息,都是通过在线运行时平台打入到容器或者物理机,供 kit 库读取使用。
静态配置
资源需要初始化的配置信息,比如 http/gRPC server、redis、mysql 等,这类资源在线变更配置的风险非常大,我通常不鼓励 on-the-fly 变更,很可能会导致业务出现不可预期的事故,变更静态配置和发布 bianry app 没有区别,应该走一次迭代发布的流程。
动态配置
应用程序可能需要一些在线的开关,来控制业务的一些简单策略,会频繁的调整和使用,我们把这类是基础类型(int, bool)等配置,用于可以动态变更业务流的收归一起,同时可以考虑结合类似 https://pkg.go.dev/expvar 来结合使用。
全局配置
通常,我们依赖的各类组件、中间件都有大量的默认配置或者指定配置,在各个项目里大量拷贝复制,容易出现意外,所以我们使用全局配置模板来定制化常用的组件,然后再特化的应用里进行局部替换。
Redis client example
// DialTimeout acts like Dial for establishing the
// connection to the server, writing a command and reading a reply.
func Dial(network, address string) (Conn, error)
“我要自定义超时时间!”
“我要设定 Database!”
“我要控制连接池的策略!”
“我要安全使用 Redis,让我填一下 Password!”
“可以提供一下慢查询请求记录,并且可以设置 slowlog 时间?”
Add Features
// DialTimeout acts like Dial for establishing the
// connection to the server, writing a command and reading a reply.
func Dial(network, address string) (Conn, error)
// DialTimeout acts like Dial but takes timeouts for establishing the
// connection to the server, writing a command and reading a reply.
func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error)
// DialDatabase acts like Dial but takes database for establishing the
// connection to the server, writing a command and reading a reply.
func DialDatabase(network, address string, database int) (Conn, error)
// DialPool
func DialPool...
net/http
import (
"log"
"net/http"
"time"
)
func main() {
s := &http.Server{
Addr: ":8080",
Handler: nil,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
}
Configuration struct API
// NewConn new a redis conn.
func NewConn(c Config) (cn Conn, err error)
// NewConn new a redis conn.
func NewConn(c *Config) (cn Conn, err error)
// NewConn new a redis conn.
func NewConn(c ...*Config) (cn Conn, err error)
import (
"github.com/go-kratos/kratos/pkg/log"
)
func main() {
log.Init(nil) // 这样使用默认配置
// config.fix() // 修正默认配置
}
“I believe that we, as Go programmers, should work hard to ensure that nil is never a parameter that needs to be passed to any public function.” – Dave Cheney
Functional options
Self-referential functions and the design of options — Rob Pike Functional options for friendly APIs — Dave Cheney
// DialOption specifies an option for dialing a Redis server.
type DialOption struct {
f func(*dialOptions)
}
// Dial connects to the Redis server at the given network and
// address using the specified options.
func Dial(network, address string, options ...DialOption) (Conn, error) {
do := dialOptions{
dial: net.Dial,
}
for _, option := range options {
option.f(&do)
} // ...
}
package main
import (
"time"
"github.com/go-kratos/kratos/pkg/cache/redis"
)
func main() {
c, _ := redis.Dial("tcp", "127.0.0.1:3389",
redis.DialDatabase(0),
redis.DialPassword("hello"),
redis.DialReadTimeout(10*time.Second))
}
// DialOption specifies an option for dialing a Redis server.
type DialOption func(*dialOptions)
// Dial connects to the Redis server at the given network and
// address using the specified options.
func Dial(network, address string, options ...DialOption) (Conn, error) {
do := dialOptions{
dial: net.Dial,
}
for _, option := range options {
option(&do)
}
// ...
}
type option func(f *Foo) option
// Verbosity sets Foo's verbosity level to v.
func Verbosity(v int) option {
return func(f *Foo) option {
prev := f.verbosity
f.verbosity = v
return Verbosity(prev)
}
}
func DoSomethingVerbosely(foo *Foo, verbosity int) {
// Could combine the next two lines,
// with some loss of readability.
prev := foo.Option(pkg.Verbosity(verbosity))
defer foo.Option(prev)
// ... do some stuff with foo under high verbosity.
}
type GreeterClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
type CallOption interface {
before(*callInfo) error
after(*callInfo)
}
// EmptyCallOption does not alter the Call configuration.
type EmptyCallOption struct{}
// TimeoutCallOption timeout option.
type TimeoutCallOption struct {
grpc.EmptyCallOption
Timeout time.Duration
}
Hybrid APIs
// Dial connects to the Redis server at the given network and
// address using the specified options.
func Dial(network, address string, options ...DialOption) (Conn, error)
// NewConn new a redis conn.
func NewConn(c *Config) (cn Conn, err error)
“JSON/YAML
配置怎么加载,无法映射 DialOption 啊!”
“嗯,不依赖配置的走 options,配置加载走 config ”
Configuration & APIs
package redis
// Option configures how we set up the connection.
type Option interface {
apply(*options)
}
// Options apply config to options.
func (c *Config) Options() []redis.Options {
return []redis.Options{
redis.DialDatabase(c.Database),
redis.DialPassword(c.Password),
redis.DialReadTimeout(c.ReadTimeout),
}
}
func main() {
// instead use load yaml file.
c := &Config{
Network: "tcp",
Addr: "127.0.0.1:3389",
Database: 1,
Password: "Hello",
ReadTimeout: 1 * time.Second,
}
r, _ := redis.Dial(c.Network, c.Addr, c.Options()...)
}
Configuration Best Pratice
代码更改系统功能是一个冗长且复杂的过程,往往还涉及 Review、测试等流程,但更改单个配置选项可能会对功能产生重大影响,通常配置还未经测试。配置的目标:
- 避免复杂
- 多样的配置
- 简单化努力
- 以基础设施 -> 面向用户进行转变
- 配置的必选项和可选项
- 配置的防御编程
- 权限和变更跟踪
- 配置的版本和应用对齐
- 安全的配置变更:逐步部署、回滚更改、自动回滚