Talk about the implementation of the increase and decrease operations of Prometheus Gauge

Permalink to this article – https://ift.tt/RIcUr15

1. What is a gauge?

Friends who are familiar with Prometheus know that Prometheus provides four types of indicators :

  • Counter
  • Gauge
  • Histogram
  • Summary

Histogram and Summary are one type, but it is a little more complicated to understand, so we won’t mention it here. Counter, as the name implies, “counter”, only provides the Add method, which is a value that is always increasing; while Gauge is literally translated as “dashboard”, which is also a value, but unlike Counter, it not only provides the Add method, but also provides the Sub method. If your indicator can increase or decrease or needs to support negative numbers, then Gauge is obviously a more suitable indicator type than Counter.

Recently, we found a problem that the Gauge value is negative during testing. The Gauge itself supports negative values, but this indicator value in our system should not be negative in terms of business meaning. In order to fix this problem, I I took a deep look at the implementation of Gauge in the Prometheus Go client package . The implementation of Gauge represents a typical solution to a class of problems. Here is a brief chat.

2. The principle of Gauge increase and decrease operation

In the Prometheus Go client package, we see that Gauge is an interface type:

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

The client package also provides a default implementation type gauge of this interface:

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

From the definition of the gauge type, the core field of the gauge as the real-time value of the dashboard is valBits of type uint64, which stores the real-time value represented by the gauge indicator.

However, we see that the parameters of the Add and Sub methods in the Gauge interface type are both float64 types. It is understandable that the method in the Gauge interface type uses the float64 type as a parameter . This is because Gauge needs to support floating-point numbers and decimals. Floating-point numbers can be converted into integers, but integers cannot be converted into floats with decimal parts. points.

So why is the field of type uint64 instead of float64 used in the gauge type to store the instant value represented by the gauge? This starts with a feature of the Prometheus go client, that is , the modification of the instant value of the Gauge must ensure goroutine-safe . Specifically, gauge uses atomic operations provided by the atomic package to ensure the safety of concurrent access. However, the atomic package of the standard library supports atomic operations of uint64 type, but not of float64 type. It is just that the size of float64 and uint64 is 8 bytes, so Prometheus go client uses uint64 to support atomic operations and uint64 and float64 types Both are 64bits in length. These two points implement the Add and Sub methods of the gauge type:

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

We see that the Sub method actually calls the Add method, but the val value is multiplied by -1 as the parameter of the Add method. Let’s focus on the Add method of gauge next.

The implementation of the gauge Add method is a typical usage mode of CAS (CompareAndSwap) atomic operation , that is, in an infinite loop, first atomically read the current instant value, and then add it to the incoming incremental value to obtain a new value , and finally set the new value to the current immediate value via a CAS operation. If the CAS operation fails, go through the loop again.

However, what deserves our attention is the respective functions and mutual conversion of the float64 and uint64 types in the Add method. The Add method first uses atomic.LoadUint64 to read the value of valBits, and then converts it to float64 type through math.Float64frombits, and then uses the obtained instant value of float64 type to add to val to get the new value we want. The next thing is to store it back into valBits. float64 does not support atomic operations, so before calling CAS, the Add method needs to convert the new value back to uint64, which is why the above code calls math.Float64bits, and then saves the new uint64 type of float64 bit mode through atomic.CompareAndSwapUint64 The value newBits is written into valBits.

Everyone must be curious about how math.Float64frombits and math.Float64bits do the conversion between uint64 and float64. Let’s take a look at their implementation:

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

We see that these two functions just use the unsafe package for type conversion, but do not do any arithmetic operations.

For how to use the unsafe package for safe type conversion, please refer to Article 58 of “Mastering the Safe Use Mode of the Unsafe Package” in my book “The Road to Go Language Improvement “.

In summary:

  • The uint64 type valBits in the gauge structure is essentially only used as the “carrier” of the float64 value, and uses atomic operations to support its type to achieve instant value updates. It does not participate in any integer or floating-point calculations. ;
  • The operations in the Add method are performed between floating-point types. The Add method restores the IEEE 754-compliant floating-point number representation carried in uint64 to a floating-point number type through math.Float64frombits, and then uses the input parameter of the same float64 type Addition calculation is performed, and the calculated result is converted to uint64 type by the math.Float64bits function. During this process, the bit pattern of the 8-byte field does not change. Finally, the result value (new bit pattern) is written into valBits through the CAS operation.

What is stored in valBits is the bit pattern of a floating-point number that meets IEEE 754 . In the IEEE 754 specification, a floating-point number is composed of “sign bit + exponent code + mantissa”. For details, please refer to the 12th basic data type of my “Go Language Lesson 1” column : What are the numerical types natively supported by Go .

3. Summary

The mode used by the gauge structure and its Add method to implement float64 atomic operations through bit pattern conversion is worth learning.


“Gopher Tribe” knowledge planet aims to create a boutique Go learning and advanced community! High-quality first release of Go technical articles, “three days” first reading right, analysis of the development status of Go language twice a year, fresh Gopher daily 1 hour in advance every day, online courses, technical columns, book content preview, must answer within six hours Guaranteed to meet all your needs about the Go language ecology! In 2022, the Gopher tribe will be fully revised, and will continue to share knowledge, skills and practices in the Go language and Go application fields, and add many interactive forms. Everyone is welcome to join!

img{512x368}

img{512x368}

img{512x368}

img{512x368}

The well-known cloud hosting service provider DigitalOcean released the latest hosting plan. The entry-level Droplet configuration is upgraded to: 1 core CPU, 1G memory, 25G high-speed SSD, and the price is 5$/month. Friends who need to use DigitalOcean, you can open this link address : https://ift.tt/LVEB0HI to start your DO host road.

Gopher Daily (Gopher Daily News) archive repository – https://ift.tt/H14shSW

my contact information:

  • Weibo (temporarily unavailable): https://ift.tt/Z0DlcVM
  • Weibo 2: https://ift.tt/Q1tlpf9
  • Blog: tonybai.com
  • github: https://ift.tt/Igqpbc6

Business cooperation methods: writing, publishing, training, online courses, partnerships, consulting, advertising cooperation.

© 2023, bigwhite . All rights reserved.

This article is transferred from https://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub/
This site is only for collection, and the copyright belongs to the original author.