Skip to content

Commit 94ae175

Browse files
authored
Merge pull request #7886 from plkokanov/fix/empty-histogram-after-load-from-checkpoint
Fixes histograms becoming empty after loaded from checkpoints
2 parents 6330997 + 4a233bf commit 94ae175

File tree

2 files changed

+55
-0
lines changed

2 files changed

+55
-0
lines changed

vertical-pod-autoscaler/pkg/recommender/util/histogram.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,15 @@ func (h *histogram) LoadFromCheckpoint(checkpoint *vpa_types.HistogramCheckpoint
278278
}
279279
h.totalWeight += checkpoint.TotalWeight
280280

281+
// In some cases where the weight of the max bucket is close (equal or less) to `MaxCheckpointWeight` times epsilon
282+
// and there are buckets with weights slightly higher or equal to epsilon, saving the histogram to a checkpoint and
283+
// then loading it will cause the weights that are close to epsilon to become smaller than epsilon due to rounding errors
284+
// and differences between the load and save algorithm. If one of those weights is the min weight, this will cause the
285+
// histogram to incorrectly become "empty" and the `Percentile(...)` function to always return 0.
286+
// To cover for such cases, the min and max buckets are updated here, so that those less than epsilon are dropped.
287+
// For more information check https://github.com/kubernetes/autoscaler/issues/7726
288+
h.updateMinAndMaxBucket()
289+
281290
return nil
282291
}
283292

vertical-pod-autoscaler/pkg/recommender/util/histogram_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,52 @@ func TestHistogramLoadFromCheckpointReturnsErrorOnNilInput(t *testing.T) {
265265
assert.Error(t, err)
266266
}
267267

268+
func TestHistogramIsNotEmptyAfterSavingAndLoadingCheckpointsWithBoundaryValues(t *testing.T) {
269+
// There is a specific scenario in which the weights of the minimum and maximum histogram buckets,
270+
// when saved to a VPACheckpoint and subsequently loaded, result in diminished weights for the minimum buckets.
271+
272+
// This issue arises due to rounding errors when converting float weights to integers in the VPACheckpoint.
273+
// For instance, consider the weights:
274+
// `w1` which approximates but is slightly larger than or equal to `epsilon`,
275+
// `w2` which approximates but is slightly smaller than or equal to (`MaxCheckpointWeight` * `epsilon`) - `epsilon`.
276+
277+
// When these weights are stored in a VPACheckpoint, they are translated to integers:
278+
// `w1` rounds to `1` (`wi1`),
279+
// `w2` rounds to `MaxCheckpointWeight` (`wi2`).
280+
281+
// Upon loading from the VPACheckpoint, the histogram reconstructs its weights using a calculated ratio,
282+
// aimed at reverting integer weights back to float values. This ratio is derived from:
283+
// (`w1` + `w2`) / (`wi1` + `wi2`)
284+
// Reference: https://github.com/kubernetes/autoscaler/blob/aa1d413ea3bf319b56c7b2e65ade1a028e149439/vertical-pod-autoscaler//pkg/recommender/util/histogram.go#L256-L269
285+
286+
// Given the maximum potential values for `w1`, `w2`, `wi1` and `wi2` we arrive at:
287+
// (`epsilon` + `MaxCheckpointWeight` * `epsilon` - `epsilon`) / (1 + MaxCheckpointWeight) = epsilon * `MaxCheckpointWeight` / (1 + MaxCheckpointWeight)
288+
289+
// Consequently, the maximum value for this ratio is less than `epsilon`, implying that when `w1`,
290+
// initially scaled to `1`, is adjusted by this ratio, its recalculated weight falls short of `epsilon`.
291+
// When the `minBucket`'s weight is less than `epsilon`, the `histogram.IsEmpty()` returns true.
292+
// Reference: https://github.com/kubernetes/autoscaler/blob/aa1d413ea3bf319b56c7b2e65ade1a028e149439/vertical-pod-autoscaler/pkg/recommender/util/histogram.go#L181-L183
293+
// Consequently, the `histogram.Percentile(...)` function will always return 0.
294+
// Reference: https://github.com/kubernetes/autoscaler/blob/aa1d413ea3bf319b56c7b2e65ade1a028e149439/vertical-pod-autoscaler/pkg/recommender/util/histogram.go#L159-L162
295+
// The same behavior can be observed when there are more than two weights.
296+
297+
// This test ensures that in such cases the histogram does not become empty.
298+
// For more information check https://github.com/kubernetes/autoscaler/issues/7726
299+
300+
histogram := NewHistogram(testHistogramOptions)
301+
histogram.AddSample(1, weightEpsilon, anyTime)
302+
histogram.AddSample(2, (float64(MaxCheckpointWeight)*weightEpsilon - weightEpsilon), anyTime)
303+
assert.False(t, histogram.IsEmpty())
304+
305+
checkpoint, err := histogram.SaveToChekpoint()
306+
assert.NoError(t, err)
307+
308+
newHistogram := NewHistogram(testHistogramOptions)
309+
err = newHistogram.LoadFromCheckpoint(checkpoint)
310+
assert.NoError(t, err)
311+
assert.False(t, newHistogram.IsEmpty())
312+
}
313+
268314
func areUnique(values ...interface{}) bool {
269315
dict := make(map[interface{}]bool)
270316
for i, v := range values {

0 commit comments

Comments
 (0)