Skip to content

Commit 9e4fdd0

Browse files
dqminhbobrik
authored andcommitted
add a negative cache to ebpf exporter
Often, some decoders such as regexp can run repeatedly on the same input and skip them with regexp filter. An common example is matching cgroup path in a chain like so: ``` - name: cgroup - name: regexp regexps: - ^.*(system.slice).*$ ``` Anything that is not in system.slice cgroup will be skipped. When only a small subset of inputs is matched, the overhead of regexp matching can often be noticable. We add a skip cache here to test for input that would produce ErrSkipLabelSet and skip regex matching on them to reduce the work done on regexp matching. The cache size is customizable with the flag `config.skip-cache-size` Signed-off-by: Daniel Dao <dqminh@cloudflare.com>
1 parent 4488c71 commit 9e4fdd0

File tree

7 files changed

+153
-14
lines changed

7 files changed

+153
-14
lines changed

cmd/ebpf_exporter/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func main() {
4141
metricsPath := kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()
4242
capabilities := kingpin.Flag("capabilities.keep", "Comma separated list of capabilities to keep (cap_syslog, cap_bpf, etc.), 'all' or 'none'").Default("all").String()
4343
btfPath := kingpin.Flag("btf.path", "Optional BTF file path.").Default("").String()
44+
skipCacheSize := kingpin.Flag("config.skip-cache-size", "Size of the LRU skip cache").Int()
4445
kingpin.Version(version.Print("ebpf_exporter"))
4546
kingpin.HelpFlag.Short('h')
4647
kingpin.Parse()
@@ -92,7 +93,7 @@ func main() {
9293

9394
notify("creating exporter...")
9495

95-
e, err := exporter.New(configs, tracing.NewProvider(processor), *btfPath)
96+
e, err := exporter.New(configs, *skipCacheSize, tracing.NewProvider(processor), *btfPath)
9697
if err != nil {
9798
log.Fatalf("Error creating exporter: %s", err)
9899
}

decoder/decoder.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/cloudflare/ebpf_exporter/v2/config"
99
"github.com/cloudflare/ebpf_exporter/v2/kallsyms"
10+
lru "github.com/hashicorp/golang-lru/v2"
1011
)
1112

1213
// ErrSkipLabelSet instructs exporter to skip label set
@@ -21,13 +22,14 @@ type Decoder interface {
2122

2223
// Set is a set of Decoders that may be applied to produce a label
2324
type Set struct {
24-
mu sync.Mutex
25-
decoders map[string]Decoder
26-
cache map[string]map[string][]string
25+
mu sync.Mutex
26+
decoders map[string]Decoder
27+
cache map[string]map[string][]string
28+
skipCache *lru.Cache[string, struct{}]
2729
}
2830

2931
// NewSet creates a Set with all known decoders
30-
func NewSet() (*Set, error) {
32+
func NewSet(skipCacheSize int) (*Set, error) {
3133
cgroup, err := NewCgroupDecoder()
3234
if err != nil {
3335
return nil, fmt.Errorf("error creating cgroup decoder: %w", err)
@@ -38,7 +40,7 @@ func NewSet() (*Set, error) {
3840
return nil, fmt.Errorf("error creating ksym decoder: %w", err)
3941
}
4042

41-
return &Set{
43+
s := &Set{
4244
decoders: map[string]Decoder{
4345
"cgroup": cgroup,
4446
"dname": &Dname{},
@@ -60,7 +62,16 @@ func NewSet() (*Set, error) {
6062
"uint": &UInt{},
6163
},
6264
cache: map[string]map[string][]string{},
63-
}, nil
65+
}
66+
67+
if skipCacheSize > 0 {
68+
skipCache, err := lru.New[string, struct{}](skipCacheSize)
69+
if err != nil {
70+
return nil, err
71+
}
72+
s.skipCache = skipCache
73+
}
74+
return s, nil
6475
}
6576

6677
// decode transforms input byte field into a string according to configuration
@@ -75,6 +86,9 @@ func (s *Set) decode(in []byte, label config.Label) ([]byte, error) {
7586
decoded, err := s.decoders[decoder.Name].Decode(result, decoder)
7687
if err != nil {
7788
if errors.Is(err, ErrSkipLabelSet) {
89+
if s.skipCache != nil {
90+
s.skipCache.Add(string(in), struct{}{})
91+
}
7892
return decoded, err
7993
}
8094

@@ -106,6 +120,14 @@ func (s *Set) DecodeLabelsForMetrics(in []byte, name string, labels []config.Lab
106120
return cached, nil
107121
}
108122

123+
// Also check the skip cache if the input would have return ErrSkipLabelSet
124+
// and return the error early.
125+
if s.skipCache != nil {
126+
if _, ok := s.skipCache.Get(string(in)); ok {
127+
return nil, ErrSkipLabelSet
128+
}
129+
}
130+
109131
values, err := s.decodeLabels(in, labels)
110132
if err != nil {
111133
return nil, err

decoder/decoder_test.go

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package decoder
22

33
import (
4+
"errors"
45
"fmt"
56
"sync"
67
"testing"
@@ -148,7 +149,7 @@ func TestDecodeLabels(t *testing.T) {
148149
}
149150

150151
for i, c := range cases {
151-
s, err := NewSet()
152+
s, err := NewSet(0)
152153
if err != nil {
153154
t.Fatal(err)
154155
}
@@ -178,6 +179,118 @@ func TestDecodeLabels(t *testing.T) {
178179
}
179180
}
180181

182+
func TestDecodeSkipLabels(t *testing.T) {
183+
cases := []struct {
184+
in []byte
185+
skipCacheIn string
186+
labels []config.Label
187+
out []string
188+
err bool
189+
}{
190+
{
191+
in: append([]byte{0x8, 0x0, 0x0, 0x0}, zeroPaddedString("bananas", 32)...),
192+
skipCacheIn: "",
193+
labels: []config.Label{
194+
{
195+
Name: "number",
196+
Size: 4,
197+
Decoders: []config.Decoder{
198+
{
199+
Name: "uint",
200+
},
201+
},
202+
},
203+
{
204+
Name: "fruit",
205+
Size: 32,
206+
Decoders: []config.Decoder{
207+
{
208+
Name: "string",
209+
},
210+
{
211+
Name: "regexp",
212+
Regexps: []string{
213+
"^bananas$",
214+
"$is-banana-even-fruit$",
215+
},
216+
},
217+
},
218+
},
219+
},
220+
out: []string{"8", "bananas"},
221+
err: false,
222+
},
223+
{
224+
in: append([]byte{0x8, 0x0, 0x0, 0x0}, zeroPaddedString("bananas", 32)...),
225+
skipCacheIn: string(zeroPaddedString("bananas", 32)),
226+
labels: []config.Label{
227+
{
228+
Name: "number",
229+
Size: 4,
230+
Decoders: []config.Decoder{
231+
{
232+
Name: "uint",
233+
},
234+
},
235+
},
236+
{
237+
Name: "fruit",
238+
Size: 32,
239+
Decoders: []config.Decoder{
240+
{
241+
Name: "string",
242+
},
243+
{
244+
Name: "regexp",
245+
Regexps: []string{
246+
"^tomato$",
247+
},
248+
},
249+
},
250+
},
251+
},
252+
out: []string{"8", "bananas"},
253+
err: true, // this label should be skipped, only tomatoes allowed
254+
},
255+
}
256+
257+
for i, c := range cases {
258+
s, err := NewSet(100)
259+
if err != nil {
260+
t.Fatal(err)
261+
}
262+
263+
out, err := s.DecodeLabelsForMetrics(c.in, fmt.Sprintf("test:%d", i), c.labels)
264+
if c.err {
265+
if err == nil {
266+
t.Errorf("Expected error for input %#v and labels %#v, but did not receive it", c.in, c.labels)
267+
}
268+
269+
if errors.Is(err, ErrSkipLabelSet) {
270+
if !s.skipCache.Contains(c.skipCacheIn) {
271+
t.Errorf("Expected skipCache to have input %#v", c.skipCacheIn)
272+
}
273+
}
274+
275+
continue
276+
}
277+
278+
if err != nil {
279+
t.Errorf("Error decoding %#v with labels set to %#v: %s", c.in, c.labels, err)
280+
}
281+
282+
if len(c.out) != len(out) {
283+
t.Errorf("Expected %d outputs (%v), received %d (%v)", len(c.out), c.out, len(out), out)
284+
}
285+
286+
for i := 0; i < len(c.out) && i < len(out); i++ {
287+
if c.out[i] != out[i] {
288+
t.Errorf("Output label %d for input %#v is wrong: expected %s, but received %s", i, c.in, c.out[i], out[i])
289+
}
290+
}
291+
}
292+
}
293+
181294
func TestDecoderSetConcurrency(t *testing.T) {
182295
in := append([]byte{0x8, 0x0, 0x0, 0x0}, zeroPaddedString("bananas", 32)...)
183296

@@ -209,7 +322,7 @@ func TestDecoderSetConcurrency(t *testing.T) {
209322
},
210323
}
211324

212-
s, err := NewSet()
325+
s, err := NewSet(0)
213326
if err != nil {
214327
t.Fatal(err)
215328
}
@@ -274,7 +387,7 @@ func TestDecoderSetCache(t *testing.T) {
274387
},
275388
}
276389

277-
s, err := NewSet()
390+
s, err := NewSet(0)
278391
if err != nil {
279392
t.Fatal(err)
280393
}
@@ -345,7 +458,7 @@ func BenchmarkCache(b *testing.B) {
345458
},
346459
}
347460

348-
s, err := NewSet()
461+
s, err := NewSet(0)
349462
if err != nil {
350463
b.Fatal(err)
351464
}

exporter/exporter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type Exporter struct {
5656
}
5757

5858
// New creates a new exporter with the provided config
59-
func New(configs []config.Config, tracingProvider tracing.Provider, btfPath string) (*Exporter, error) {
59+
func New(configs []config.Config, skipCacheSize int, tracingProvider tracing.Provider, btfPath string) (*Exporter, error) {
6060
enabledConfigsDesc := prometheus.NewDesc(
6161
prometheus.BuildFQName(prometheusNamespace, "", "enabled_configs"),
6262
"The set of enabled configs",
@@ -101,7 +101,7 @@ func New(configs []config.Config, tracingProvider tracing.Provider, btfPath stri
101101
decoderErrorCount.WithLabelValues(config.Name).Add(0.0)
102102
}
103103

104-
decoders, err := decoder.NewSet()
104+
decoders, err := decoder.NewSet(skipCacheSize)
105105
if err != nil {
106106
return nil, fmt.Errorf("error creating decoder set: %w", err)
107107
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/aquasecurity/libbpfgo v0.8.0-libbpf-1.5
88
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
99
github.com/elastic/go-perf v0.0.0-20191212140718-9c656876f595
10+
github.com/hashicorp/golang-lru/v2 v2.0.7
1011
github.com/iovisor/gobpf v0.2.0
1112
github.com/jaypipes/pcidb v1.0.1
1213
github.com/mdlayher/sdnotify v1.0.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3030
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3131
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
3232
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
33+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
34+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
3335
github.com/iovisor/gobpf v0.2.0 h1:34xkQxft+35GagXBk3n23eqhm0v7q0ejeVirb8sqEOQ=
3436
github.com/iovisor/gobpf v0.2.0/go.mod h1:WSY9Jj5RhdgC3ci1QaacvbFdQ8cbrEjrpiZbLHLt2s4=
3537
github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic=

tracing/extract_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func TestExtractFilled(t *testing.T) {
151151
},
152152
}
153153

154-
decoders, err := decoder.NewSet()
154+
decoders, err := decoder.NewSet(0)
155155
if err != nil {
156156
t.Fatalf("Error creating decoders set: %v", err)
157157
}

0 commit comments

Comments
 (0)