标签 Pointer 下的文章

聊聊Prometheus Gauge的增减操作实现

本文永久链接 – https://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub

1. Gauge是啥?

熟悉Prometheus的小伙伴们都知道Prometheus提供了四大指标类型

  • Counter
  • Gauge
  • Histogram
  • Summary

Histogram和Summary是一类,但理解起来稍复杂一些,这里我们暂且不提。Counter顾名思义“计数器”,仅提供了Add方法,是一个一直递增的数值;而Gauge直译为“仪表盘”,它也是一个数值,但和Counter不同,它不仅提供Add方法,还提供了Sub方法。如果你的指标可增可减或是需要支持负数,那么Gauge显然是一个比Counter更适合的指标类型。

近期我们在测试时发现一个Gauge值为负的问题,Gauge本身是支持负值的,但我们系统中的这个指标值从业务含义上来说是不应该为负值的,为了fix掉这个问题,我深入看了一下Prometheus Go client包中Gauge的实现方式,Gauge的实现方式代表了一类问题的典型解决方法,这里简单聊聊。

2. Gauge增减操作的原理

在Prometheus Go client包中,我们看到Gauge是一个接口类型:

// github.com/prometheus/client_golang/prometheus/gauge.go
type Gauge interface {
    Metric
    Collector

    // Set sets the Gauge to an arbitrary value.
    Set(float64)
    // Inc increments the Gauge by 1. Use Add to increment it by arbitrary
    // values.
    Inc()
    // Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary
    // values.
    Dec()
    // Add adds the given value to the Gauge. (The value can be negative,
    // resulting in a decrease of the Gauge.)
    Add(float64)
    // Sub subtracts the given value from the Gauge. (The value can be
    // negative, resulting in an increase of the Gauge.)
    Sub(float64)

    // SetToCurrentTime sets the Gauge to the current Unix time in seconds.
    SetToCurrentTime()
}

client包还提供了该接口的默认实现类型gauge:

// github.com/prometheus/client_golang/prometheus/gauge.go
type gauge struct {
    // valBits contains the bits of the represented float64 value. It has
    // to go first in the struct to guarantee alignment for atomic
    // operations.  http://golang.org/pkg/sync/atomic/#pkg-note-BUG
    valBits uint64

    selfCollector

    desc       *Desc
    labelPairs []*dto.LabelPair
}

从gauge类型定义来看,作为仪表盘即时数值的gauge,其核心字段是uint64类型的valBits,该字段存储了gauge指标所代表的即时值

不过我们看到Gauge接口类型中的Add和Sub方法的参数都是float64类型。Gauge接口类型中的方法使用float64类型作为参数是无可厚非的,这是因为Gauge要支持浮点数,要支持小数,浮点数可以转化为整型,但整型却无法支持转换为带有小数部分的浮点数。

那么为什么gauge类型中使用了uint64类型而不是float64类型的字段来存储gauge代表的即时值呢?这就要从Prometheus go client的一个特性说起,那就是对Gauge即时值的修改要保证goroutine-safe。具体来说,gauge使用的是atomic包提供的原子操作来保证这种并发访问安全。但标准库的atomic包支持uint64类型的原子操作,而不支持float64类型的原子操作,恰float64和uint64的size又都是8字节,于是Prometheus go client利用了uint64支持原子操作以及uint64和float64类型都是64bits长度这两点实现了gauge类型的Add和Sub方法:

// github.com/prometheus/client_golang/prometheus/gauge.go

func (g *gauge) Add(val float64) {
    for {
        oldBits := atomic.LoadUint64(&g.valBits)
        newBits := math.Float64bits(math.Float64frombits(oldBits) + val)
        if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) {
            return
        }
    }
}

func (g *gauge) Sub(val float64) {
    g.Add(val * -1)
}

我们看到Sub方法实际调用的也是Add方法,只是将val值乘了个-1后作为Add方法的参数。我们接下来重点来看看gauge的Add方法。

gauge Add方法的实现是一个典型的CAS(CompareAndSwap)原子操作的使用模式,即在一个无限循环中,先原子读取当前即时值,然后将其与传入的增量值进行加和得到新值,最后通过CAS操作将新值设置为当前即时值。如果CAS操作失败,则重新走一遍循环。

不过值得我们关注的是Add方法中的float64与uint64类型各自的功用与相互的转换。Add方法先是利用atomic.LoadUint64原子读取valBits的值,然后通过math.Float64frombits将其转换为float64类型,之后用得到的float64类型即时值与val进行加法运算,得到我们想要的新值。接下来就是将其重新存储到valBits中。float64不支持原子操作,因此再调用CAS之前,Add方法还需将新值转换回uint64,这就是上面代码调用math.Float64bits的原因,之后通过atomic.CompareAndSwapUint64将保存了float64位模式的uint64类型的新值newBits写入valBits中。

大家一定很好奇,math.Float64frombits和math.Float64bits是如何做的uint64和float64间的转换,我们来看一下他们的实现:

// $GOROOT/src/math/unsafe.go

// Float64bits returns the IEEE 754 binary representation of f,
// with the sign bit of f and the result in the same bit position,
// and Float64bits(Float64frombits(x)) == x.
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

// Float64frombits returns the floating-point number corresponding
// to the IEEE 754 binary representation b, with the sign bit of b
// and the result in the same bit position.
// Float64frombits(Float64bits(x)) == x.
func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) }

我们看到,这两个函数只是利用unsafe包进行了类型转换,而并没有做任何算术运算。

关于如何使用unsafe包进行安全的类型转换,可以参见我的《Go语言精进之路》一书的第58条“掌握unsafe包的安全使用模式”。

综上:

  • gauge结构体中uint64类型的valBits实质上只是用来做float64数值的“承载体”,并借助原子操作对其类型的支持实现即时值的更新,它本身并不参与任何整型或浮点型计算;
  • Add方法中的运算都是在浮点型之间进行的,Add方法通过math.Float64frombits将uint64中承载的符合IEEE 754的浮点数表示还原为一个浮点数类型,然后与同样是float64类型的输入参数进行加和计算,计算的结果再通过math.Float64bits函数转换为uint64类型,这个过程8字节字段的位模式没有发生任何变化,最后通过CAS操作将结果值(新的位模式)写入valBits。

valBits中存储的是满足IEEE 754的浮点数的位模式。IEEE 754规范中,一个浮点数是由“符号位+阶码+尾数”构成的。详情可参考我的《Go语言第一课》专栏的第12讲基本数据类型:Go原生支持的数值类型有哪些

3. 小结

gauge结构体以及其Add方法所使用的这种通过位模式转换实现float64原子操作的模式值得借鉴。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商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
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

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

Go语言map类型变量背后的那些事儿

本文永久链接 – https://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable

切片(slice)和map是Go语言中最常用的两种原生复合数据类型,同时也是最容易使初学者感觉迷惑和“掉坑”的两个类型,这很大程度是因为Go runtime层的存在。什么是Go runtime层?可以参考我在《Go语言第一课FAQ》中的解释。

我们在Go用户源码层看到的切片与map是这样的:

var sl = make([]int, 3)
var m = make(map[string]int)

但在runtime层,它们又是另一幅“样子”。Go用户源码层的切片和map类型的变量,我常将它们称为“描述符”,因为它们和linux平台上通过open系统调用打开的文件描述符的功用十分类似,都是某个大块头儿数据(比如:一个500M的文本数据)的“代言人”,避免了和外界交互时对底层数据的搬动与拷贝。

很多人知道,在runtime层,切片是一个三元组结构(在我的“Go语言第一课”专栏中有单独一讲详细讲解),这里假定这个三元组结构为T,那么上面例子中通过make创建的切片m是类型T的实例还是*T的实例呢?很多人都知道答案:类型T的实例

同样看过我的专栏《Go语言精进之路》一书的读者也都知道:map类型在runtime层的表示为runtime.hmap,那么,上面通过make创建的map[string]int类型变量m究竟就是hmap类型实例还是*hmap类型实例呢?可能有些朋友还不明确,这里我们就来简单探究一下。

注:探究方法同样适用于切片类型。

m是hmap类型实例还是*hmap类型实例呢?最直接的方法是看runtime包的源码。在runtime/map.go中,我们找到了对应make(map[string]int)的源码makemap(或makemap_small):

func makemap(t *maptype, hint int, h *hmap) *hmap
func makemap_small() *hmap

我们看到:无论哪个函数返回的都是*hmap类型。到这里你的心里似乎有点倾向了,应该是*hmap。但还不那么确认。

我们假设m是*hmap,那么根据Go指针类型的定义(关于Go指针,我在专栏《聊聊Go语言中的指针》一讲中有较为全面讲解),Go为变量m分配的内存块中存储的值就应该是一个hmap实例的地址:

也就是说给m分配一块可以存储指针值的内存块儿即可。这样我们就可以通过相邻变量间的地址间隔来判定m是否仅仅是一个指针大小的内存块了。我们看下面例子:

package main

func main() {
    var a int = 5
    println("&a=", &a)
    var m1 = make(map[string]int)
    println("&m1=", &m1)
    var m2 = make(map[string]int)
    println("&m2=", &m2)
}

运行这个程序,输出结果如下:

&a=  0xc000046558
&m1= 0xc000046568
&m2= 0xc000046560

由于这些变量都分配在栈上(通过go build -gcflags ‘-m’可判断是否逃逸),我们用一幅图来展示一下上面示例中各个变量的内存块排列情况:

从m1与m2两个map类型变量的地址间隔情况来看,间隔8个字节,也就是一个指针大小,基本可以断定m2是指针类型实例了。

那么m是否是*hmap类型实例呢?如果是,我们是否可以通过对m的“解引用”得到该实例的值呢?我们下面试一下。

由于hmap是runtime包的非导出类型,所以我们无法在用户层直接使用,考虑到hmap都是由一些基本类型字段组成并且与runtime包的其他类型关联不多,我这里直接将其相关源码copy到示例源码中备用了。

package main

import (
    "fmt"
    "runtime"
    "unsafe"
)

type hmap struct {
    count     int // # live cells == size of map.  Must be first (used by len() builtin)
    flags     uint8
    B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
    noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
    hash0     uint32 // hash seed

    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

    extra *mapextra // optional fields
}

// mapextra holds fields that are not present on all maps.
type mapextra struct {
    overflow    *[]*bmap
    oldoverflow *[]*bmap

    nextOverflow *bmap
}

// A bucket for a Go map.
type bmap struct {
    tophash [bucketCnt]uint8
}

const (
    // Maximum number of key/elem pairs a bucket can hold.
    bucketCntBits = 3
    bucketCnt     = 1 << bucketCntBits
)

func main() {
    m := make(map[string]int)
    m["tony"] = 11
    m["bai"] = 12
    p := (*hmap)(unsafe.Pointer(*(*uintptr)((unsafe.Pointer)(&m))))
    fmt.Printf("%#v\n", *p)
}

这个例子中最难理解的就是变量p的声明与赋初值那一行,对于这一行我们分解来讲一下。

首先,前面我们说过:map类型变量m是指针,其存储的是一个hmap类型实例的地址。通过

*(*uintptr)((unsafe.Pointer)(&m))

我们得到的是m指向的那个hmap类型实例的地址。

然后通过将其转换为*hmap类型,我们就相当于直接得到了一个指向hmap类型实例地址的*hmap类型变量p。通过对p进行解引用,我们就能看到hmap结构体的内容了。运行上面代码我们得到下面输出结果:

main.hmap{count:2, flags:0x0, B:0x0, noverflow:0x0, hash0:0x42833520, buckets:(unsafe.Pointer)(0xc000072ea0), oldbuckets:(unsafe.Pointer)(nil), nevacuate:0x0, extra:(*main.mapextra)(nil)}

当我们看到输出结果中hmap.count这个字段(表示当前map中存储的键值对的个数)的值为2,我们就可以确定:m就是一个执行hmap结构体实例的指针这一结论是正确的。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}

img{512x368}
img{512x368}
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

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 从 0 开始构建 Agent Harness Go语言精进之路1 Go语言精进之路2 Go语言第一课 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