标签 Golang 下的文章

使用section.key的形式读取ini配置项

本文永久链接 – https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-key

配置文件读取是很多Go项目必备的功能,这方面社区提供的方案也相对成熟稳定。但之前写这部分代码时除了使用了针对不同配置文件格式(比如:ini、toml等)的驱动包之外,很少直接使用第三方包对读取出的配置项的值进行管理。于是我们就面对这样一个问题:其他包如果要使用这些被读取出的配置项的值该如何做呢?我们以读取ini格式承载的配置文件为例,来简单说说。

1. 全局变量法

这是最粗糙的方法,但却是最易理解的方法。我们建立一个config包,在main函数中读取配置文件并将读取到的配置项信息存放在config包的一个导出的全局变量中,这样其他包要想获取配置文件中配置项的信息,直接通过该全局变量读取即可。下面的demo1就是一个使用全局变量组织读取出的配置项信息的示例项目,其代码结构如下:

// github.com/bigwhite/experiments/tree/master/read-ini/demo1

demo1
├── conf
│   └── demo.ini
├── go.mod
├── go.sum
├── main.go
└── pkg
    ├── config
    │   └── config.go
    └── pkg1
        └── pkg1.go

demo1中的conf/demo.ini中存储了下面这些配置项信息:

$cat demo.ini
[server]
id = 100001
port = 23333
tls_port = 83333

[log]
level = 0; info:0, warn: 1, error: 2, dpanic:3, panic:4, fatal: 5, debug: -1
compress = true ; indicate whether the rotated log files should be compressed using gzip (default true)
path = "./log/demo.log" ; if it is empty, we use default logger(to stderr)
max_age = 3 ; the maximum number of days to retain old log files based on the timestamp encoded in their filename
maxbackups = 7 ; the maximum number of old log files to retain (default 7)
maxsize = 100 ; the maximum size in megabytes of the log file before it gets rotated (default 100)

[debug]
profile_on = true ;add profile web server for app to enable pprof through web
profile_port = 8091 ; profile web port

我们通过config包读取该配置文件(基于github.com/go-ini/ini包实现ini配置文件读取):

// github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/config/config.go
package config

import (
    ini "github.com/go-ini/ini"
)

type Server struct {
    Id      string `ini:""`
    Port    int    `ini:"port"`
    TlsPort int    `ini:"tls_port"`
}

type Log struct {
    Compress   bool   `ini:"compress"`
    LogPath    string `ini:"path"`
    MaxAge     int    `ini:"max_age"`
    MaxBackups int    `ini:"maxbackups"`
    MaxSize    int    `ini:"maxsize"`
}

type Debug struct {
    ProfileOn   bool   `ini:"profile_on"`
    ProfilePort string `ini:"profile_port"`
}

type IniConfig struct {
    Server `ini:"server"`
    Log    `ini:"log"`
    Debug  `ini:"debug"`
}

var Config = &IniConfig{}

func InitFromFile(path string) error {
    cfg, err := ini.Load(path)
    if err != nil {
        return err
    }

    return cfg.MapTo(Config)
}

这是一种典型的Go通过struct field tag与ini配置文件中section和key绑定读取的示例,我们在main包中调用InitFromFile读取ini配置文件:

// github.com/bigwhite/experiments/tree/master/read-ini/demo1/main.go

package main

import (
    "github.com/bigwhite/readini/pkg/config"
    "github.com/bigwhite/readini/pkg/pkg1"
)

func main() {
    err := config.InitFromFile("conf/demo.ini")
    if err != nil {
        panic(err)
    }
    pkg1.Foo()
}

读取后的配置项信息存储在config.Config这个全局变量中。在其他包中(比如pkg/pkg1/pkg1.go),我们可直接访问该全局变量获取配置项信息:

// github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/pkg1/pkg1.go
package pkg1

import (
    "fmt"

    "github.com/bigwhite/readini/pkg/config"
)

func Foo() {
    fmt.Printf("%#v\n", config.Config)
}

这种方式很简单、直观也易于理解,但以全局变量形式将配置项信息暴露给其他包,从代码设计层面,这总是会予人口实的。那么我们是否可以只暴露包函数,而不暴露具体实现呢?

2. 通过section.key形式读取配置项

由于是采用的tag与结构体字段的绑定方法,实际配置项名字与绑定的字段名字可能是不一致的,比如下面代码段中的结构体字段TlsPort与其tag tls_port:

type Server struct {
    Id      string `ini:"id"`
    Port    int    `ini:"port"`
    TlsPort int    `ini:"tls_port"`
}

这样使用config包的用户在要获取配置项值时就必须了解绑定的结构体字段的名字。如果我们不暴露这些绑定结构体的实现细节的话,config包的用户所掌握的信息仅仅就是配置文件(比如:demo.ini)本身了。

于是一个很自然的想法就会萌发出来!我们是否可以通过section.key的形式得到对应配置项的值,比如以下面配置项为例:

[server]
id = 100001
port = 23333
tls_port = 83333

我们需要通过server.id来获得id这个配置项的值,类似的其他配置项的获取方式是传入server.port、server.tls_port等。这样我们的config包仅需保留类似一个接收xx.yy.zz为参数的GetSectionKey函数即可,就像下面这样:

id, ok := config.GetSectionKey("server.id")

接下来,我们就沿着这个思路在demo1的基础上重构为新方案demo2。下面是修改后的demo2的config包代码:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/config/config.go
package config

import (
    "reflect"
    "strings"

    ini "github.com/go-ini/ini"
)

type server struct {
    Id      string `ini:"id"`
    Port    int    `ini:"port"`
    TlsPort int    `ini:"tls_port"`
}

type log struct {
    Compress   bool   `ini:"compress"`
    LogPath    string `ini:"path"`
    MaxAge     int    `ini:"max_age"`
    MaxBackups int    `ini:"maxbackups"`
    MaxSize    int    `ini:"maxsize"`
}

type debug struct {
    ProfileOn   bool   `ini:"profile_on"`
    ProfilePort string `ini:"profile_port"`
}

type iniConfig struct {
    Server server `ini:"server"`
    Log    log    `ini:"log"`
    Dbg    debug  `ini:"debug"`
}

var thisConfig = iniConfig{}

func InitFromFile(path string) error {
    cfg, err := ini.Load(path)
    if err != nil {
        return err
    }

    return cfg.MapTo(&thisConfig)
}

func GetSectionKey(name string) (interface{}, bool) {
    keys := strings.Split(name, ".")
    lastKey := keys[len(keys)-1]
    v := reflect.ValueOf(thisConfig)
    t := reflect.TypeOf(thisConfig)

    found := false
    for _, key := range keys {
        cnt := v.NumField()

        for i := 0; i < cnt; i++ {
            field := t.Field(i)
            if field.Tag.Get("ini") == key {
                t = field.Type
                v = v.Field(i)
                if key == lastKey {
                    found = true
                }
                break
            }
        }
    }

    if found {
        return v.Interface(), true
    }
    return nil, false
}

我们将原先暴露出去的全局变量改为了包内变量(thisConfig),几个绑定的结构体类型也都改为非导出的了。我们提供了一个对外的函数:GetSectionKey,这样通过该函数,我们就可以使用section.key的形式获取到对应配置项的值了。在GetSectionKey函数的实现中,我们使用了反射来获取结构体定义中各个字段的tag来和传入的section.key的各个部分做比对,一旦匹配,便将对应的值传出来。如果没有匹配到,则返回false,这里GetSectionKey的返回值列表设计也使用了经典的“comma, ok”模式。

这样,我们在pkg1包中便可以这样来获取对应的配置项的值了:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/pkg1/pkg1.go
package pkg1

import (
    "fmt"

    "github.com/bigwhite/readini/pkg/config"
)

func Foo() {
    id, ok := config.GetSectionKey("server.id")
    fmt.Printf("id = [%v], ok = [%t]\n", id, ok)
    tlsPort, ok := config.GetSectionKey("server.tls_port")
    fmt.Printf("tls_port = [%v], ok = [%t]\n", tlsPort, ok)
    logPath, ok := config.GetSectionKey("log.path")
    fmt.Printf("path = [%v], ok = [%t]\n", logPath, ok)
    logPath1, ok := config.GetSectionKey("log.path1")
    fmt.Printf("path1 = [%v], ok = [%t]\n", logPath1, ok)
}

运行demo2,我们将看到如下结果:

$go run main.go
id = [100001], ok = [true]
tls_port = [83333], ok = [true]
path = [./log/demo.log], ok = [true]
path1 = [<nil>], ok = [false]

现在还有一个问题,那就是config包暴露的函数GetSectionKey的第一个返回值类型为interface{},这样我们得到配置项的值后还得根据其类型通过类型断言方式进行转型,体验略差,我们可以在config包中提供常见类型的“语法糖”函数,比如下面这些:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/config/config.go
func GetInt(name string) (int, bool) {
    i, ok := GetSectionKey(name)
    if !ok {
        return 0, false
    }

    if v, ok := i.(int); ok {
        return v, true
    }

    // maybe it is a digital string
    s, ok := i.(string)
    if !ok {
        return 0, false
    }

    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, false
    }
    return n, true
}

func GetString(name string) (string, bool) {
    i, ok := GetSectionKey(name)
    if !ok {
        return "", false
    }

    s, ok := i.(string)
    if !ok {
        return "", false
    }
    return s, true
}

func GetBool(name string) (bool, bool) {
    i, ok := GetSectionKey(name)
    if !ok {
        return false, false
    }

    b, ok := i.(bool)
    if !ok {
        return false, false
    }
    return b, true
}

这样我们在pkg1包中就可以直接使用这些语法糖函数获取对应类型的配置项值了:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/pkg1/pkg1.go
b, ok := config.GetBool("debug.profile_on")
fmt.Printf("profile_on = [%t], ok = [%t]\n", b, ok)

3. 优化

配置读取一般都是在系统初始化阶段,对其性能要求不高。后续系统运行过程中,也会偶有获取配置项的业务逻辑。一旦在关键路径上有获取配置项值的逻辑,上面的方案便值得商榷,因为每次通过GetSectionKey获取一个配置项的值都要通过反射进行一番操作,性能肯定不佳。

那么如何优化呢?我们可以通过为每个key建立索引来进行。我们在config包中创建一个除初始化时只读的map变量:

// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go
var index = make(map[string]interface{}, 100)

在config包的InitFromFile中我们将配置项以section.key为key的形式索引到该index变量中:

// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go

func InitFromFile(path string) error {
    cfg, err := ini.Load(path)
    if err != nil {
        return err
    }

    err = cfg.MapTo(&thisConfig)
    if err != nil {
        return err
    }

    createIndex()
    return nil
}

createIndex的实现如下:

// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go
func createIndex() {
    v := reflect.ValueOf(thisConfig)
    t := reflect.TypeOf(thisConfig)
    cnt := v.NumField()
    for i := 0; i < cnt; i++ {
        fieldVal := v.Field(i)
        if fieldVal.Kind() != reflect.Struct {
            continue
        }

        // it is a struct kind field, go on to get tag
        fieldStructTyp := t.Field(i)
        tag := fieldStructTyp.Tag.Get("ini")
        if tag == "" {
            continue // no ini tag, ignore it
        }

        // append Field Recursively
        appendField(tag, fieldVal)
    }
}

func appendField(parentTag string, v reflect.Value) {
    cnt := v.NumField()
    for i := 0; i < cnt; i++ {
        fieldVal := v.Field(i)
        fieldTyp := v.Type()
        fieldStructTyp := fieldTyp.Field(i)
        tag := fieldStructTyp.Tag.Get("ini")
        if tag == "" {
            continue
        }
        if fieldVal.Kind() != reflect.Struct {
            // leaf field,  add to map
            index[parentTag+"."+tag] = fieldVal.Interface()
        } else {
            // recursive call appendField
            appendField(parentTag+"."+tag, fieldVal)
        }
    }
}

这样我们的GetSectionKey就会变得异常简单:

func GetSectionKey(name string) (interface{}, bool) {
    v, ok := index[name]
    return v, ok
}

我们看到:每次调用config.GetSectionKey将变成一次map的查询操作,这性能那是相当的高:)。

4. 第三方方案

其实前面那些仅仅是一个配置项读取思路的演进过程,你完全无需自行实现,因为我们有实现的更好的第三方包可以直接使用,比如viper。我们用viper来替换demo3中的config包,代码见demo4:

// github.com/bigwhite/experiments/tree/master/read-ini/demo4/main.go
package main

import (
    "github.com/bigwhite/readini/pkg/pkg1"
    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigName("demo")
    viper.SetConfigType("ini")
    viper.AddConfigPath("./conf")
    err := viper.ReadInConfig()
    if err != nil {
        panic(err)
    }
    pkg1.Foo()
}

我们在main函数中利用viper的API读取demo.ini中的配置。然后在pkg1.Foo函数中向下面这样获取配置项的值即可:

// github.com/bigwhite/experiments/tree/master/read-ini/demo4/pkg/pkg1/pkg1.go
package pkg1

import (
    "fmt"

    "github.com/spf13/viper"
)

func Foo() {
    id := viper.GetString("server.id")
    fmt.Printf("id = [%s]\n", id)
    tlsPort := viper.GetInt("server.tls_port")
    fmt.Printf("tls_port = [%d]\n", tlsPort)

    logPath := viper.GetString("log.path")
    fmt.Printf("path = [%s]\n", logPath)
    if viper.IsSet("log.path1") {
        logPath1 := viper.GetString("log.path1")
        fmt.Printf("path1 = [%s]\n", logPath1)
    } else {
        fmt.Printf("log.path1 is not found\n")
    }
}

上面的实现基本等价于我们在demo3中所作的一切,viper没有使用“comma, ok”模式,我们需要自己调用viper.IsSet来判断是否有某个配置项,而不是通过像GetString这样的函数返回的空字符串来判断。

使用viper后,我们甚至无需创建与配置文件中配置项对应的结构体类型了。viper是一个强大的Go配置操作框架,它能实现的不仅限于上面这些,它还支持写配置文件、监视配置文件变化并热加载、支持多种配置文件类型(JSON, TOML, YAML, HCL, ini等)、支持从环境变量和命令行参数读取配置,并且命令行参数、环境变量、配置文件等究竟以哪个配置为准,viper是按一定优先级次序的,从高到低分别为:

  • 明确调用Set
  • flag
  • env
  • config
  • key/value store
  • default

有如此完善的配置操作第三方库,我们完全无需手动撸自己的实现了。

5. 小结

除了在本文中提供的使用包级API获取配置项值的方法外,我们还可以将读取出的配置项集合放入应用上下文,以参数的形式“传递”到应用的各个角落,但笔者更喜欢向viper这种通过公共函数获取配置项的方法。本文阐述的就是这种思路的演化过程,并给出一个“玩票”的实现(未经系统测试),以帮助大家了解其中原理,但不要将其用到你的项目中哦。

本文涉及的源码请到这里下载:https://github.com/bigwhite/experiments/tree/master/read-ini。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

使用go-metrics在Go应用中增加度量

本文永久链接 – https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics

Go语言内置expvar,基于expvar提供的对基础度量的支持能力,我们可以自定义各种度量(metrics)。但是expvar仅仅是提供了最底层的度量定义支持,对于一些复杂的度量场景,第三方或自实现的metrics包必不可少。

go-metrics包是Go领域使用较多的是metrics包,该包是对Java社区依旧十分活跃的Coda Hale’s Metrics library的不完全Go移植(不得不感慨一下:Java的生态还真是强大)。因此该包在概念上与Coda Hale’s Metrics library是基本保持一致的。go-metrics包在文档方面做的还不够,要理解很多概念性的东西,我们还得回到Coda Hale’s Metrics library的项目文档去挖掘。

go-metrics这样的包是纯工具类的包,没有太多“烧脑”的地方,只需要会用即可,这篇文章我们就来简单地看看如何使用go-metrics在Go应用中增加度量。

1. go-metrics的结构

go-metrics在度量指标组织上采用了与Coda Hale’s Metrics library相同的结构,即使用Metrics Registry(Metrics注册表)。Metrics注册表是一个度量指标的集合:

                             ┌─────────────┐
                             │             │
                      ┌──────┤    metric1  │
                      │      │             │
                      │      └─────────────┘
                      │
                      │
┌─────────────────┐   │      ┌─────────────┐
│                 ├───┘      │             │
│                 │          │    metric2  │
│     Registry    ├──────────┤             │
│                 │          └─────────────┘
│                 ├───────┐
│                 │       │
└──────────────┬──┘       │  ┌─────────────┐
               │          │  │             │
               │          └──┤    metric3  │
               │             │             │
               │             └─────────────┘
               │                   ... ...
               │             ┌─────────────┐
               │             │             │
               └─────────────┤    metricN  │
                             │             │
                             └─────────────┘

go-metrics包将Metrics注册表的行为定义为了一个接口类型:

// https://github.com/rcrowley/go-metrics/blob/master/registry.go
type Registry interface {

    // Call the given function for each registered metric.
    Each(func(string, interface{}))

    // Get the metric by the given name or nil if none is registered.
    Get(string) interface{}

    // GetAll metrics in the Registry.
    GetAll() map[string]map[string]interface{}

    // Gets an existing metric or registers the given one.
    // The interface can be the metric to register if not found in registry,
    // or a function returning the metric for lazy instantiation.
    GetOrRegister(string, interface{}) interface{}

    // Register the given metric under the given name.
    Register(string, interface{}) error

    // Run all registered healthchecks.
    RunHealthchecks()

    // Unregister the metric with the given name.
    Unregister(string)

    // Unregister all metrics.  (Mostly for testing.)
    UnregisterAll()
}

并提供了一个Registry的标准实现类型StandardRegistry:

// https://github.com/rcrowley/go-metrics/blob/master/registry.go
type StandardRegistry struct {
    metrics map[string]interface{}
    mutex   sync.RWMutex
}

我们看到StandardRegistry使用map结构来组织metrics。我们可以通过NewRegistry函数创建了一个基于StandardRegistry的Registry实例:

// https://github.com/rcrowley/go-metrics/blob/master/registry.go
func NewRegistry() Registry {
    return &StandardRegistry{metrics: make(map[string]interface{})}
}

和标准库的flag或log包的设计方式类似,go-metrics包也在包层面上提供了默认的StandardRegistry实例:DefaultRegistry,这样大多数情况直接使用DefaultRegistry实例即可满足你的需求:

// https://github.com/rcrowley/go-metrics/blob/master/registry.go
var DefaultRegistry Registry = NewRegistry()

一旦有了默认Registry实例,我们通常使用下面goroutine并发安全的包级函数GetOrRegister来注册或获取某个度量指标:

// https://github.com/rcrowley/go-metrics/blob/master/registry.go
func GetOrRegister(name string, i interface{}) interface{} {
    return DefaultRegistry.GetOrRegister(name, i)
}

2. go-metrics的度量类型

go-metrics继承了其前身Coda Hale’s Metrics library所支持的几种基本的度量类型,它们是Gauges、Counters、Histograms、Meters和Timers。下面我们就针对这几种基本度量类型逐一说明一下其含义和使用方法。

1) Gauge

Gauge是对一个数值的即时测量值,其反映一个值的瞬时快照,比如我们要度量当前队列中待发送消息数量、当前应用程序启动的goroutine数量,都可以用Gauge这种度量类型实现。

下面的例子使用一个Gauge度量类型度量程序当前启动的goroutine数量:

// gauge.go
package main

import (
    "fmt"
    "net/http"
    "runtime"
    "time"

    "github.com/rcrowley/go-metrics"
)

func main() {
    g := metrics.NewGauge()
    metrics.GetOrRegister("goroutines.now", g)
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    })

    go func() {
        t := time.NewTicker(time.Second)
        for {
            select {
            case <-t.C:
                c := runtime.NumGoroutine()
                g.Update(int64(c))
                fmt.Println("goroutines now =", g.Value())
            }
        }
    }()
    http.ListenAndServe(":8080", nil)
}

启动该程序,并用hey工具发起http请求,我们看到如下输出:

$hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080

$go run gauge.go
goroutines now = 9
goroutines now = 10
goroutines now = 7
goroutines now = 8
goroutines now = 7
goroutines now = 7
... ...

go-metrics包提供了将Registry中的度量指标格式化输出的接口,我们可以使用该接口将指标情况输出出来,而无需自行输出log,比如上面例子可以改造为下面这样:

// gauge1.go
package main

import (
    "log"
    "net/http"
    "runtime"
    "time"

    "github.com/rcrowley/go-metrics"
)

func main() {
    g := metrics.NewGauge()
    metrics.GetOrRegister("goroutines.now", g)
    go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    })

    go func() {
        t := time.NewTicker(time.Second)
        for {
            select {
            case <-t.C:
                c := runtime.NumGoroutine()
                g.Update(int64(c))
            }
        }
    }()
    http.ListenAndServe(":8080", nil)
}

同样方式运行上面gauge1.log:

$go run gauge1.go
2021/07/04 09:42:58 gauge goroutines.now
2021/07/04 09:42:58   value:              10
2021/07/04 09:42:59 gauge goroutines.now
2021/07/04 09:42:59   value:               9
2021/07/04 09:43:00 gauge goroutines.now
2021/07/04 09:43:00   value:               9
2021/07/04 09:43:01 gauge goroutines.now
2021/07/04 09:43:01   value:              10
... ...

go-metrics包的Log函数必须放在一个单独的goroutine中执行,否则它将阻塞调用它的goroutine的继续执行。但Log函数也是goroutine安全的,其每次输出度量值时其实输出的都是Registry中各个度量值的“快照副本”:

// https://github.com/rcrowley/go-metrics/blob/master/registry.go
func (r *StandardRegistry) Each(f func(string, interface{})) {
    metrics := r.registered()
    for i := range metrics {
        kv := &metrics[i]
        f(kv.name, kv.value)
    }
}

func (r *StandardRegistry) registered() []metricKV {
    r.mutex.RLock()
    defer r.mutex.RUnlock()
    metrics := make([]metricKV, 0, len(r.metrics))
    for name, i := range r.metrics {
        metrics = append(metrics, metricKV{
            name:  name,
            value: i,
        })
    }
    return metrics
}

对于Gauge这类的季世志度量,就像上面代码那样,我们都是通过Update直接设置其值的。

2) Counter

Counter顾名思义计数器!和Gauge相比,其提供了指标增减方法Inc和Dec,如下面代码:

// https://github.com/rcrowley/go-metrics/blob/master/counter.go
type Counter interface {
    Clear()
    Count() int64
    Dec(int64)
    Inc(int64)
    Snapshot() Counter
}

计数是日常使用较多的度量场景,比如一个服务处理的请求次数就十分适合用计数这个度量指标,下面这段代码演示的就是这一场景:

// counter.go

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/rcrowley/go-metrics"
)

func main() {
    c := metrics.NewCounter()
    metrics.GetOrRegister("total.requests", c)
    go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        c.Inc(1)
    })

    http.ListenAndServe(":8080", nil)
}

在这段代码中,我们每收到一个http request就在其对应的处理函数中利用Counter的Inc方法增加计数,运行上述代码:

$go run counter.go
2021/07/04 10:29:03 counter total.requests
... ...
2021/07/04 10:29:06 counter total.requests
2021/07/04 10:29:06   count:               0
2021/07/04 10:29:07 counter total.requests
2021/07/04 10:29:07   count:           33890
2021/07/04 10:29:08 counter total.requests
2021/07/04 10:29:08   count:           80160
2021/07/04 10:29:09 counter total.requests
2021/07/04 10:29:09   count:          124855
2021/07/04 10:29:10 counter total.requests
2021/07/04 10:29:10   count:          172077
2021/07/04 10:29:11 counter total.requests
2021/07/04 10:29:11   count:          218466
2021/07/04 10:29:12 counter total.requests
2021/07/04 10:29:12   count:          265476
2021/07/04 10:29:13 counter total.requests
2021/07/04 10:29:13   count:          309153
... ...

3) Meter

Meter这个类型用于测量一组事件发生的速度,比如:web服务的平均处理性能(条/秒),除了平均值,go-metrics的Meter默认还提供1分钟、5分钟和15分钟时间段的平均速度,和top命令中的load average输出的一分钟、五分钟、以及十五分钟的系统平均负载类似。

下面就是一个用Meter来测量web服务处理性能的例子:

// meter.go 

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/rcrowley/go-metrics"
)

func main() {
    m := metrics.NewMeter()
    metrics.GetOrRegister("rate.requests", m)
    go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        m.Mark(1)
    })
    http.ListenAndServe(":8080", nil)
}

我们用hey给该web server“施压”并查看Meter度量指标的输出结果:

$hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080

$go run meter.go
2021/07/04 10:55:59 meter rate.requests
2021/07/04 10:55:59   count:               0
2021/07/04 10:55:59   1-min rate:          0.00
2021/07/04 10:55:59   5-min rate:          0.00
2021/07/04 10:55:59   15-min rate:         0.00
2021/07/04 10:55:59   mean rate:           0.00
2021/07/04 10:56:00 meter rate.requests
2021/07/04 10:56:00   count:               0
2021/07/04 10:56:00   1-min rate:          0.00
2021/07/04 10:56:00   5-min rate:          0.00
2021/07/04 10:56:00   15-min rate:         0.00
2021/07/04 10:56:00   mean rate:           0.00
2021/07/04 10:56:01 meter rate.requests
2021/07/04 10:56:01   count:            8155
2021/07/04 10:56:01   1-min rate:          0.00
2021/07/04 10:56:01   5-min rate:          0.00
2021/07/04 10:56:01   15-min rate:         0.00
2021/07/04 10:56:01   mean rate:        2718.27
2021/07/04 10:56:02 meter rate.requests
2021/07/04 10:56:02   count:           50937
2021/07/04 10:56:02   1-min rate:          0.00
2021/07/04 10:56:02   5-min rate:          0.00
2021/07/04 10:56:02   15-min rate:         0.00
2021/07/04 10:56:02   mean rate:       12734.04
2021/07/04 10:56:03 meter rate.requests
2021/07/04 10:56:03   count:           96129
2021/07/04 10:56:03   1-min rate:      19225.00
2021/07/04 10:56:03   5-min rate:      19225.00
2021/07/04 10:56:03   15-min rate:     19225.00
2021/07/04 10:56:03   mean rate:       19225.54
2021/07/04 10:56:04 meter rate.requests
2021/07/04 10:56:04   count:          141076
2021/07/04 10:56:04   1-min rate:      19225.00
2021/07/04 10:56:04   5-min rate:      19225.00
2021/07/04 10:56:04   15-min rate:     19225.00
2021/07/04 10:56:04   mean rate:       23512.40
2021/07/04 10:56:05 meter rate.requests
2021/07/04 10:56:05   count:          187733
2021/07/04 10:56:05   1-min rate:      19225.00
2021/07/04 10:56:05   5-min rate:      19225.00
2021/07/04 10:56:05   15-min rate:     19225.00
2021/07/04 10:56:05   mean rate:       26818.71
2021/07/04 10:56:06 meter rate.requests
2021/07/04 10:56:06   count:          234874
2021/07/04 10:56:06   1-min rate:      19225.00
2021/07/04 10:56:06   5-min rate:      19225.00
2021/07/04 10:56:06   15-min rate:     19225.00
2021/07/04 10:56:06   mean rate:       29358.98
2021/07/04 10:56:07 meter rate.requests
2021/07/04 10:56:07   count:          279201
2021/07/04 10:56:07   1-min rate:      19225.00
2021/07/04 10:56:07   5-min rate:      19225.00
2021/07/04 10:56:07   15-min rate:     19225.00
2021/07/04 10:56:07   mean rate:       31022.05
2021/07/04 10:56:08 meter rate.requests
2021/07/04 10:56:08   count:          321704
2021/07/04 10:56:08   1-min rate:      21295.03
2021/07/04 10:56:08   5-min rate:      19652.92
2021/07/04 10:56:08   15-min rate:     19368.43
2021/07/04 10:56:08   mean rate:       32170.20
2021/07/04 10:56:09 meter rate.requests
2021/07/04 10:56:09   count:          362403
2021/07/04 10:56:09   1-min rate:      21295.03
2021/07/04 10:56:09   5-min rate:      19652.92
2021/07/04 10:56:09   15-min rate:     19368.43
2021/07/04 10:56:09   mean rate:       32945.48
2021/07/04 10:56:10 meter rate.requests
2021/07/04 10:56:10   count:          401442
2021/07/04 10:56:10   1-min rate:      21295.03
2021/07/04 10:56:10   5-min rate:      19652.92
2021/07/04 10:56:10   15-min rate:     19368.43
2021/07/04 10:56:10   mean rate:       33453.34
2021/07/04 10:56:11 meter rate.requests
2021/07/04 10:56:11   count:          440905
2021/07/04 10:56:11   1-min rate:      21295.03
2021/07/04 10:56:11   5-min rate:      19652.92
2021/07/04 10:56:11   15-min rate:     19368.43
2021/07/04 10:56:11   mean rate:       33915.67
2021/07/04 10:56:12 meter rate.requests
2021/07/04 10:56:12   count:          479301
2021/07/04 10:56:12   1-min rate:      21295.03
2021/07/04 10:56:12   5-min rate:      19652.92
2021/07/04 10:56:12   15-min rate:     19368.43
2021/07/04 10:56:12   mean rate:       34235.60
2021/07/04 10:56:13 meter rate.requests
2021/07/04 10:56:13   count:          518843
2021/07/04 10:56:13   1-min rate:      22744.85
2021/07/04 10:56:13   5-min rate:      19979.77
2021/07/04 10:56:13   15-min rate:     19479.57
2021/07/04 10:56:13   mean rate:       34589.43
2021/07/04 10:56:14 meter rate.requests
2021/07/04 10:56:14   count:          560260
2021/07/04 10:56:14   1-min rate:      22744.85
2021/07/04 10:56:14   5-min rate:      19979.77
2021/07/04 10:56:14   15-min rate:     19479.57
2021/07/04 10:56:14   mean rate:       35016.17

如果使用Meter度量服务的最佳性能值,那么需要有持续稳定的“施压”,待1、5、15分钟速率稳定后,这时的值才有意义。Meter的最后一项mean rate是平均值,即服务启动后处理请求的总量与程序运行时间的比值。

4) Histogram

Histogram是直方图,与概率统计学上直方图的概念类似,go-metrics中的Histogram也是用来统计一组数据的统计学分布情况的。除了最小值(min)、最大值(max)、平均值(mean)等,它还测量中位数(median)、第75、90、95、98、99和99.9百分位数。

直方图可以用来度量事件发生的数据分布情况,比如:服务器处理请求时长的数据分布情况,下面就是这样一个例子:

// histogram.go
package main

import (
    "log"
    "math/rand"
    "net/http"
    "time"

    "github.com/rcrowley/go-metrics"
)

func main() {
    s := metrics.NewExpDecaySample(1028, 0.015)
    h := metrics.NewHistogram(s)
    metrics.GetOrRegister("latency.response", h)
    go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        i := rand.Intn(10)
        h.Update(int64(time.Microsecond * time.Duration(i)))
    })
    http.ListenAndServe(":8080", nil)
}

在上面这个例子中,我们使用一个随机值来模拟服务处理http请求的时间。Histogram需要一个采样算法,go-metrics内置了ExpDecaySample采样。运行上述示例,并使用hey模拟客户端请求,我们得到如下输出:

$go run histogram.go
2021/07/04 11:31:54 histogram latency.response
2021/07/04 11:31:54   count:               0
2021/07/04 11:31:54   min:                 0
2021/07/04 11:31:54   max:                 0
2021/07/04 11:31:54   mean:                0.00
2021/07/04 11:31:54   stddev:              0.00
2021/07/04 11:31:54   median:              0.00
2021/07/04 11:31:54   75%:                 0.00
2021/07/04 11:31:54   95%:                 0.00
2021/07/04 11:31:54   99%:                 0.00
2021/07/04 11:31:54   99.9%:               0.00
2021/07/04 11:31:55   99.9%:               0.00
... ...
2021/07/04 11:31:59 histogram latency.response
2021/07/04 11:31:59   count:           33244
2021/07/04 11:31:59   min:                 0
2021/07/04 11:31:59   max:              9000
2021/07/04 11:31:59   mean:             4457.20
2021/07/04 11:31:59   stddev:           2793.67
2021/07/04 11:31:59   median:           4000.00
2021/07/04 11:31:59   75%:              7000.00
2021/07/04 11:31:59   95%:              9000.00
2021/07/04 11:31:59   99%:              9000.00
2021/07/04 11:31:59   99.9%:            9000.00
2021/07/04 11:32:00 histogram latency.response
2021/07/04 11:32:00   count:           78970
2021/07/04 11:32:00   min:                 0
2021/07/04 11:32:00   max:              9000
2021/07/04 11:32:00   mean:             4465.95
2021/07/04 11:32:00   stddev:           2842.12
2021/07/04 11:32:00   median:           4000.00
2021/07/04 11:32:00   75%:              7000.00
2021/07/04 11:32:00   95%:              9000.00
2021/07/04 11:32:00   99%:              9000.00
2021/07/04 11:32:00   99.9%:            9000.00
2021/07/04 11:32:01 histogram latency.response
2021/07/04 11:32:01   count:          124573
2021/07/04 11:32:01   min:                 0
2021/07/04 11:32:01   max:              9000
2021/07/04 11:32:01   mean:             4459.14
2021/07/04 11:32:01   stddev:           2820.38
2021/07/04 11:32:01   median:           4000.00
2021/07/04 11:32:01   75%:              7000.00
2021/07/04 11:32:01   95%:              9000.00
2021/07/04 11:32:01   99%:              9000.00
2021/07/04 11:32:01   99.9%:            9000.00
... ...

Histogram度量输出的值包括min、max、mean(平均数)、median(中位数)、75、95、99、99.9百分位数上的度量结果。

5) Timer

最后我们来介绍Timer这个度量类型。大家千万别被这度量类型的名称所误导,这并不是一个定时器。

Timer是go-metrics定义的一个抽象度量类型,它可以理解为Histogram和Meter的“合体”,即既度量一段代码的执行频率(rate),又给出这段代码执行时间的数据分布。这一点从Timer的实现亦可以看出来:

// https://github.com/rcrowley/go-metrics/blob/master/timer.go

func NewTimer() Timer {
    if UseNilMetrics {
        return NilTimer{}
    }
    return &StandardTimer{
        histogram: NewHistogram(NewExpDecaySample(1028, 0.015)),
        meter:     NewMeter(),
    }
}

我们看到一个StandardTimer是由histogram和meter组成的。 我们还是以上面的http server服务为例,我们这次用Timer来度量:

// timer.go

package main

import (
    "log"
    "math/rand"
    "net/http"
    "time"

    "github.com/rcrowley/go-metrics"
)

func main() {
    m := metrics.NewTimer()
    metrics.GetOrRegister("timer.requests", m)
    go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        i := rand.Intn(10)
        m.Update(time.Microsecond * time.Duration(i))
    })
    http.ListenAndServe(":8080", nil)
}

大家可以看到在这里我们同样用随机数模拟请求的处理时间并传给Timer的Update方法。运行这段代码并用hey压测:

$go run timer.go
2021/07/04 17:13:47 timer timer.requests
2021/07/04 17:13:47   count:           13750
2021/07/04 17:13:47   min:                 0.00ns
2021/07/04 17:13:47   max:              9000.00ns
2021/07/04 17:13:47   mean:             4406.61ns
2021/07/04 17:13:47   stddev:           2785.11ns
2021/07/04 17:13:47   median:           4000.00ns
2021/07/04 17:13:47   75%:              7000.00ns
2021/07/04 17:13:47   95%:              9000.00ns
2021/07/04 17:13:47   99%:              9000.00ns
2021/07/04 17:13:47   99.9%:            9000.00ns
2021/07/04 17:13:47   1-min rate:          0.00
2021/07/04 17:13:47   5-min rate:          0.00
2021/07/04 17:13:47   15-min rate:         0.00
2021/07/04 17:13:47   mean rate:       13748.57
2021/07/04 17:13:48 timer timer.requests
2021/07/04 17:13:48   count:           56584
2021/07/04 17:13:48   min:                 0.00ns
2021/07/04 17:13:48   max:              9000.00ns
2021/07/04 17:13:48   mean:             4442.61ns
2021/07/04 17:13:48   stddev:           2895.66ns
2021/07/04 17:13:48   median:           4000.00ns
2021/07/04 17:13:48   75%:              7000.00ns
2021/07/04 17:13:48   95%:              9000.00ns
2021/07/04 17:13:48   99%:              9000.00ns
2021/07/04 17:13:48   99.9%:            9000.00ns
2021/07/04 17:13:48   1-min rate:          0.00
2021/07/04 17:13:48   5-min rate:          0.00
2021/07/04 17:13:48   15-min rate:         0.00
2021/07/04 17:13:48   mean rate:       28289.23
2021/07/04 17:13:49 timer timer.requests
2021/07/04 17:13:49   count:          102426
2021/07/04 17:13:49   min:                 0.00ns
2021/07/04 17:13:49   max:              9000.00ns
2021/07/04 17:13:49   mean:             4436.77ns
2021/07/04 17:13:49   stddev:           2892.85ns
2021/07/04 17:13:49   median:           4000.00ns
2021/07/04 17:13:49   75%:              7000.00ns
2021/07/04 17:13:49   95%:              9000.00ns
2021/07/04 17:13:49   99%:              9000.00ns
2021/07/04 17:13:49   99.9%:            9000.00ns
2021/07/04 17:13:49   1-min rate:          0.00
2021/07/04 17:13:49   5-min rate:          0.00
2021/07/04 17:13:49   15-min rate:         0.00
2021/07/04 17:13:49   mean rate:       34140.68

我们看到Timer度量的输出也的确是Histogram和Meter的联合体!

3. 小结

通过go-metrics包,我们可以很方便地为一个Go应用添加度量指标,go-metrics提供的meter、histogram可以覆盖Go应用基本性能指标需求(吞吐性能、延迟数据分布等)。go-metrics还支持各种指标值导出的,只是这里没有提及,大家可以到go-metrics官网了解详情。

本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/go-metrics


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats