2017年3月23日 工程

使用 Elastic Stack 来监控和调优 Golang 应用程序

作者 曾勇

Golang 因为其语法简单,上手快且方便部署被开发者所青睐。今天在这里就给大家介绍一下如果使用 Elastic Stack 来分析 Golang 程序的内存使用情况,方便对 Golang 程序做长期监控进而调优和诊断,甚至发现一些潜在的内存泄露等问题。

Elastic Stack 包含 Elasticsearch、Logstash 和 Beats 这几个开源软件,而 Beats 又包含 Filebeat、Packetbeat、Winlogbeat、Metricbeat 和新出的 Heartbeat。今天主要用到 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 模块收集的数据进行的可视化分析:

Kibana Dashboard

如上图所见,最上面一栏是 Golang Heap 的摘要信息,可以大致了解 Golang 的内存使用和 GC 情况。System 表示 Golang 程序从操作系统申请的内存,可以理解为进程所占的内存而不是进程对应的虚拟内存;Bytes Allocated 表示 Heap 目前分配的内存,也就是 Golang 里面直接可使用的内存;GC limit 表示当 Golang 的 Heap 内存分配达到这个 limit 值之后就会开始执行 GC,这个 limit 值会随着每次 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")
    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 信息,也就是上面分析的数据源。

现在 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 访问到内部运行情况:

{
"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")

效果:

Graph 1

从上图可以看到,发往 Elasticsearch 的消息很稳定。同时关于 Metricbeat 的内存情况,我们打开导入的 Dashboard 查看:

Graph 2

上面介绍了如何基于 expvar 来监控 Golang 的内存情况和自定义业务监控指标,在结合 Elastic Stack 可以快速的进行分析,希望对大家有用。