1. What is a Gauge?
Those of you who are familiar with Prometheus will know that Prometheus offers four main metric types.
Histogram and Summary are in the same category, but are a little more complex to understand, so we’ll leave that aside for now; Counter only provides an Add method, which is an increasing value, while Gauge, which is also a value, but unlike Counter, provides not only an Add method but also a Sub method. If you have a metric that can be incremented or decremented or need to support negative numbers, then Gauge is clearly a more suitable metric type than Counter.
2. The rationale for the Gauge Add/Sub operation
In the Prometheus Go client package, we see that Gauge is an interface type.
The client package also provides the default implementation type of the interface, gauge.
Looking at the definition of the gauge type, the core field of gauge, which is the instantaneous value of a gauge, is the uint64 type valBits, which stores the instantaneous value represented by the gauge indicator.
However, we see that the arguments to the Add and Sub methods of the Gauge interface type are of type float64. There is no excuse for using float64 as an argument to the methods of the Gauge interface type, because Gauge has to support floating point numbers and decimals, and floating point numbers can be converted to integers, but integers cannot be converted to floating point numbers with a decimal part .
So why does the gauge type use a field of type uint64 rather than a field of type float64 to store the instantaneous value represented by the gauge? This starts with a feature of the Prometheus go client, which is that modifications to the instantaneous value of gauge are
goroutine-safe. specifically, gauge uses atomic operations provided by the atomic package to ensure that this concurrent access is safe. However, the atomic package of the standard library supports atomic operations of type uint64, but not of type float64, and the size of both float64 and uint64 is 8 bytes. The Prometheus go client takes advantage of the fact that uint64 supports atomic operations and that both uint64 and float64 are 64bits long to implement the Add and Sub methods of the gauge type.
We see that the Sub method actually calls the Add method, but multiplies the val value by -1 as an argument to the Add method. Let’s focus on the gauge’s Add method.
The implementation of the gauge Add method is a typical use of the CAS (CompareAndSwap) atomic operation, i.e. in an infinite loop, the current instantaneous value is read atomically, then it is summed with the incoming incremental value to get the new value, and finally the new value is set to the current instantaneous value by the CAS operation. If the CAS operation fails, the loop is repeated.
However, it is worth looking at the respective functions of the
uint64 types in the Add method and their conversion to each other.
The Add method first reads the value of valBits using the
atomic.LoadUint64 atom. It then converts it to
math.Float64frombits, and then adds the instantaneous value of
val to get the new value we want.
The next step is to re-store it in valBits. float64 does not support atomic operations, so the Add method needs to convert the new value back to uint64 before calling CAS, which is why the above code calls math.Float64bits. The newBits is then written to valBits using the atomic.CompareAndSwapUint64 method.
You must be wondering how math.Float64frombits and math.Float64bits do the conversion between uint64 and float64, let’s take a look at their implementation.
We see that these two functions only do type conversions using the unsafe package and do not do any arithmetic operations.
- the valBits of type uint64 in the gauge structure are essentially only used as “carriers” for float64 values, and are updated in real time with the support of atomic operations on their type; they do not themselves participate in any integer or floating-point calculations.
- The operations in the Add method are performed between floating-point types. The Add method reduces the IEEE 754-compliant floating-point representation carried in uint64 to a floating-point type via math.Float64frombits, then sums the input parameters, also of type float64, and the result of the calculation is then converted to a uint64 type via the
math.Float64bitsfunction to uint64, without any change in the bit pattern of the 8-byte field, and finally the result value (the new bit pattern) is written to valBits via a CAS operation.
This model of implementing float64 atomic operations via bit-mode conversion, as used by the gauge structure and its Add method, is worth learning from.