最近用 Go 写后端写得很开心,写篇比较实用的博客总结下如何通过 Spring Cloud Config Server 管理 Go 程序中的配置。 实现并不复杂,因此也可以很轻易地推广到其他语言的程序中。
conf在 Google 上搜索 "配置中心" 能找到不少不错的开源软件,但大部分都比较重,并且需要引入特定的客户端。这对没到那么大规模的中小团队来说未免太过折腾,因此反而像 Spring Cloud Config Server 这样的轻量级配置中心比较适合,几分钟就能跑起来, 而且和配置本身相关的功能也足够丰富了。
因此我们的架构就像下面这样:
- Git: 储存具体的配置文件, 并且负责配置版本管理
- Spring Cloud Config Server:提供配置的查询接口
- Go App:从配置中心载入配置并使用
GET /search?q=main.go
1package main
2
3import ...
4
5func main() {
6 http.HandleFunc("/search", func(w http.ResponseWriter, req *http.Request) {
7 q := req.URL.Query().Get("q")
8 fmt.Fprintf(w, `<iframe width="100%%" height="98%%" scrolling="auto" frameborder="0" src="https://cn.bing.com/search?q=%v">`, q)
9 })
10 log.Fatal(http.ListenAndServe(":8081", nil))
11}接着把服务跑起来:
1go run main.gohttp://localhost:8081/search?q=golanghttps://cn.bing.com配置文件go-app.yml1app:
2 search_url: https://cn.bing.com/search?q=%v然后通过 viper 这个比较流行的配置库加载这个配置。
conf/conf.go
1package conf
2
3import ...
4
5func init() {
6 viper.SetConfigName("go-app")
7 viper.AddConfigPath(os.ExpandEnv(`$GOPATH\src\github.com\GotaX\config-server-demo`))
8 viper.SetConfigType("yaml")
9 if err := viper.ReadInConfig(); err != nil {
10 log.Fatal("Fail to load config", err)
11 }
12}现在我们就把搜索引擎的地址解耦到配置文件中去了。
main.go
1package main
2
3import ...
4
5func main() {
6 http.HandleFunc("/search", func(w http.ResponseWriter, req *http.Request) {
7 q := req.URL.Query().Get("q")
8 src := fmt.Sprintf(viper.GetString("app.search_url"), q)
9 fmt.Fprintf(w, `<iframe width="100%%" height="98%%" scrolling="auto" frameborder="0" src="%v">`, src)
10 })
11 log.Fatal(http.ListenAndServe(":8081", nil))
12}转移配置到云端接下来我们将配置文件从本地转移到 Git 中,处于方便我就直接放在当前仓库的 config 分支中了。
地址为: https://github.com/GotaX/config-server-demo/tree/config
启动配置中心配置文件上传完毕,我们再新开一个 config-server 空分支搭建配置中心。
首先到 https://start.spring.io/ 页面新建一个 Java + Gradle 的 Spring Boot 工程,依赖项选 Config Server。
点击 "Generate Project" 将下载压缩包, 并解压。
修改 Application.java
1package com.example.demo;
2
3import org.springframework.boot.SpringApplication;
4import org.springframework.boot.autoconfigure.SpringBootApplication;
5import org.springframework.cloud.config.server.EnableConfigServer;
6
7@EnableConfigServer // 添加这行
8@SpringBootApplication
9public class DemoApplication {
10 public static void main(String[] args) {
11 SpringApplication.run(DemoApplication.class, args);
12 }
13}修改 application.yml, 填入我们存放配置文件的仓库地址。
1spring.cloud.config.server.git.uri: https://github.com/GotaX/config-server-demo.git在工程根目录启动 config server。
1gradle bootrunhttp://localhost:8080/config/go-app-default.yml1app:
2 search_url: https://cn.bing.com/search?q=%v这样我们的配置中心就启动完毕了。
在 Go 应用中读取配置spring-cloud-config-clientloadRemote()conf/conf.go
1// ...
2const (
3 kAppName = "APP_NAME"
4 kConfigServer = "CONFIG_SERVER"
5 kConfigLabel = "CONFIG_LABEL"
6 kConfigProfile = "CONFIG_PROFILE"
7 kConfigType = "CONFIG_TYPE"
8)
9
10func loadRemoteConfig() (err error) {
11 // 组装配置文件地址: http://localhost:8080/config/go-app-default.yaml
12 confAddr := fmt.Sprintf("%v/%v/%v-%v.yml",
13 viper.Get(kConfigServer), viper.Get(kConfigLabel),
14 viper.Get(kAppName), viper.Get(kConfigProfile))
15 resp, err := http.Get(confAddr)
16 if err != nil {
17 return
18 }
19 defer resp.Body.Close()
20
21 // 设置配置文件格式: yaml
22 viper.SetConfigType(viper.GetString(kConfigType))
23 // 载入配置文件
24 if err = viper.ReadConfig(resp.Body); err != nil {
25 return
26 }
27 log.Println("Load config from: ", confAddr)
28 return
29}initDefault()conf/conf.go
1func initDefault() {
2 viper.SetDefault(kAppName, "go-app")
3 viper.SetDefault(kConfigServer, "http://localhost:8080")
4 viper.SetDefault(kConfigLabel, "config")
5 viper.SetDefault(kConfigProfile, "default")
6 viper.SetDefault(kConfigType, "yaml")
7}init()conf/conf.go
1func init() {
2 viper.AutomaticEnv()
3 initDefault()
4
5 if err := loadRemoteConfig(); err != nil {
6 log.Fatal("Fail to load config", err)
7 }
8}viper.AutomaticEnv()initDefault()CONFIG_PROFILE=prod最后我们希望 viper 仅在 conf 包中出现, 而对外隐藏我们加载配置的具体实现。 因此我们将配置读到结构体中再对外提供:
conf/conf.go
1var App AppConfig
2
3type AppConfig struct {
4 SearchUrl string `mapstructure:"search_url"`
5}
6
7func init() {
8 // ...
9 if err := sub("app", &App); err != nil {
10 log.Fatal("Fail to parse config", err)
11 }
12}
13
14func sub(key string, value interface{}) error {
15 sub := viper.Sub(key)
16 sub.AutomaticEnv()
17 sub.SetEnvPrefix(key)
18 return sub.Unmarshal(value)
19}viper.Get()main.go
1import ...
2
3func main() {
4 http.HandleFunc("/search", func(w http.ResponseWriter, req *http.Request) {
5 q := req.URL.Query().Get("q")
6 src := fmt.Sprintf(conf.App.SearchUrl, q)
7 fmt.Fprintf(w, `<iframe width="100%%" height="98%%" scrolling="auto" frameborder="0" src="%v">`, src)
8 })
9 log.Fatal(http.ListenAndServe(":8081", nil))
10}总结我们通过 Git + Spring Could Config Server + Viper + 少量 Go 代码, 实现了基于配置中心的配置管理及使用
我们甚至可以在 Go 中使用类似于 Spring Boot 的 Profile 管理, 对比下:
- http://localhost:8080/config/go-app-default.yml
- http://localhost:8080/config/go-app-prod.yml
完整的代码可以参考 https://github.com/GotaX/config-server-demo 下的 3 个分支:
- config: 配置文件
- config-server: 配置中心
- app: Go 应用
当然, 目前这种使用方式还比较简陋, 还有很多可以改进的地方, 比如:
- 结合 Spring Cloud Bus 实现配置的实时推送
- 结合 Spring Cloud Eureka 实现配置服务器的高可用
- 监听 SIGINT 和 SIGTERM 实现 Go 应用优雅退出