Skip to content

Commit 129853d

Browse files
authored
Merge pull request #3419 from alvaroaleman/limit-cardinality
🐛 Limit depthWithPriorityMetric cardinality to 25
2 parents 43b0e35 + 00b8b07 commit 129853d

File tree

3 files changed

+207
-4
lines changed

3 files changed

+207
-4
lines changed

pkg/internal/metrics/workqueue.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ package metrics
1818

1919
import (
2020
"strconv"
21+
"sync"
2122
"time"
2223

2324
"github.com/prometheus/client_golang/prometheus"
25+
"k8s.io/apimachinery/pkg/util/sets"
2426
"k8s.io/client-go/util/workqueue"
2527
"sigs.k8s.io/controller-runtime/pkg/metrics"
2628
)
@@ -154,17 +156,55 @@ type DepthMetricWithPriority interface {
154156
var _ MetricsProviderWithPriority = WorkqueueMetricsProvider{}
155157

156158
func (WorkqueueMetricsProvider) NewDepthMetricWithPriority(name string) DepthMetricWithPriority {
157-
return &depthWithPriorityMetric{lvs: []string{name, name}}
159+
return &depthWithPriorityMetric{depth: depth, lvs: []string{name, name}, observedPriorities: sets.Set[int]{}}
158160
}
159161

162+
type prometheusGaugeVec interface {
163+
WithLabelValues(lvs ...string) prometheus.Gauge
164+
}
165+
166+
const (
167+
priorityCardinalityExceededPlaceholder = "exceeded_cardinality_limit"
168+
// maxRecommendedUniquePriorities is not scientifically chosen, we assume
169+
// that the 99% use-case is to only use the two priorities that c-r itself
170+
// uses and then leave a bit of leeway for other use-cases.
171+
// We may decide to update this value in the future if we find that a
172+
// a different value is more appropriate.
173+
maxRecommendedUniquePriorities = 25
174+
)
175+
160176
type depthWithPriorityMetric struct {
161-
lvs []string
177+
depth prometheusGaugeVec
178+
lvs []string
179+
180+
observedPrioritiesLock sync.Mutex
181+
priorityCardinalityLimitReached bool
182+
observedPriorities sets.Set[int]
183+
}
184+
185+
func (g *depthWithPriorityMetric) priorityLabel(priority int) string {
186+
g.observedPrioritiesLock.Lock()
187+
defer g.observedPrioritiesLock.Unlock()
188+
189+
if g.priorityCardinalityLimitReached {
190+
return priorityCardinalityExceededPlaceholder
191+
}
192+
193+
g.observedPriorities.Insert(priority)
194+
195+
if g.observedPriorities.Len() > maxRecommendedUniquePriorities {
196+
g.observedPriorities = nil
197+
g.priorityCardinalityLimitReached = true
198+
return priorityCardinalityExceededPlaceholder
199+
}
200+
201+
return strconv.Itoa(priority)
162202
}
163203

164204
func (g *depthWithPriorityMetric) Inc(priority int) {
165-
depth.WithLabelValues(append(g.lvs, strconv.Itoa(priority))...).Inc()
205+
g.depth.WithLabelValues(append(g.lvs, g.priorityLabel(priority))...).Inc()
166206
}
167207

168208
func (g *depthWithPriorityMetric) Dec(priority int) {
169-
depth.WithLabelValues(append(g.lvs, strconv.Itoa(priority))...).Dec()
209+
g.depth.WithLabelValues(append(g.lvs, g.priorityLabel(priority))...).Dec()
170210
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metrics
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestWorkqueue(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Workqueue Metrics Suite")
29+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
Copyright The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metrics
18+
19+
import (
20+
"sync"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
"github.com/prometheus/client_golang/prometheus"
25+
"k8s.io/apimachinery/pkg/util/sets"
26+
)
27+
28+
type fakeGauge struct {
29+
prometheus.Gauge
30+
inc func()
31+
}
32+
33+
func (f *fakeGauge) Inc() { f.inc() }
34+
35+
type fakeGaugeVec struct {
36+
gauges map[string]int
37+
mu sync.Mutex
38+
}
39+
40+
func (f *fakeGaugeVec) WithLabelValues(lvs ...string) prometheus.Gauge {
41+
f.mu.Lock()
42+
defer f.mu.Unlock()
43+
44+
key := ""
45+
for _, lv := range lvs {
46+
key += lv + "|"
47+
}
48+
if _, ok := f.gauges[key]; !ok {
49+
f.gauges[key] = 0
50+
}
51+
return &fakeGauge{inc: func() {
52+
f.mu.Lock()
53+
f.gauges[key]++
54+
f.mu.Unlock()
55+
}}
56+
}
57+
58+
var _ = Describe("depthWithPriorityMetric", func() {
59+
Describe("CardinalityLimit", func() {
60+
type testCase struct {
61+
numUniquePriorities int
62+
expectedKeyCount int
63+
expectedCardinalityExceededVal int
64+
}
65+
66+
DescribeTable("should respect cardinality limits",
67+
func(tc testCase) {
68+
fakeVec := &fakeGaugeVec{gauges: make(map[string]int)}
69+
m := &depthWithPriorityMetric{
70+
depth: fakeVec,
71+
lvs: []string{"test", "test"},
72+
observedPriorities: sets.Set[int]{},
73+
}
74+
75+
wg := &sync.WaitGroup{}
76+
wg.Add(tc.numUniquePriorities)
77+
for i := 1; i <= tc.numUniquePriorities; i++ {
78+
go func() {
79+
m.Inc(i)
80+
wg.Done()
81+
}()
82+
}
83+
wg.Wait()
84+
85+
Expect(fakeVec.gauges).To(HaveLen(tc.expectedKeyCount))
86+
87+
placeholderKey := "test|test|" + priorityCardinalityExceededPlaceholder + "|"
88+
Expect(fakeVec.gauges[placeholderKey]).To(Equal(tc.expectedCardinalityExceededVal))
89+
},
90+
Entry("under limit does not use placeholder", testCase{
91+
numUniquePriorities: 10,
92+
expectedKeyCount: 10,
93+
expectedCardinalityExceededVal: 0,
94+
}),
95+
Entry("at limit does not use placeholder", testCase{
96+
numUniquePriorities: 25,
97+
expectedKeyCount: 25,
98+
expectedCardinalityExceededVal: 0,
99+
}),
100+
Entry("exceeding limit uses placeholder", testCase{
101+
numUniquePriorities: 26,
102+
expectedKeyCount: 26,
103+
expectedCardinalityExceededVal: 1,
104+
}),
105+
Entry("well over limit uses placeholder for all excess", testCase{
106+
numUniquePriorities: 30,
107+
expectedKeyCount: 26,
108+
expectedCardinalityExceededVal: 5,
109+
}),
110+
)
111+
})
112+
113+
It("same priority many adds does not trigger cardinality limit", func() {
114+
fakeVec := &fakeGaugeVec{gauges: make(map[string]int)}
115+
m := &depthWithPriorityMetric{
116+
depth: fakeVec,
117+
lvs: []string{"test", "test"},
118+
observedPriorities: sets.Set[int]{},
119+
}
120+
121+
wg := &sync.WaitGroup{}
122+
wg.Add(200)
123+
for range 200 {
124+
go func() {
125+
m.Inc(1)
126+
wg.Done()
127+
}()
128+
}
129+
wg.Wait()
130+
131+
Expect(fakeVec.gauges).To(HaveLen(1))
132+
Expect(fakeVec.gauges["test|test|1|"]).To(Equal(200))
133+
})
134+
})

0 commit comments

Comments
 (0)