Skip to content

Commit 2f97c89

Browse files
[8.19](backport #48296) libbeat: memoize asset decoding per beat (#48376)
* libbeat: memoize asset decoding per beat (#48296) Asset decoding is an expensive operation that happens during Beat initialization. For standalone beats it runs only once at startup, so the cost is amortized over the execution time. For Beats receivers, multiple beat instances can be started by the collector in the same process, delaying startup. Since asset data is static per beat, decoded assets can be reused if the same Beat was already initialized. This change adds a global asset cache protected by a mutex, ensuring each Beat is decoded only once, with subsequent instances reading from the cache. This works because we only set fields in the registry during program initialization, so calls to GetFields are immutable per beat. While at it, cache the fqdn resolution that takes a significant portion of the remaining cpu time. Real collector (oteltestcol with 4 filebeat receivers) benchmarks Measured time to first log ingested: sec/op 46.01m ± 2% 10.74m ± 1% -76.67% B/op 47.694Mi ± 0% 5.186Mi ± 4% -89.13% allocs/op 49.82k ± 0% 39.22k ± 0% -21.28% (cherry picked from commit aa318d4) # Conflicts: # libbeat/asset/registry.go * fix conflicts --------- Co-authored-by: Mauri de Souza Meneguzzo <mauri870@gmail.com>
1 parent 1509272 commit 2f97c89

4 files changed

Lines changed: 111 additions & 9 deletions

File tree

libbeat/asset/registry.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ import (
2222
"compress/zlib"
2323
"encoding/base64"
2424
"sort"
25+
"sync"
2526

2627
"github.com/elastic/elastic-agent-libs/iobuf"
2728
)
2829

30+
var beatFieldsCacheMu sync.Mutex
31+
var beatFieldsCache = map[string][]byte{}
32+
2933
// FieldsRegistry contains a list of fields.yml files
3034
// As each entry is an array of bytes multiple fields.yml can be added under one path.
3135
// This can become useful as we don't have to generate anymore the fields.yml but can
@@ -53,6 +57,10 @@ func SetFields(beat, name string, p Priority, asset func() string) error {
5357

5458
// GetFields returns a byte array contains all fields for the given beat
5559
func GetFields(beat string) ([]byte, error) {
60+
if cached, ok := getBeatFieldsCache(beat); ok {
61+
return cached, nil
62+
}
63+
5664
var fields []byte
5765

5866
// Get all priorities and sort them
@@ -86,6 +94,8 @@ func GetFields(beat string) ([]byte, error) {
8694
}
8795
}
8896
}
97+
98+
setBeatFieldsCache(beat, fields)
8999
return fields, nil
90100
}
91101

@@ -105,6 +115,19 @@ func EncodeData(data string) (string, error) {
105115
return base64.StdEncoding.EncodeToString(zlibBuf.Bytes()), nil
106116
}
107117

118+
func getBeatFieldsCache(beat string) ([]byte, bool) {
119+
beatFieldsCacheMu.Lock()
120+
defer beatFieldsCacheMu.Unlock()
121+
cached, ok := beatFieldsCache[beat]
122+
return cached, ok
123+
}
124+
125+
func setBeatFieldsCache(beat string, fields []byte) {
126+
beatFieldsCacheMu.Lock()
127+
defer beatFieldsCacheMu.Unlock()
128+
beatFieldsCache[beat] = fields
129+
}
130+
108131
// DecodeData base64 decodes the data and uncompresses it
109132
func DecodeData(data string) ([]byte, error) {
110133
decoded, err := base64.StdEncoding.DecodeString(data)

x-pack/filebeat/tests/integration/otel_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,3 +1333,73 @@ func setupRoleMapping(t *testing.T, client *elasticsearch.Client) {
13331333

13341334
require.Equal(t, resp.StatusCode, http.StatusOK, "incorrect response code")
13351335
}
1336+
1337+
func BenchmarkFilebeatOTelCollector(b *testing.B) {
1338+
numReceivers := 4
1339+
1340+
for b.Loop() {
1341+
b.StopTimer()
1342+
tmpDir := b.TempDir()
1343+
1344+
type receiverConfig struct {
1345+
Index int
1346+
PathHome string
1347+
}
1348+
1349+
configData := struct {
1350+
Receivers []receiverConfig
1351+
}{
1352+
Receivers: make([]receiverConfig, numReceivers),
1353+
}
1354+
1355+
for i := range numReceivers {
1356+
configData.Receivers[i] = receiverConfig{
1357+
Index: i + 1,
1358+
PathHome: filepath.Join(tmpDir, strconv.Itoa(i+1)),
1359+
}
1360+
}
1361+
1362+
cfgTemplate := `receivers:
1363+
{{range .Receivers}}
1364+
filebeatreceiver/{{.Index}}:
1365+
filebeat:
1366+
inputs:
1367+
- type: benchmark
1368+
enabled: true
1369+
count: 1
1370+
path.home: {{.PathHome}}
1371+
queue.mem.flush.timeout: 0s
1372+
{{end}}
1373+
exporters:
1374+
debug:
1375+
verbosity: detailed
1376+
service:
1377+
pipelines:
1378+
logs:
1379+
receivers:
1380+
{{range .Receivers}}
1381+
- filebeatreceiver/{{.Index}}
1382+
{{end}}
1383+
exporters:
1384+
- debug
1385+
telemetry:
1386+
logs:
1387+
level: DEBUG
1388+
metrics:
1389+
level: none
1390+
`
1391+
1392+
var configBuffer bytes.Buffer
1393+
require.NoError(b, template.Must(template.New("config").Parse(cfgTemplate)).Execute(&configBuffer, configData))
1394+
1395+
b.StartTimer()
1396+
1397+
col := oteltestcol.New(b, configBuffer.String())
1398+
require.NotNil(b, col)
1399+
require.Eventually(b, func() bool {
1400+
return col.ObservedLogs().
1401+
FilterMessageSnippet("Publish event").Len() == numReceivers
1402+
}, 30*time.Second, 1*time.Millisecond, "expected all receivers to publish events")
1403+
col.Shutdown()
1404+
}
1405+
}

x-pack/libbeat/cmd/instance/beat.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package instance
77
import (
88
"context"
99
"fmt"
10+
"sync"
1011
"time"
1112

1213
"go.opentelemetry.io/collector/consumer"
@@ -38,6 +39,18 @@ import (
3839
// input type.
3940
const receiverPublisherCloseTimeout = 5 * time.Second
4041

42+
var fqdnOnce = sync.OnceValues(func() (string, error) {
43+
h, err := sysinfo.Host()
44+
if err != nil {
45+
return "", fmt.Errorf("failed to get host information: %w", err)
46+
}
47+
48+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
49+
defer cancel()
50+
51+
return h.FQDNWithContext(ctx)
52+
})
53+
4154
// NewBeatForReceiver creates a Beat that will be used in the context of an otel receiver
4255
func NewBeatForReceiver(settings instance.Settings, receiverConfig map[string]any, consumer consumer.Logs, componentID string, core zapcore.Core) (*instance.Beat, error) {
4356
b, err := instance.NewBeat(settings.Name,
@@ -180,15 +193,7 @@ func NewBeatForReceiver(settings instance.Settings, receiverConfig map[string]an
180193
logger.Infof("Beat ID: %v", b.Info.ID)
181194

182195
// Try to get the host's FQDN and set it.
183-
h, err := sysinfo.Host()
184-
if err != nil {
185-
return nil, fmt.Errorf("failed to get host information: %w", err)
186-
}
187-
188-
fqdnLookupCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
189-
defer cancel()
190-
191-
fqdn, err := h.FQDNWithContext(fqdnLookupCtx)
196+
fqdn, err := fqdnOnce()
192197
if err != nil {
193198
// FQDN lookup is "best effort". We log the error, fallback to
194199
// the OS-reported hostname, and move on.

x-pack/libbeat/common/otelbeat/oteltestcol/collector.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ func (c *Collector) ObservedLogs() *observer.ObservedLogs {
9494
return c.observer
9595
}
9696

97+
func (c *Collector) Shutdown() {
98+
c.collector.Shutdown()
99+
}
100+
97101
func getComponent() (otelcol.Factories, error) {
98102
receivers, err := otelcol.MakeFactoryMap(
99103
fbreceiver.NewFactory(),

0 commit comments

Comments
 (0)