使用 Elastic Stack 來監控和調優 Golang 應用程式

astaxie發表於2017-03-05

Golang 因為其語法簡單,上手快且方便部署正被越來越多的開發者所青睞,一個 Golang 程式開發好了之後,勢必要關心其執行情況,今天在這裡就給大家介紹一下如果使用 Elastic Stack 來分析 Golang 程式的記憶體使用情況,方便對 Golang 程式做長期監控進而調優和診斷,甚至發現一些潛在的記憶體洩露等問題。

Elastic Stack 其實是一個集合,包含 Elasticsearch、Logstash 和 Beats 這幾個開源軟體,而 Beats 又包含 Filebeat、Packetbeat、Winlogbeat、Metricbeat 和新出的 Heartbeat,呵呵,有點多吧,恩,每個 beat 做的事情不一樣,沒關係,今天主要用到 Elasticsearch、Metricbeat 和 Kibana 就行了。

Metricbeat 是一個專門用來獲取伺服器或應用服務內部執行指標資料的收集程式,也是 Golang 寫的,部署包比較小才10M 左右,對目標伺服器的部署環境也沒有依賴,記憶體資源佔用和 CPU 開銷也較小,目前除了可以監控伺服器本身的資源使用情況外,還支援常見的應用伺服器和服務,目前支援列表如下:

  • Apache Module
  • Couchbase Module
  • Docker Module
  • HAProxy Module
  • kafka Module
  • MongoDB Module
  • MySQL Module
  • Nginx Module
  • PostgreSQL Module
  • Prometheus Module
  • Redis Module
  • System Module
  • ZooKeeper Module

當然,也有可能你的應用不在上述列表,沒關係,Metricbeat 是可以擴充套件的,你可以很方便的實現一個模組,而本文接下來所用的 Golang Module 也就是我剛剛為 Metricbeat 新增的擴充套件模組,目前已經 merge 進入 Metricbeat 的 master 分支,預計會在 6.0 版本釋出,想了解是如何擴充套件這個模組的可以檢視 程式碼路徑 和 PR地址。

上面的這些可能還不夠吸引人,我們來看一下 Kibana 對 Metricbeat 使用 Golang 模組收集的資料進行的視覺化分析吧:

上面的圖簡單解讀一下: 最上面一欄是 Golang Heap 的摘要資訊,可以大致瞭解 Golang 的記憶體使用和 GC 情況,System 表示 Golang 程式從作業系統申請的記憶體,可以理解為程式所佔的記憶體(注意不是程式對應的虛擬記憶體),Bytes allocated 表示 Heap 目前分配的記憶體,也就是 Golang 裡面直接可使用的記憶體,GC limit 表示當 Golang 的 Heap 記憶體分配達到這個 limit 值之後就會開始執行 GC,這個值會隨著每次 GC 而變化, GC cycles 則代表監控週期內的 GC 次數;

中間的三列分別是堆記憶體、程式記憶體和物件的統計情況;Heap Allocated 表示正在用和沒有用但還未被回收的物件的大小;Heap Inuse 顯然就是活躍的物件大小了;Heap Idle 表示已分配但空閒的記憶體;

底下兩列是 GC 時間和 GC 次數的監控統計,CPUFraction 這個代表該程式 CPU 佔用時間花在 GC 上面的百分比,值越大說明 GC 越頻繁,浪費更多的時間在 GC 上面,上圖雖然趨勢陡峭,但是看範圍在0.41%~0.52%之間,看起來還算可以,如果GC 比率佔到個位數甚至更多比例,那肯定需要進一步優化程式了。

有了這些資訊我們就能夠知道該 Golang 的記憶體使用和分配情況和 GC 的執行情況,假如要分析是否有記憶體洩露,看記憶體使用和堆記憶體分配的趨勢是否平穩就可以了,另外 GC_Limit 和 Byte Allocation 一直上升,那肯定就是有記憶體洩露了,結合歷史資訊還能對不同版本/提交對 Golang 的記憶體使用和 GC 影響進行分析。

接下來就要給大家介紹如何具體使用了,首先需要啟用 Golang 的 expvar 服務,expvar(https://golang.org/pkg/expvar/) 是 Golang 提供的一個暴露內部變數或統計資訊的標準包。 使用的方法很簡單,只需要在 Golang 的程式引入該包即可,它會自動註冊現有的 http 服務上,如下:

import _ "expvar"

如果 Golang 沒有啟動 http 服務,使用下面的方式啟動一個即可,這裡埠是 6060,如下:

func metricsHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")

    first := true
    report := func(key string, value interface{}) {
        if !first {
            fmt.Fprintf(w, ",\n")
        }
        first = false
        if str, ok := value.(string); ok {
            fmt.Fprintf(w, "%q: %q", key, str)
        } else {
            fmt.Fprintf(w, "%q: %v", key, value)
        }
    }

    fmt.Fprintf(w, "{\n")
    monitoring.Do(monitoring.Full, report)
    expvar.Do(func(kv expvar.KeyValue) {
        report(kv.Key, kv.Value)
    })
    fmt.Fprintf(w, "\n}\n")
}

func main() {
   mux := http.NewServeMux()
   mux.HandleFunc("/debug/vars", metricsHandler)
   endpoint := http.ListenAndServe("localhost:6060", mux)
}

預設註冊的訪問路徑是/debug/vars, 編譯啟動之後,就可以通過 http://localhost:6060/debug/vars 來訪問 expvar 以 JSON 格式暴露出來的這些內部變數,預設提供了 Golang 的 runtime.Memstats 資訊,也就是上面分析的資料來源,當然你還可以註冊自己的變數,這裡暫時不提。

OK,現在我們的 Golang 程式已經啟動了,並且通過 expvar 暴露出了執行時的記憶體使用情況,現在我們需要使用 Metricbeat 來獲取這些資訊並存進 Elasticsearch。

關於 Metricbeat 的安裝其實很簡單,下載對應平臺的包解壓(下載地址:https://www.elastic.co/downloads/beats/metricbeat ),啟動 Metricbeat 前,修改配置檔案:metricbeat.yml

metricbeat.modules:
  - module: golang
     metricsets: ["heap"]
     enabled: true
     period: 10s
     hosts: ["localhost:6060"]
     heap.path: "/debug/vars"

上面的引數啟用了 Golang 監控模組,並且會10秒獲取一次配置路徑的返回記憶體資料,我們同樣配置該配置檔案,設定資料輸出到本機的 Elasticsearch:

output.elasticsearch:
  hosts: ["localhost:9200"]

現在啟動 Metricbeat:

./metricbeat -e -v

現在在 Elasticsearch 應該就有資料了,當然記得確保 Elasticsearch 和 Kibana 是可用狀態,你可以在 Kibana 根據資料靈活自定義視覺化,推薦使用 Timelion 來進行分析,當然為了方便也可以直接匯入提供的樣例儀表板,就是上面第一個圖的效果。 關於如何匯入樣例儀表板請參照這個文件:https://www.elastic.co/guide/e ... .html

除了監控已經有的記憶體資訊之外,如果你還有一些內部的業務指標想要暴露出來,也是可以的,通過 expvar 來做同樣可以。一個簡單的例子如下:

var inerInt int64 = 1024
pubInt := expvar.NewInt("your_metric_key")
pubInt.Set(inerInt)
pubInt.Add(2)

在 Metricbeat 內部也同樣暴露了很多內部執行的資訊,所以 Metricbeat 可以自己監控自己了。。。 首先,啟動的時候帶上引數設定pprof監控的地址,如下:

./metricbeat -httpprof="127.0.0.1:6060" -e -v

這樣我們就能夠通過 http://127.0.0.1:6060/debug/vars">http://127.0.0.1:6060/debug/vars]http://127.0.0.1:6060/debug/vars 訪問到內部執行情況了,如下:

{
"output.events.acked": 1088,
"output.write.bytes": 1027455,
"output.write.errors": 0,
"output.messages.dropped": 0,
"output.elasticsearch.publishEvents.call.count": 24,
"output.elasticsearch.read.bytes": 12215,
"output.elasticsearch.read.errors": 0,
"output.elasticsearch.write.bytes": 1027455,
"output.elasticsearch.write.errors": 0,
"output.elasticsearch.events.acked": 1088,
"output.elasticsearch.events.not_acked": 0,
"output.kafka.events.acked": 0,
"output.kafka.events.not_acked": 0,
"output.kafka.publishEvents.call.count": 0,
"output.logstash.write.errors": 0,
"output.logstash.write.bytes": 0,
"output.logstash.events.acked": 0,
"output.logstash.events.not_acked": 0,
"output.logstash.publishEvents.call.count": 0,
"output.logstash.read.bytes": 0,
"output.logstash.read.errors": 0,
"output.redis.events.acked": 0,
"output.redis.events.not_acked": 0,
"output.redis.read.bytes": 0,
"output.redis.read.errors": 0,
"output.redis.write.bytes": 0,
"output.redis.write.errors": 0,
"beat.memstats.memory_total": 155721720,
"beat.memstats.memory_alloc": 3632728,
"beat.memstats.gc_next": 6052800,
"cmdline": ["./metricbeat","-httpprof=127.0.0.1:6060","-e","-v"],
"fetches": {"system-cpu": {"events": 4, "failures": 0, "success": 4}, "system-filesystem": {"events": 20, "failures": 0, "success": 4}, "system-fsstat": {"events": 4, "failures": 0, "success": 4}, "system-load": {"events": 4, "failures": 0, "success": 4}, "system-memory": {"events": 4, "failures": 0, "success": 4}, "system-network": {"events": 44, "failures": 0, "success": 4}, "system-process": {"events": 1008, "failures": 0, "success": 4}},
"libbeat.config.module.running": 0,
"libbeat.config.module.starts": 0,
"libbeat.config.module.stops": 0,
"libbeat.config.reloads": 0,
"memstats": {"Alloc":3637704,"TotalAlloc":155
... ...

比如,上面就能看到output模組Elasticsearch的處理情況,如 output.elasticsearch.events.acked 參數列示傳送到 Elasticsearch Ack返回之後的訊息。

現在我們要修改 Metricbeat 的配置檔案,Golang 模組有兩個 metricset,可以理解為兩個監控的指標型別,我們現在需要加入一個新的 expvar 型別,這個即自定義的其他指標,相應配置檔案修改如下:

- module: golang
  metricsets: ["heap","expvar"]
  enabled: true
  period: 1s
  hosts: ["localhost:6060"]
  heap.path: "/debug/vars"
  expvar:
    namespace: "metricbeat"
    path: "/debug/vars"

上面的一個引數 namespace 表示自定義指標的一個命令空間,主要是為了方便管理,這裡是 Metricbeat 自身的資訊,所以 namespace 就是 metricbeat。

重啟 Metricbeat 應該就能收到新的資料了,我們前往 Kibana。

這裡假設關注 output.elasticsearch.events.acked和 output.elasticsearch.events.not_acked這兩個指標,我們在Kibana裡面簡單定義一個曲線圖就能看到 Metricbeat 發往 Elasticsearch 訊息的成功和失敗趨勢。 Timelion 表示式:

.es("metricbeat*",metric="max:golang.metricbeat.output.elasticsearch.events.acked").derivative().label("Elasticsearch Success"),.es("metricbeat*",metric="max:golang.metricbeat.output.elasticsearch.events.not_acked").derivative().label("Elasticsearch Failed")

效果如下:

從上圖可以看到,發往 Elasticsearch 的訊息很穩定,沒有出現丟訊息的情況,同時關於 Metricbeat 的記憶體情況,我們開啟匯入的 Dashboard 檢視:

關於如何使用 Metricbeat 來監控 Golang 應用程式的內容基本上就差不多到這裡了,上面介紹瞭如何基於 expvar 來監控 Golang 的記憶體情況和自定義業務監控指標,在結合 Elastic Stack 可以快速的進行分析,希望對大家有用。

最後,這個 Golang 模組目前還沒 release,估計在 beats 6.0 釋出,有興趣嚐鮮的可以自己下載原始碼打包。

相關文章