一 前言
1.1背景说明
目前, Kitex 用户修改客户端/服务器的配置时,需要调用option.go的函数手动生成Option配置类,进行初始化:
cli, err := somService.NewClient(svcName, client.WithHostPorts(addr))*
如果需要修改配置,就需要直接修改代码并重新编译项目。
在查阅资料的过程中,我发现在Kitex框架中已经存在更改运行时状态的扩展config-file,可以从json或yml文件中读取熔断、请求超时、失败重置的运行时状态。
1.2 代码结构
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE-APACHE
├── README.md
├── README_CN.md
├── _typos.toml
├── check_branch_name.sh
├── client
│ ├── circuit_breaker.go
│ ├── client.go
│ ├── retry.go
│ ├── rpc_timeout.go
│ └── suite.go
├── example
│ ├── client
│ │ ├── doc.go
│ │ ├── kitex_client.json
│ │ └── main.go
│ ├── go.mod
│ ├── go.sum
│ └── server
│ ├── kitex_server.json
│ └── main.go
├── filewatcher
│ └── filewatcher.go
├── go.mod
├── go.sum
├── mock
│ └── filewatcher_mock.go
├── monitor
│ ├── monitor.go
│ └── monitor_test.go
├── parser
│ ├── client.go
│ ├── parser.go
│ └── server.go
├── profile
│ └── README.md
├── server
│ ├── limit.go
│ ├── server.go
│ └── suite.go
├── testdata
│ └── test.json
└── utils
├── set.go
├── set_test.go
└── utils.go
client 目录:包含客户端相关的 Go 代码文件,如 circuit_breaker.go(可能包含断路器的实现)、client.go(可能包含客户端的主要逻辑)、retry.go(可能包含重试逻辑)、rpc_timeout.go(可能包含 RPC 超时设置)和 suite.go(可能包含一些客户端的辅助函数或方法)。
example 目录:包含示例代码,包括一个客户端和一个服务器,每个都有一个主要的 main.go 文件和一个 JSON 配置文件。
filewatcher 目录:包含 filewatcher.go,可能是一个用于监视文件变化的工具。
mock 目录:包含 filewatcher_mock.go,可能是用于测试的模拟文件监视器。
monitor 目录:包含 monitor.go 和 monitor_test.go,可能是用于监控和测试监控功能的代码。
parser 目录:包含 client.go、parser.go 和 server.go,可能是用于解析客户端和服务器配置的代码。
profile 目录:包含 README.md,可能是关于性能分析的文档。
server 目录:包含服务器相关的 Go 代码文件,如 limit.go(可能包含限流逻辑)、server.go(可能包含服务器的主要逻辑)和 suite.go(可能包含一些服务器的辅助函数或方法)。
testdata 目录:包含 test.json,可能是用于测试的数据。
utils 目录:包含 set.go、set_test.go 和 utils.go,可能是一些通用的工具函数和测试。
go.mod 和 go.sum:这些是 Go 项目的依赖管理文件。
二 原理分析
2.1 监听:fsnotify
fsnotify 是一个 Go 语言库,用于文件系统的通知。它是一个跨平台的库,可以在 Linux、BSD、OS X 和 Windows 等操作系统上使用。fsnotify 可以监视文件或目录的创建、删除、移动、重命名和修改等事件。
在本项目中,fsnotify 库被用于监视特定文件的变化。具体实现在 filewatcher.go 文件中的 fileWatcher 结构体和相关方法。 fileWatcher 结构体中有一个 *fsnotify.Watcher 类型的字段 watcher,这是 fsnotify 的核心组件,用于监视文件系统事件。在 NewFileWatcher 函数中,创建了一个新的 fsnotify.Watcher 实例,并将其赋值给 fileWatcher 结构体的 watcher 字段。在 StartWatching 方法中,调用了 fsnotify.Watcher 的 Add 方法,将需要监视的文件路径添加到监视器中,然后在一个新的 goroutine 中启动了文件监视。具体代码段如下:
在 start 方法中,使用了一个无限循环来接收和处理 fsnotify.Watcher 的事件。当接收到文件写入事件(fsnotify.Write)时,会调用 CallOnceAll 方法,该方法会读取文件内容,并将内容传递给所有注册的回调函数。如果接收到文件被删除的事件(fsnotify.Remove),则会停止文件监视。
2.2 加载:基于mapstructure的字节流解码
mapstructure 是一个 Go 语言库,用于将通用的 map 结构解码到 Go 语言的结构体中。它主要用于在处理 JSON、YAML 或其他类似的数据结构时,将这些数据结构解码到具体的 Go 语言结构体中。在使用 mapstructure 进行解码时,首先需要定义一个 Go 语言的结构体,该结构体的字段名和 map 中的键名对应。然后,可以使用 mapstructure.Decode 函数将 map 解码到这个结构体中。
以本项目中的ClientFileConfig为例:
其中,rpctimeout.RPCTimeout、retry.Policy、circuitbreak.CBConfig均为Kitex库中已定义好的类,因此此处不需要重新声明。如果用户需要使用自定义的配置项结构,则需要按照该格式为字段打上Tag。然后,我们可以使用 mapstructure.Decode 将数据解码到这个结构体中。
例如 map[string]interface{}{"timeout": value},使用 mapstructure 库将这个 map 解码到 ClientFileConfig 结构体时,value 会被解码到 Timeout 字段。client和server的Config是通过monitor内不同的manager区分。例如client/suite.go在Options()中设置了client的configManager:
三 模块结构
3.1 Parser:文件的编码与解码
Parser 是一个在项目中用于处理配置文件编码和解码的模块。Parser 模块的作用是提供一个通用的接口,用于将不同格式的配置数据解码到 Go 语言的结构体中。它定义了一个 ConfigParser 接口和一个 Parser 结构体,这个结构体实现了 ConfigParser 接口。
ConfigParser 接口定义了一个 Decode 方法,这个方法接收三个参数:配置类型(kind)、配置数据(data)和一个空接口(config)。这个方法的作用是将 data 中的数据解码到 config 中,解码的方式取决于 kind。
Parser 结构体实现了 ConfigParser 接口的 Decode 方法。在这个方法中,根据 kind 的值,选择不同的解码方式。如果 kind 是 JSON,则使用 sonic.Unmarshal 方法进行解码;如果 kind 是 YAML,则使用 yaml.Unmarshal 方法进行解码。如果 kind 不是这两种类型,那么会返回一个错误。
DefaultConfigParser 函数返回一个默认的 ConfigParser 实例,这个实例是 Parser 结构体的一个实例。
DefaultConfigParam 函数返回一个默认的 ConfigParam 实例,这个实例的 Type 字段的值是 JSON。
3.2 Filewatcher:文件监听
FileWatcher 是一个在项目中用于监视文件变化的模块。它的主要功能是监视指定文件的变化,并在文件发生变化时执行注册的回调函数。在 filewatcher.go 文件中,定义了一个 FileWatcher 接口和一个 fileWatcher 结构体,这个结构体实现了 FileWatcher 接口。
FileWatcher 接口定义了以下方法:
FilePath():返回当前对象正在监听的文件地址。
CallbackSize():返回回调函数的数量。
RegisterCallback(callback func(data []byte)) int64:设置回调函数,当文件发生变化时,这些回调函数会被执行。
DeregisterCallback(uniqueID int64):移除回调函数。
StartWatching() error:开始监视文件变化。
StopWatching():停止监视文件变化。
CallOnceAll() error:一次性调用所有的回调函数。
CallOnceSpecific(uniqueID int64) error:通过唯一ID一次性调用特定的回调函数。
fileWatcher 实现了FileWatcher接口,其结构体中有以下字段:
filePath:需要监视的文件路径。
callbacks:注册的回调函数,当文件发生变化时,这些函数会被执行。
watcher:fsnotify.Watcher 实例,用于监视文件系统事件。
done:一个用于通知监视器停止的通道。
lock:一个读写锁,用于保护 callbacks 的并发访问。
counter:一个原子计数器,用于生成回调函数的唯一ID。
NewFileWatcher 函数用于创建一个新的 FileWatcher 实例。在这个函数中,创建了一个新的 fsnotify.Watcher 实例,并将其赋值给 fileWatcher 结构体的 watcher 字段。
StartWatching 方法用于开始监视文件变化。在这个方法中,将需要监视的文件路径添加到 fsnotify.Watcher 中,并在一个新的 goroutine 中启动文件监视。
start 方法用于处理 fsnotify.Watcher 的事件。当接收到文件写入事件时,会调用 CallOnceAll 方法,该方法会读取文件内容,并将内容传递给所有注册的回调函数。如果接收到文件被删除的事件,则会停止文件监视。
3.3 ConfigMonitor:配置管理中心
ConfigMonitor 是一个在项目中用于管理配置的模块。它的主要职责是处理所有与配置相关的操作,包括读取配置文件,解析配置数据,提供配置数据的访问接口,以及监视配置文件的变化。
在 monitor.go 文件中,定义了一个 ConfigMonitor 接口和一个 configMonitor 结构体,这个结构体实现了 ConfigMonitor 接口。
ConfigMonitor 接口定义了以下方法:
Key():返回配置文件的键。
Config():返回配置详情。
CallbackSize():返回回调函数的数量。
Start():开始监视文件变化。
WatcherID():返回文件监视器的唯一ID。
Stop():停止监视文件变化。
SetManager(manager parser.ConfigManager):设置配置文件的管理器。
SetParser(parser parser.ConfigParser):设置配置文件的解析器。
SetParams(params *parser.ConfigParam):设置配置文件的参数,例如文件类型。
ConfigParse(kind parser.ConfigType, data []byte, config interface{}):调用配置解析器的 Decode 方法。
RegisterCallback(callback func()) int64:添加回调函数,当文件发生变化时,这些回调函数会被调用。
DeregisterCallback(uniqueID int64):移除回调函数
configMonitor 结构体中有以下字段:
parser:配置文件的解析器。
params:配置文件的参数。
manager:配置文件的管理器。
config:配置详情。
fileWatcher:本地配置文件的监视器。
callbacks:当配置文件发生变化时,会被调用的回调函数。
key:配置文件的键。
id:文件监视器的唯一ID。
lock:互斥锁,用于保护 callbacks 的并发访问。
counter:一个原子计数器,用于生成回调函数的唯一ID。
NewConfigMonitor 函数用于创建一个新的 ConfigMonitor 实例。在这个函数中,创建了一个新的 fileWatcher 实例,并将其赋值给 configMonitor 结构体的 fileWatcher 字段。
Start 方法用于开始监视文件变化。在这个方法中,将需要监视的文件路径添加到 fileWatcher 中,并在一个新的 goroutine 中启动文件监视。
parseHandler 方法用于解析配置文件并调用每个回调函数。当配置文件发生变化时,这个方法会被调用。
总的来说,ConfigMonitor 模块的作用是提供一个中心化的配置管理服务,使得其他模块可以方便地获取和使用配置数据,同时也能够响应配置数据的变化。
四 源代码分析
4.1 文件监听与回调函数触发
文件监听和回调函数触发的流程如下:
1. 通过 NewFileWatcher 函数创建一个新的 FileWatcher 实例。在这个函数中,创建了一个新的 fsnotify.Watcher 实例,并将其赋值给 fileWatcher 结构体的 watcher 字段。
2. 使用 StartWatching 方法开始监视文件变化。在这个方法中,将需要监视的文件路径添加到 fsnotify.Watcher 中,并在一个新的 goroutine 中启动文件监视。
3. 在 start 方法中,处理 fsnotify.Watcher 的事件。当接收到文件写入事件时,会调用 CallOnceAll 方法,该方法会读取文件内容,并将内容传递给所有注册的回调函数。如果接收到文件被删除的事件,则会停止文件监视。
4. CallOnceAll 方法会一次性调用所有的回调函数。首先,它会读取文件的内容,然后遍历 callbacks 字段中的所有回调函数,并将文件内容作为参数传递给这些函数。
5. 如果需要对特定的回调函数进行一次性调用,可以使用 CallOnceSpecific 方法。这个方法接收一个唯一ID作为参数,然后找到对应的回调函数并调用它。
6. 如果需要停止文件监视,可以使用 StopWatching 方法。这个方法会关闭 done 通道,从而停止文件监视。
4.2 配置信息导入与回调函数定义、注册与注销
通过阅读Kitex的option.go部分源码得知,实现了suite接口的struct,都可以通过func WithSuite(suite Suite) Option,调用suite.Options()将配置传入client。
在client/suite中,定义了Options()方法,根据ConfigMonitor中的配置信息,生成对应的Option数组并返回。
每次触发回调函数时,重新读取一边数据存入data字节流,并执行注册的回调函数重新加载配置项。配置项的回调函数定义,以circuitBreak为例,定义onChange函数将回调函数注册到Monitor。
五 结语
通过分析config-file的源码,我们对Kitex的配置项、各个组件的功能有了直观的认识。原理分析深入探讨了文件监听和配置加载的机制,特别是fsnotify和mapstructure库的应用,为动态配置管理提供了技术支撑。模块结构的详细介绍,让我们对Parser、FileWatcher和ConfigMonitor等核心组件的工作方式有了清晰的理解。这些模块相互协作,共同实现了配置文件的读取、解析、监控和更新。源代码分析部分则进一步深入到具体的实现细节,通过文件监听与回调函数的触发流程,以及配置信息的导入和回调函数的定义、注册与注销,展示了配置中心在实际工作中的应用。