引言
最近用 Go 寫後端寫得很開心, 寫篇比較實用的部落格總結下如何通過 Spring Cloud Config Server 管理 Go 程式中的配置. 實現並不複雜, 因此也可以很輕易地推廣到其他語言的程式中.
先來說說為什麼要做集中組態管理. 在單體應用時代組態管理並不是什麼大問題, 一般設定檔就和源碼一起放在代碼倉庫中, 要查看或者修改直接到 conf
目錄裡找就完事兒了. 但到了微服務時代, 服務的數量比過去多了幾十倍, 再到茫茫多的代碼倉庫裡找配置可就沒這麼簡單了. 因此我們需要一個能夠統一查看修改配置, 能夠對配置進資料列版本設定的地方, 這就是配置中心了.
在 Google 上搜尋 "配置中心" 能找到不少不錯的開源軟體, 但大部分都比較重, 並且需要引入特定的用戶端. 這對沒到那麼大規模的中小團隊來說未免太過折騰. 因此反而像 Spring Cloud Config Server 這樣的輕量級配置中心比較適合, 幾分鐘就能跑起來, 而且和配置本身相關的功能也足夠豐富了.
因此我們的架構就像下面這樣:
- Git: 儲存具體的設定檔, 並且負責配置版本管理
- Spring Cloud Config Server: 提供配置的查詢介面
- Go App: 從配置中心載入配置並使用
OK, 下面正式開幹吧.
簡單的搜尋服務
作為示範我們用 Go 寫一個很簡單的搜尋服務. 只要訪問 GET /search?q=<keyword>
服務就會把搜尋引擎查到的結果展示出來. 用 Go 實現只要一個檔案哦 ~
main.go
package mainimport ...func main() { http.HandleFunc("/search", func(w http.ResponseWriter, req *http.Request) { q := req.URL.Query().Get("q") fmt.Fprintf(w, `<iframe width="100%%" height="98%%" scrolling="auto" frameborder="0" src="https://cn.bing.com/search?q=%v">`, q) }) log.Fatal(http.ListenAndServe(":8081", nil))}
接著把服務跑起來:
go run main.go
在瀏覽器中訪問 http://localhost:8081/search?q=golang
很簡單是不是? 但這裡的問題在於我們把 https://cn.bing.com
寫死在了代碼中, 如果要切換搜尋引擎就得重新編譯器, 真的是費時費力. 這時候我們就需要將配置解耦到設定檔中了.
設定檔
我們先在本地建一個設定檔 go-app.yml
app: search_url: https://cn.bing.com/search?q=%v
然後通過 viper 這個比較流行的配置庫載入這個配置
conf/conf.go
package confimport ...func init() { viper.SetConfigName("go-app") viper.AddConfigPath(os.ExpandEnv(`$GOPATH\src\github.com\GotaX\config-server-demo`)) viper.SetConfigType("yaml") if err := viper.ReadInConfig(); err != nil { log.Fatal("Fail to load config", err) }}
現在我們就把搜尋引擎的地址解耦到設定檔中去了
main.go
package mainimport ...func main() { http.HandleFunc("/search", func(w http.ResponseWriter, req *http.Request) { q := req.URL.Query().Get("q") src := fmt.Sprintf(viper.GetString("app.search_url"), q) fmt.Fprintf(w, `<iframe width="100%%" height="98%%" scrolling="auto" frameborder="0" src="%v">`, src) }) log.Fatal(http.ListenAndServe(":8081", nil))}
轉移配置到雲端
接下來我們將設定檔從本地轉移到 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
package com.example.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.config.server.EnableConfigServer;@EnableConfigServer // 添加這行@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}
修改 application.yml, 填入我們存放設定檔的倉庫地址
spring.cloud.config.server.git.uri: https://github.com/GotaX/config-server-demo.git
在工程根目錄啟動 config server
gradle bootrun
訪問 http://localhost:8080/config/go-app-default.yml
查看配置
app: search_url: https://cn.bing.com/search?q=%v
這樣我們的配置中心就啟動完畢了
在 Go 應用中讀取配置
最後就是在應用中使用 Spring Cloud Config Server 中的配置了. 如果是基於 Spring Boot 的應用可以直接使用 spring-cloud-config-client
載入配置. 在 Go 中就需要稍微寫點代碼了, 不過並不多.
我們先在 config.go 中添加一個 loadRemote()
函數, 用來從配置中心讀取配置
conf/conf.go
// ...const ( kAppName = "APP_NAME" kConfigServer = "CONFIG_SERVER" kConfigLabel = "CONFIG_LABEL" kConfigProfile = "CONFIG_PROFILE" kConfigType = "CONFIG_TYPE")func loadRemoteConfig() (err error) { // 組裝設定檔地址: http://localhost:8080/config/go-app-default.yaml confAddr := fmt.Sprintf("%v/%v/%v-%v.yml", viper.Get(kConfigServer), viper.Get(kConfigLabel), viper.Get(kAppName), viper.Get(kConfigProfile)) resp, err := http.Get(confAddr) if err != nil { return } defer resp.Body.Close() // 設定設定檔格式: yaml viper.SetConfigType(viper.GetString(kConfigType)) // 載入設定檔 if err = viper.ReadConfig(resp.Body); err != nil { return } log.Println("Load config from: ", confAddr) return}
當然, 我們需要知道配置中心的入口, 因此還需要一個 initDefault()
函數來初始化這些配置
conf/conf.go
func initDefault() { viper.SetDefault(kAppName, "go-app") viper.SetDefault(kConfigServer, "http://localhost:8080") viper.SetDefault(kConfigLabel, "config") viper.SetDefault(kConfigProfile, "default") viper.SetDefault(kConfigType, "yaml")}
現在我們的 init()
函數變成了這樣
conf/conf.go
func init() { viper.AutomaticEnv() initDefault() if err := loadRemoteConfig(); err != nil { log.Fatal("Fail to load config", err) }}
其中的 viper.AutomaticEnv()
可以讓我們通過環境變數修改任意配置, 因此 initDefault()
中的配置也不是寫死在代碼中的了. 其中比較常見的用法是通過 CONFIG_PROFILE=prod
環境變數來切換 profile
最後我們希望 viper 僅在 conf 包中出現, 而對外隱藏我們載入配置的具體實現. 因此我們將配置讀到結構體中再對外提供.
conf/conf.go
var App AppConfigtype AppConfig struct { SearchUrl string `mapstructure:"search_url"`}func init() { // ... if err := sub("app", &App); err != nil { log.Fatal("Fail to parse config", err) }}func sub(key string, value interface{}) error { sub := viper.Sub(key) sub.AutomaticEnv() sub.SetEnvPrefix(key) return sub.Unmarshal(value)}
這時我們就可以從 main.go 中去掉 viper.Get()
調用了
main.go
import ...func main() { http.HandleFunc("/search", func(w http.ResponseWriter, req *http.Request) { q := req.URL.Query().Get("q") src := fmt.Sprintf(conf.App.SearchUrl, q) fmt.Fprintf(w, `<iframe width="100%%" height="98%%" scrolling="auto" frameborder="0" src="%v">`, src) }) log.Fatal(http.ListenAndServe(":8081", nil))}
總結
我們通過 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 應用優雅退出
有機會的話下次再寫, 或者也可以直接參考 Spring Cloud Config Server 的官方文檔
如果你有疑問或者更好的想法, 實踐, 歡迎留言討論