使用 expvar 暴露 Go 程序運行指標
獲取應用程序的運行指標,可以讓我們更好地了解它的實際狀況。將這些指標對接到 prometheus、zabbix 等監控系統,能夠對應用程序持續檢測,發現異常可以及時告警并得到處理。
Pull 與 Push
與監控系統對接方式有兩種,一種是 Pull(拉取),另外一種 Push(推送)。
以 Prometheus 為例,應用程序通過暴露出 HTTP 接口,讓 Prometheus 周期性地通過該接口抓取指標,這就是 Pull。而 Push 是應用程序主動將指標推送給 PushGateway, Prometheus 則去 PushGateway 抓取數據。
Go 標準庫中有一個名為 expvar 的包,它的名字由 exp 和 var 兩部分組合而成,意味著導出變量。
expvar 為公共變量提供了標準化的接口,并通過 HTTP 以 Json 的格式將這些變量暴露出去,很適合采用 Pull 的方式與監控系統進行對接。
使用 expvar 庫
expvar 是標準庫,意味著我們并不要額外的依賴,并且它還提供了一些開箱即用的指標。下面我們來學習一下該庫的使用。
當引用了 expvar 庫(import "expvar"),以下 init 函數將被自動調用。
func init() {
http.HandleFunc("/debug/vars", expvarHandler)
Publish("cmdline", Func(cmdline))
Publish("memstats", Func(memstats))
}
該函數為我們注冊了 /debug/vars 路徑的 HTTP 服務,訪問該路徑將得到 Json 格式的指標。
因此,我們還需要調用 ListenAndServe 綁定端口,并開始服務 HTTP 請求。
http.ListenAndServe(":8080", nil)
完整代碼如下
package main
import (
_ "expvar"
"net/http"
)
func main() {
http.ListenAndServe(":8080", nil)
}
將程序運行起來后,通過 curl 請求,得到以下結果
$ curl localhost:8080/debug/vars
{
"cmdline": ["/var/folders/xk/gn46n46d503dsztbc6_9qb2h0000gn/T/go-build1657217338/b001/exe/main"],
"memstats": {"Alloc":278880,"TotalAlloc":278880,"Sys":8735760,"Lookups":0,"Mallocs":1169,"Frees":87,"HeapAlloc":278880,"HeapSys":3866624,"HeapIdle":2949120,"HeapInuse":917504,"HeapReleased":2899968,"HeapObjects":1082,"StackInuse":327680,"StackSys":327680,"MSpanInuse":28696,"MSpanSys":32640,"MCacheInuse":9600,"MCacheSys":15600,"BuckHashSys":3875,"GCSys":3826448,"OtherSys":662893,"NextGC":4194304,"LastGC":0,"PauseTotalNs":0,"PauseNs":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"PauseEnd":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"NumGC":0,"NumForcedGC":0,"GCCPUFraction":0,"EnableGC":true,"DebugGC":false,"BySize":[{"Size":0,"Mallocs":0,"Frees":0},{"Size":8,"Mallocs":41,"Frees":0},{"Size":16,"Mallocs":496,"Frees":0},{"Size":24,"Mallocs":63,"Frees":0},{"Size":32,"Mallocs":28,"Frees":0},{"Size":48,"Mallocs":134,"Frees":0},{"Size":64,"Mallocs":50,"Frees":0},{"Size":80,"Mallocs":17,"Frees":0},{"Size":96,"Mallocs":17,"Frees":0},{"Size":112,"Mallocs":6,"Frees":0},{"Size":128,"Mallocs":9,"Frees":0},{"Size":144,"Mallocs":9,"Frees":0},{"Size":160,"Mallocs":18,"Frees":0},{"Size":176,"Mallocs":6,"Frees":0},{"Size":192,"Mallocs":0,"Frees":0},{"Size":208,"Mallocs":37,"Frees":0},{"Size":224,"Mallocs":6,"Frees":0},{"Size":240,"Mallocs":0,"Frees":0},{"Size":256,"Mallocs":12,"Frees":0},{"Size":288,"Mallocs":7,"Frees":0},{"Size":320,"Mallocs":2,"Frees":0},{"Size":352,"Mallocs":13,"Frees":0},{"Size":384,"Mallocs":1,"Frees":0},{"Size":416,"Mallocs":30,"Frees":0},{"Size":448,"Mallocs":1,"Frees":0},{"Size":480,"Mallocs":2,"Frees":0},{"Size":512,"Mallocs":0,"Frees":0},{"Size":576,"Mallocs":5,"Frees":0},{"Size":640,"Mallocs":5,"Frees":0},{"Size":704,"Mallocs":3,"Frees":0},{"Size":768,"Mallocs":0,"Frees":0},{"Size":896,"Mallocs":6,"Frees":0},{"Size":1024,"Mallocs":8,"Frees":0},{"Size":1152,"Mallocs":10,"Frees":0},{"Size":1280,"Mallocs":3,"Frees":0},{"Size":1408,"Mallocs":1,"Frees":0},{"Size":1536,"Mallocs":0,"Frees":0},{"Size":1792,"Mallocs":7,"Frees":0},{"Size":2048,"Mallocs":2,"Frees":0},{"Size":2304,"Mallocs":3,"Frees":0},{"Size":2688,"Mallocs":2,"Frees":0},{"Size":3072,"Mallocs":0,"Frees":0},{"Size":3200,"Mallocs":0,"Frees":0},{"Size":3456,"Mallocs":0,"Frees":0},{"Size":4096,"Mallocs":8,"Frees":0},{"Size":4864,"Mallocs":1,"Frees":0},{"Size":5376,"Mallocs":1,"Frees":0},{"Size":6144,"Mallocs":2,"Frees":0},{"Size":6528,"Mallocs":0,"Frees":0},{"Size":6784,"Mallocs":0,"Frees":0},{"Size":6912,"Mallocs":0,"Frees":0},{"Size":8192,"Mallocs":2,"Frees":0},{"Size":9472,"Mallocs":8,"Frees":0},{"Size":9728,"Mallocs":0,"Frees":0},{"Size":10240,"Mallocs":0,"Frees":0},{"Size":10880,"Mallocs":0,"Frees":0},{"Size":12288,"Mallocs":0,"Frees":0},{"Size":13568,"Mallocs":0,"Frees":0},{"Size":14336,"Mallocs":0,"Frees":0},{"Size":16384,"Mallocs":0,"Frees":0},{"Size":18432,"Mallocs":0,"Frees":0}]}
}
可以看到,expvar 默認已提供了兩項指標,分別是程序執行命令(os.Args)和運行時內存分配(runtime.Memstats)信息。
expvar 庫重點內容
expvar 中最重要的是 Publish 函數和 Var 接口。
func Publish(name string, v Var) {}
Publish 函數簽名中需要兩個參數,name 是我們指定的指標名。例如在上文 expvar 的 init 函數下 Publish("cmdline", Func(cmdline))代碼行,其中cmdline就是指標名,而Func(cmdline)則是實現了 Var 接口的 expvar.Func 類型變量。
Var 接口,它只定義了一個 String 方法。需要注意的是,該方法必須要返回一個有效的 Json字符串。
// Var is an abstract type for all exported variables.
type Var interface {
// String returns a valid JSON value for the variable.
// Types with String methods that do not return valid JSON
// (such as time.Time) must not be used as a Var.
String() string
}
為了方便使用,expvar 庫中提供了五種導出變量類型,它們均實現了 Var 接口。
type Int struct {
i int64
}
type Float struct {
f uint64
}
type Map struct {
m sync.Map // map[string]Var
keysMu sync.RWMutex
keys []string // sorted
}
type String struct {
s atomic.Value // string
}
type Func func() any
我們分別通過調用 expvar.NewXXX 函數即可完成前四種類型的變量創建與指標注冊。
intVar = expvar.NewInt(“metricName”) floatVar = expvar.NewFloat(“metricName”) mapVar = expvar.NewMap(“metricName”) stringVar = expvar.NewString(“metricName”)
例如 expvar.NewInt 函數,它會內部調用 Publish 方法完成指標名與 expvar.Int 類型變量的綁定。
func NewInt(name string) *Int {
v := new(Int)
Publish(name, v)
return v
}
而 expvar.Func 類型,其實是為了讓我們可以自定義導出類型。
例如,假如我們想要暴露以下定義的結構體
type MyStruct struct {
Field1 string
Field2 int
Field3 float64
}
首先需要創建一個數據生成函數。它用于在每次調用 HTTP 服務路徑時,通過該函數導出這里面的數據。
func MyStructData() interface{} {
return MyStruct{
Field1: "Gopher",
Field2: 22,
Field3: 19.99,
}
}
最后,通過 Publish 函數注冊指標名即可。
expvar.Publish("metricName", expvar.Func(MyStructData))
完整示例
下面,我們給出一個覆蓋五種導出變量類型的完整示例。
package main
import (
"expvar"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/load"
"github.com/shirou/gopsutil/v3/mem"
"net/http"
"time"
)
type Load struct {
Load1 float64
Load5 float64
Load15 float64
}
func AllLoadAvg() interface{} {
return Load{
Load1: LoadAvg(1),
Load5: LoadAvg(5),
Load15: LoadAvg(15),
}
}
func LoadAvg(loadNumber int) float64 {
avg, _ := load.Avg()
switch loadNumber {
case 5:
return avg.Load5
case 15:
return avg.Load15
default:
return avg.Load1
}
}
func main() {
var (
aliveOfSeconds = expvar.NewInt("aliveOfSeconds")
hostID = expvar.NewString("hostID")
lastLoad = expvar.NewFloat("lastLoad")
virtualMemory = expvar.NewMap("virtualMemory")
)
expvar.Publish("allLoadAvg", expvar.Func(AllLoadAvg))
h, _ := host.HostID()
hostID.Set(h)
go http.ListenAndServe(":8080", nil)
for {
aliveOfSeconds.Add(1)
vm, _ := mem.VirtualMemory()
lastLoad.Set(LoadAvg(1))
virtualMemory.Add("active", int64(vm.Active))
virtualMemory.Add("buffer", int64(vm.Buffers))
time.Sleep(1 * time.Second)
}
}
在上述示例中,我們通過 gopsutil 庫(介紹可見還在自己寫 Go 系統監控函數嗎一文)獲取了一些系統信息,并展示了如何通過 expvar 中的各種變量類型將這些信息進行導出。
curl 訪問 localhost:8080/debug/vars 結果如下
$ curl localhost:8080/debug/vars
{
"aliveOfSeconds": 1,
"allLoadAvg": {"Load1":1.69580078125,"Load5":1.97412109375,"Load15":1.90283203125},
"cmdline": ["/var/folders/xk/gn46n46d503dsztbc6_9qb2h0000gn/T/go-build3566019824/b001/exe/main"],
"hostID": "7a1a74f2-30fc-5bc1-b439-6b7aef22e58d",
"lastLoad": 1.69580078125,
"memstats": {"Alloc":256208,"TotalAlloc":256208,"Sys":8735760,"Lookups":0,"Mallocs":1089,"Frees":48,"HeapAlloc":256208,"HeapSys":3866624,"HeapIdle":2891776,"HeapInuse":974848,"HeapReleased":2859008,"HeapObjects":1041,"StackInuse":327680,"StackSys":327680,"MSpanInuse":19992,"MSpanSys":32640,"MCacheInuse":9600,"MCacheSys":15600,"BuckHashSys":3905,"GCSys":3851120,"OtherSys":638191,"NextGC":4194304,"LastGC":0,"PauseTotalNs":0,"PauseNs":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"PauseEnd":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"NumGC":0,"NumForcedGC":0,"GCCPUFraction":0,"EnableGC":true,"DebugGC":false,"BySize":[{"Size":0,"Mallocs":0,"Frees":0},{"Size":8,"Mallocs":35,"Frees":0},{"Size":16,"Mallocs":415,"Frees":0},{"Size":24,"Mallocs":71,"Frees":0},{"Size":32,"Mallocs":37,"Frees":0},{"Size":48,"Mallocs":141,"Frees":0},{"Size":64,"Mallocs":52,"Frees":0},{"Size":80,"Mallocs":20,"Frees":0},{"Size":96,"Mallocs":23,"Frees":0},{"Size":112,"Mallocs":14,"Frees":0},{"Size":128,"Mallocs":7,"Frees":0},{"Size":144,"Mallocs":7,"Frees":0},{"Size":160,"Mallocs":18,"Frees":0},{"Size":176,"Mallocs":6,"Frees":0},{"Size":192,"Mallocs":1,"Frees":0},{"Size":208,"Mallocs":42,"Frees":0},{"Size":224,"Mallocs":3,"Frees":0},{"Size":240,"Mallocs":0,"Frees":0},{"Size":256,"Mallocs":9,"Frees":0},{"Size":288,"Mallocs":8,"Frees":0},{"Size":320,"Mallocs":5,"Frees":0},{"Size":352,"Mallocs":13,"Frees":0},{"Size":384,"Mallocs":3,"Frees":0},{"Size":416,"Mallocs":33,"Frees":0},{"Size":448,"Mallocs":0,"Frees":0},{"Size":480,"Mallocs":2,"Frees":0},{"Size":512,"Mallocs":1,"Frees":0},{"Size":576,"Mallocs":4,"Frees":0},{"Size":640,"Mallocs":8,"Frees":0},{"Size":704,"Mallocs":3,"Frees":0},{"Size":768,"Mallocs":1,"Frees":0},{"Size":896,"Mallocs":6,"Frees":0},{"Size":1024,"Mallocs":8,"Frees":0},{"Size":1152,"Mallocs":9,"Frees":0},{"Size":1280,"Mallocs":3,"Frees":0},{"Size":1408,"Mallocs":1,"Frees":0},{"Size":1536,"Mallocs":1,"Frees":0},{"Size":1792,"Mallocs":9,"Frees":0},{"Size":2048,"Mallocs":1,"Frees":0},{"Size":2304,"Mallocs":2,"Frees":0},{"Size":2688,"Mallocs":2,"Frees":0},{"Size":3072,"Mallocs":0,"Frees":0},{"Size":3200,"Mallocs":1,"Frees":0},{"Size":3456,"Mallocs":0,"Frees":0},{"Size":4096,"Mallocs":5,"Frees":0},{"Size":4864,"Mallocs":0,"Frees":0},{"Size":5376,"Mallocs":1,"Frees":0},{"Size":6144,"Mallocs":1,"Frees":0},{"Size":6528,"Mallocs":0,"Frees":0},{"Size":6784,"Mallocs":0,"Frees":0},{"Size":6912,"Mallocs":0,"Frees":0},{"Size":8192,"Mallocs":1,"Frees":0},{"Size":9472,"Mallocs":8,"Frees":0},{"Size":9728,"Mallocs":0,"Frees":0},{"Size":10240,"Mallocs":0,"Frees":0},{"Size":10880,"Mallocs":0,"Frees":0},{"Size":12288,"Mallocs":0,"Frees":0},{"Size":13568,"Mallocs":0,"Frees":0},{"Size":14336,"Mallocs":0,"Frees":0},{"Size":16384,"Mallocs":0,"Frees":0},{"Size":18432,"Mallocs":0,"Frees":0}]},
"virtualMemory": {"active": 1957449728, "buffer": 0}
}
總結
標準庫 expvar 為需要導出的公共變量提供了一個標準化的接口,使用比較簡單。
expvar 包內部定義的幾種基礎類型都相應給出了并發安全的操作方法,我們不需要去重復實現一遍,能夠開箱即用。
但是, 在 https://go.dev/ 上統計的公共項目,該庫的 import 數量還不足 1萬。


相比于其他標準庫的 import 數量而言,expvar 的存在感太低了。
所以問題來了,你認為是什么原因導致 expvar 庫這么冷門呢?歡迎留言討論。