Skip to content

Commit 0fa3a27

Browse files
ultrotterGuido Trotter
andauthored
Move Query locking back into private query function (#4694)
* Add parallel benchmarks to silence_bench_test Add benchmarks about parallel queries, parallel queries in concurrency with adds, and about running mutes. Signed-off-by: Guido Trotter <[email protected]> * Move Query locking back into private query function This was changed in b67bde8 because Set called query, while already holding a lock, but that call was removed in dbe6312 and replaced with a simple len(s.st) call, so locking can move back to the inner function. Signed-off-by: Guido Trotter <[email protected]> --------- Signed-off-by: Guido Trotter <[email protected]> Co-authored-by: Guido Trotter <[email protected]>
1 parent e9aaa7c commit 0fa3a27

File tree

2 files changed

+258
-3
lines changed

2 files changed

+258
-3
lines changed

silence/silence.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,9 +794,6 @@ func (s *Silences) QueryOne(params ...QueryParam) (*pb.Silence, error) {
794794
// Query for silences based on the given query parameters. It returns the
795795
// resulting silences and the state version the result is based on.
796796
func (s *Silences) Query(params ...QueryParam) ([]*pb.Silence, int, error) {
797-
s.mtx.Lock()
798-
defer s.mtx.Unlock()
799-
800797
s.metrics.queriesTotal.Inc()
801798
defer prometheus.NewTimer(s.metrics.queryDuration).ObserveDuration()
802799

@@ -836,6 +833,9 @@ func (s *Silences) query(q *query, now time.Time) ([]*pb.Silence, int, error) {
836833
// the use of post-filter functions is the trivial solution for now.
837834
var res []*pb.Silence
838835

836+
s.mtx.Lock()
837+
defer s.mtx.Unlock()
838+
839839
if q.ids != nil {
840840
for _, id := range q.ids {
841841
if s, ok := s.st[id]; ok {

silence/silence_bench_test.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package silence
1515

1616
import (
1717
"strconv"
18+
"sync"
1819
"testing"
1920
"time"
2021

@@ -153,3 +154,257 @@ func benchmarkQuery(b *testing.B, numSilences int) {
153154
require.Len(b, sils, numSilences/10)
154155
}
155156
}
157+
158+
// BenchmarkQueryParallel benchmarks concurrent queries to demonstrate
159+
// the performance improvement from using read locks (RLock) instead of
160+
// write locks (Lock). With the pre-compiled matcher cache, multiple
161+
// queries can now execute in parallel.
162+
func BenchmarkQueryParallel(b *testing.B) {
163+
b.Run("100 silences, 1 goroutine", func(b *testing.B) {
164+
benchmarkQueryParallel(b, 100, 1)
165+
})
166+
b.Run("100 silences, 2 goroutines", func(b *testing.B) {
167+
benchmarkQueryParallel(b, 100, 2)
168+
})
169+
b.Run("100 silences, 4 goroutines", func(b *testing.B) {
170+
benchmarkQueryParallel(b, 100, 4)
171+
})
172+
b.Run("100 silences, 8 goroutines", func(b *testing.B) {
173+
benchmarkQueryParallel(b, 100, 8)
174+
})
175+
b.Run("1000 silences, 1 goroutine", func(b *testing.B) {
176+
benchmarkQueryParallel(b, 1000, 1)
177+
})
178+
b.Run("1000 silences, 2 goroutines", func(b *testing.B) {
179+
benchmarkQueryParallel(b, 1000, 2)
180+
})
181+
b.Run("1000 silences, 4 goroutines", func(b *testing.B) {
182+
benchmarkQueryParallel(b, 1000, 4)
183+
})
184+
b.Run("1000 silences, 8 goroutines", func(b *testing.B) {
185+
benchmarkQueryParallel(b, 1000, 8)
186+
})
187+
b.Run("10000 silences, 1 goroutine", func(b *testing.B) {
188+
benchmarkQueryParallel(b, 10000, 1)
189+
})
190+
b.Run("10000 silences, 2 goroutines", func(b *testing.B) {
191+
benchmarkQueryParallel(b, 10000, 2)
192+
})
193+
b.Run("10000 silences, 4 goroutines", func(b *testing.B) {
194+
benchmarkQueryParallel(b, 10000, 4)
195+
})
196+
b.Run("10000 silences, 8 goroutines", func(b *testing.B) {
197+
benchmarkQueryParallel(b, 10000, 8)
198+
})
199+
}
200+
201+
func benchmarkQueryParallel(b *testing.B, numSilences, numGoroutines int) {
202+
s, err := New(Options{})
203+
require.NoError(b, err)
204+
205+
clock := quartz.NewMock(b)
206+
s.clock = clock
207+
now := clock.Now()
208+
209+
lset := model.LabelSet{"aaaa": "AAAA", "bbbb": "BBBB", "cccc": "CCCC"}
210+
211+
// Create silences with pre-compiled matchers
212+
for i := 0; i < numSilences; i++ {
213+
id := strconv.Itoa(i)
214+
patA := "A{4}|" + id
215+
patB := id
216+
if i%10 == 0 {
217+
patB = "B(B|C)B.|" + id
218+
}
219+
220+
sil := &silencepb.Silence{
221+
Matchers: []*silencepb.Matcher{
222+
{Type: silencepb.Matcher_REGEXP, Name: "aaaa", Pattern: patA},
223+
{Type: silencepb.Matcher_REGEXP, Name: "bbbb", Pattern: patB},
224+
},
225+
StartsAt: now.Add(-time.Minute),
226+
EndsAt: now.Add(time.Hour),
227+
UpdatedAt: now.Add(-time.Hour),
228+
}
229+
require.NoError(b, s.Set(sil))
230+
}
231+
232+
// Verify initial query works
233+
sils, _, err := s.Query(
234+
QState(types.SilenceStateActive),
235+
QMatches(lset),
236+
)
237+
require.NoError(b, err)
238+
require.Len(b, sils, numSilences/10)
239+
240+
b.ResetTimer()
241+
242+
// Run queries in parallel across multiple goroutines
243+
b.RunParallel(func(pb *testing.PB) {
244+
for pb.Next() {
245+
sils, _, err := s.Query(
246+
QState(types.SilenceStateActive),
247+
QMatches(lset),
248+
)
249+
if err != nil {
250+
b.Error(err)
251+
}
252+
if len(sils) != numSilences/10 {
253+
b.Errorf("expected %d silences, got %d", numSilences/10, len(sils))
254+
}
255+
}
256+
})
257+
}
258+
259+
// BenchmarkQueryWithConcurrentAdds benchmarks the behavior when queries
260+
// are running concurrently with silence additions. This demonstrates how
261+
// the system handles read-heavy workloads with occasional writes.
262+
func BenchmarkQueryWithConcurrentAdds(b *testing.B) {
263+
b.Run("1000 initial silences, 10% add rate", func(b *testing.B) {
264+
benchmarkQueryWithConcurrentAdds(b, 1000, 0.1)
265+
})
266+
b.Run("1000 initial silences, 1% add rate", func(b *testing.B) {
267+
benchmarkQueryWithConcurrentAdds(b, 1000, 0.01)
268+
})
269+
b.Run("1000 initial silences, 0.1% add rate", func(b *testing.B) {
270+
benchmarkQueryWithConcurrentAdds(b, 1000, 0.001)
271+
})
272+
b.Run("10000 initial silences, 1% add rate", func(b *testing.B) {
273+
benchmarkQueryWithConcurrentAdds(b, 10000, 0.01)
274+
})
275+
b.Run("10000 initial silences, 0.1% add rate", func(b *testing.B) {
276+
benchmarkQueryWithConcurrentAdds(b, 10000, 0.001)
277+
})
278+
}
279+
280+
func benchmarkQueryWithConcurrentAdds(b *testing.B, initialSilences int, addRatio float64) {
281+
s, err := New(Options{})
282+
require.NoError(b, err)
283+
284+
clock := quartz.NewMock(b)
285+
s.clock = clock
286+
now := clock.Now()
287+
288+
lset := model.LabelSet{"aaaa": "AAAA", "bbbb": "BBBB", "cccc": "CCCC"}
289+
290+
// Create initial silences
291+
for i := 0; i < initialSilences; i++ {
292+
id := strconv.Itoa(i)
293+
patA := "A{4}|" + id
294+
patB := id
295+
if i%10 == 0 {
296+
patB = "B(B|C)B.|" + id
297+
}
298+
299+
sil := &silencepb.Silence{
300+
Matchers: []*silencepb.Matcher{
301+
{Type: silencepb.Matcher_REGEXP, Name: "aaaa", Pattern: patA},
302+
{Type: silencepb.Matcher_REGEXP, Name: "bbbb", Pattern: patB},
303+
},
304+
StartsAt: now.Add(-time.Minute),
305+
EndsAt: now.Add(time.Hour),
306+
UpdatedAt: now.Add(-time.Hour),
307+
}
308+
require.NoError(b, s.Set(sil))
309+
}
310+
311+
var addCounter int
312+
var mu sync.Mutex
313+
314+
b.ResetTimer()
315+
316+
// Run parallel operations
317+
b.RunParallel(func(pb *testing.PB) {
318+
for pb.Next() {
319+
// Determine if this iteration should add a silence
320+
mu.Lock()
321+
shouldAdd := float64(addCounter) < float64(b.N)*addRatio
322+
if shouldAdd {
323+
addCounter++
324+
}
325+
localCounter := addCounter + initialSilences
326+
mu.Unlock()
327+
328+
if shouldAdd {
329+
// Add a new silence
330+
id := strconv.Itoa(localCounter)
331+
patA := "A{4}|" + id
332+
patB := "B(B|C)B.|" + id
333+
334+
sil := &silencepb.Silence{
335+
Matchers: []*silencepb.Matcher{
336+
{Type: silencepb.Matcher_REGEXP, Name: "aaaa", Pattern: patA},
337+
{Type: silencepb.Matcher_REGEXP, Name: "bbbb", Pattern: patB},
338+
},
339+
StartsAt: now.Add(-time.Minute),
340+
EndsAt: now.Add(time.Hour),
341+
UpdatedAt: now.Add(-time.Hour),
342+
}
343+
if err := s.Set(sil); err != nil {
344+
b.Error(err)
345+
}
346+
} else {
347+
// Query silences (the common operation)
348+
_, _, err := s.Query(
349+
QState(types.SilenceStateActive),
350+
QMatches(lset),
351+
)
352+
if err != nil {
353+
b.Error(err)
354+
}
355+
}
356+
}
357+
})
358+
}
359+
360+
// BenchmarkMutesParallel benchmarks concurrent Mutes calls to demonstrate
361+
// the performance improvement from parallel query execution.
362+
func BenchmarkMutesParallel(b *testing.B) {
363+
b.Run("100 silences, 4 goroutines", func(b *testing.B) {
364+
benchmarkMutesParallel(b, 100, 4)
365+
})
366+
b.Run("1000 silences, 4 goroutines", func(b *testing.B) {
367+
benchmarkMutesParallel(b, 1000, 4)
368+
})
369+
b.Run("10000 silences, 4 goroutines", func(b *testing.B) {
370+
benchmarkMutesParallel(b, 10000, 4)
371+
})
372+
b.Run("10000 silences, 8 goroutines", func(b *testing.B) {
373+
benchmarkMutesParallel(b, 10000, 8)
374+
})
375+
}
376+
377+
func benchmarkMutesParallel(b *testing.B, numSilences, numGoroutines int) {
378+
silences, err := New(Options{})
379+
require.NoError(b, err)
380+
381+
clock := quartz.NewMock(b)
382+
silences.clock = clock
383+
now := clock.Now()
384+
385+
// Create silences that will match the alert
386+
for i := 0; i < numSilences; i++ {
387+
s := &silencepb.Silence{
388+
Matchers: []*silencepb.Matcher{{
389+
Type: silencepb.Matcher_EQUAL,
390+
Name: "foo",
391+
Pattern: "bar",
392+
}},
393+
StartsAt: now,
394+
EndsAt: now.Add(time.Minute),
395+
}
396+
require.NoError(b, silences.Set(s))
397+
}
398+
399+
m := types.NewMarker(prometheus.NewRegistry())
400+
silencer := NewSilencer(silences, m, promslog.NewNopLogger())
401+
402+
b.ResetTimer()
403+
404+
// Run Mutes in parallel
405+
b.RunParallel(func(pb *testing.PB) {
406+
for pb.Next() {
407+
silencer.Mutes(model.LabelSet{"foo": "bar"})
408+
}
409+
})
410+
}

0 commit comments

Comments
 (0)