标准库 expvar 实战

标准库 expvar 实战

Go 标准库有一个 expvar 包。 该软件包可以通过 JSON 格式的 HTTP API 公开您的应用程序和 Go 运行时的指标。 我认为这个软件包对于每个人 gopher 来说都是有用的。 但是,来自 godoc.org 的数据表明,没有多少人知道这个包。截止目前(2017-6-18),该包被公开的项目 import 2207 次,相比较而言,连 image 包被 import 3491 次之多。这篇文章,我想展示 expvar 包的工作原理,以及它的使用。

包简介

包 expvar 为公共变量提供了一个标准化的接口,如服务器中的操作计数器。它以 JSON 格式通过 /debug/vars 接口以 HTTP 的方式公开这些公共变量。

设置或修改这些公共变量的操作是原子的。

除了为程序增加 HTTP handler,此包还注册以下变量:

    cmdline   os.Args
    memstats  runtime.Memstats

导入该包有时只是为注册其 HTTP handler 和上述变量。 要以这种方式使用,请将此包通过如下形式引入到程序中:

    import _ "expvar"

例子

在浏览此包的详细信息之前,我想演示使用 expvar 包可以做什么。以下代码创建一个在监听 1818端口的 HTTP 服务器。每个请求 hander() 后,在向访问者发送响应消息之前增加计数器。

    package main

    import (
        "expvar"
        "fmt"
        "net/http"
    )

    var visits = expvar.NewInt("visits")

    func handler(w http.ResponseWriter, r *http.Request) {
        visits.Add(1)
        fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
    }

    func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":1818", nil)
    }

导入时,expvar 包将为 http.DefaultServeMux 上的模式 “/debug /vars” 注册一个处理函数。此处理程序返回已在 expvar 包中注册的所有指标。运行代码并访问 http://localhost:1818/debug/vars,您将看到如下所示的内容。输出被截断以增加可读性:

    {
      "cmdline": [
        "/tmp/go-build872151671/command-line-arguments/_obj/exe/main"
      ],
      "memstats": {
        "Alloc": 397576,
        "TotalAlloc": 397576,
        "Sys": 3084288,
        "Lookups": 7,
        "Mallocs": 5119,
        "Frees": 167,
        "HeapAlloc": 397576,
        "HeapSys": 1769472,
        "HeapIdle": 1015808,
        "HeapInuse": 753664,
        "HeapReleased": 0,
        "HeapObjects": 4952,
        "StackInuse": 327680,
        "StackSys": 327680,
        "MSpanInuse": 14240,
        "MSpanSys": 16384,
        "MCacheInuse": 4800,
        "MCacheSys": 16384,
        "BuckHashSys": 2380,
        "GCSys": 131072,
        "OtherSys": 820916,
        "NextGC": 4194304,
        "LastGC": 0,
        "PauseTotalNs": 0,
        "PauseNs": [
          0,
          0,
        ],
        "PauseEnd": [
          0,
          0
        ],
        "GCCPUFraction": 0,
        "EnableGC": true,
        "DebugGC": false,
        "BySize": [
          {
            "Size": 16640,
            "Mallocs": 0,
            "Frees": 0
          },
          {
            "Size": 17664,
            "Mallocs": 0,
            "Frees": 0
          }
        ]
      },
      "visits": 0
    }

信息真不少。这是因为默认情况下该包注册了os.Args 和 runtime.Memstats 两个指标。 我想在这个 JSON 响应结束时关注访问计数器。 因为计数器还没有增加,它的值仍然为0。现在通过访问http:// localhost:1818/golang 来增加计数器,然后返回。计数器不再为0。

expvar.Publish

expvar 包相当小且容易理解。它主要由两个部分组成。第一个是函数 expvar.Publish(name string,v expvar.Var)。该函数可用于在未导出的全局注册表中注册具有特定名称的 v。以下代码段显示了具体实现。接下来的 3 个代码段是从 expvar 包的源代码中截取的。

    // Publish declares a named exported variable. This should be called from a
    // package's init function when it creates its Vars. If the name is already
    // registered then this will log.Panic.
    func Publish(name string, v Var) {
        mutex.Lock()
        defer mutex.Unlock()

        // Check if name has been taken already. If so, panic.
        if _, existing := vars[name]; existing {
            log.Panicln("Reuse of exported var name:", name)
        }

         // vars is the global registry. It is defined somewhere else in the
         // expvar package like this:
         //
         //  vars = make(map[string]Var)
        vars[name] = v
        // 一方面,该包中所有公共变量,放在 vars 中,同时,通过 varKeys 保存了所有变量名,并且按字母序排序,即实现了一个有序的、线程安全的哈希表
        varKeys = append(varKeys, name)
        sort.Strings(varKeys)
    }

expvar.Var

另一个重要的组件是 expvar.Var 接口。 这个接口只有一个方法:

    // 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
    }

所以你可以在有 String() string 方法的所有类型上调用 Publish() 函数。

expvar.Int

expvar 包附带了其他几个类型,它们实现了 expvar.Var 接口。其中一个是 expvar.Int,我们已经在演示代码中通过 expvar.NewInt(“visits”) 方式使用它了,它会创建一个新的 expvar.Int,并使用 expvar.Publish 注册它,然后返回一个指向新创建的 expvar.Int 的指针。

    func NewInt(name string) *Int {
        v := new(Int)
        Publish(name, v)
        return v
    }

expvar.Int 包装一个 int64,并有两个函数 Add(delta int64) 和 Set(value int64),它们以线程安全的方式修改整数。

Other types

除了expvar.Int,该包还提供了一些实现 expvar.Var 接口的其他类型:

* expvar.Float
* expvar.String
* expvar.Map
* expvar.Func

前两个类型包装了 float64 和 string。后两种类型需要稍微解释下。

expvar.Map 类型可用于使指标出现在某些名称空间下。我经常这样用:

    var stats = expvar.NewMap("tcp")
    var requests, requestsFailed expvar.Int

    func init() {
        stats.Set("requests", &requests)
        stats.Set("requests_failed", &requestsFailed)
    }

这段代码使用名称空间 tcp 注册了两个指标 requests 和 requests_failed。它将显示在 JSON 响应中,如下所示:

    {
        "tcp": {
            "request": 18,
            "requests_failed": 21
        }
    }

当要使用某个函数的执行结果时,您可以使用 expvar.Func。假设您希望计算应用程序的正常运行时间,每次有人访问 http://localhost:1818/debug/vars 时,都必须重新计算此值。

    var start = time.Now()

    func calculateUptime() interface {
        return time.Since(start).String()
    }

    expvar.Publish("uptime", expvar.Func(calculateUptime))

关于 Handler 函数

本文开始时提到,可以简单的导入 expvar 包,然后使用其副作用,导出 /debug/vars 模式。然而,如果我们使用了一些框架,并非使用 http.DefaultServeMux,而是框架自己定义的 Mux,这时直接导入使用副作用可能会不生效。这时我们可以按照使用的框架,定义自己的路径,比如也叫:/debug/vars,然后,这对应的处理程序中,有两种处理方式:

1)将处理直接交给 expvar.Handler,比如:

    handler := expvar.Handler()
    handler.ServeHTTP(w, req)

2)自己遍历 expvar 中的公共变量,构造输出,甚至可以过滤 expvar 默认提供的 cmdline 和 memstats,我们看下 expvarHandler 的源码就明白了:(通过 expvar.Do 函数来遍历)

    func expvarHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        fmt.Fprintf(w, "{\n")
        first := true
        Do(func(kv KeyValue) {
            if !first {
                fmt.Fprintf(w, ",\n")
            }
            first = false
            fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
        })
        fmt.Fprintf(w, "\n}\n")
    }

Go 语言中文网 就是用了第1种方式来处理。

总结

通过 expvar 包,使得展示应用程序指标非常容易。我几乎在我写的每个应用程序中使用它来展示一些指示应用程序运行状况的指标。InfluxDB 和 Grafana,加上一个自定义的聚合器,我可以很容易监控我的应用程序。

在英文 expvar in action 的基础上有增减。

1 Star2 Stars3 Stars4 Stars5 Stars (还没有人评分,赶紧评一下)
Loading...

发表评论

电子邮件地址不会被公开。