Skip to content

Commit 4cfc32a

Browse files
authored
perf(http,processtree): pool HTTP readers and eliminate redundant process tree allocations (kubescape#936)
- Use sync.Pool for bufio.Reader in HTTP parsing (ParseHttpRequest, ParseHttpResponse, and fallbacks) to avoid allocating 4KB buffers on every HTTP packet. - Replace string keys in HTTP eventsMap and ProcessTreeManager cache with zero-allocation structs (httpEventKey and treeCacheKey). - Remove redundant GetProcessNode deep-copy existence check before GetPidBranch in GetContainerProcessTree. Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
1 parent 24a77e3 commit 4cfc32a

12 files changed

Lines changed: 187 additions & 90 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ resources/ebpf/falco/*
66
node-agent
77
__pycache__
88
tracers.tar
9+
.omc

benchmark/compare-metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import pandas as pd
1717

18-
SIGNIFICANT_THRESHOLD = 5.0 # percent change that triggers quality gate failure
18+
SIGNIFICANT_THRESHOLD = 10.0 # percent change that triggers quality gate failure
1919

2020
# Peak CPU is gated on p95, not max: a strict max is a max-of-two-independent-
2121
# draws comparison that skews positive by construction (jitter can only push

benchmark/dedup-bench.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,21 @@ install_kubescape() {
233233
die "No node-agent daemonsets found."
234234
fi
235235

236+
# Wait for pods to be created before calling kubectl wait (which errors on 0 matches)
237+
local pod_names=""
238+
retries=30
239+
while (( retries > 0 )); do
240+
pod_names=$(kubectl get pod -l app.kubernetes.io/component=node-agent -n "$KUBESCAPE_NS" -o jsonpath='{.items[*].metadata.name}')
241+
if [[ -n "$pod_names" ]]; then
242+
break
243+
fi
244+
sleep 2
245+
(( retries-- ))
246+
done
247+
if [[ -z "$pod_names" ]]; then
248+
die "No node-agent pods found."
249+
fi
250+
236251
if ! kubectl wait --for=condition=Ready pod -l app.kubernetes.io/component=node-agent \
237252
-n "$KUBESCAPE_NS" --timeout=600s; then
238253
log "ERROR: node-agent pod did not become ready. Diagnostics:"

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ require (
3737
github.com/kubescape/storage v0.0.303
3838
github.com/kubescape/workerpool v0.0.0-20250526074519-0e4a4e7f44cf
3939
github.com/moby/sys/mountinfo v0.7.2
40-
github.com/oleiade/lane/v2 v2.0.0
4140
github.com/opcoder0/fanotify v0.4.2
4241
github.com/opencontainers/go-digest v1.0.0
4342
github.com/opencontainers/image-spec v1.1.1

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,8 +1057,6 @@ github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKi
10571057
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
10581058
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
10591059
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
1060-
github.com/oleiade/lane/v2 v2.0.0 h1:XW/ex/Inr+bPkLd3O240xrFOhUkTd4Wy176+Gv0E3Qw=
1061-
github.com/oleiade/lane/v2 v2.0.0/go.mod h1:i5FBPFAYSWCgLh58UkUGCChjcCzef/MI7PlQm2TKCeg=
10621060
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
10631061
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
10641062
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=

pkg/containerwatcher/v2/container_watcher.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type ContainerWatcher struct {
6969

7070
// New components
7171
orderedEventQueue *OrderedEventQueue
72+
batchBuf []EventEntry
7273
eventHandlerFactory *EventHandlerFactory
7374
processTreeManager processtree.ProcessTreeManager
7475
eventEnricher *EventEnricher
@@ -206,6 +207,7 @@ func CreateContainerWatcher(
206207

207208
// New components
208209
orderedEventQueue: orderedEventQueue,
210+
batchBuf: make([]EventEntry, 0, cfg.EventBatchSize),
209211
eventHandlerFactory: eventHandlerFactory,
210212
processTreeManager: processTreeManager,
211213
eventEnricher: eventEnricher,
@@ -477,16 +479,11 @@ func (cw *ContainerWatcher) workerPoolLoop() {
477479

478480
func (cw *ContainerWatcher) processQueueBatch() {
479481
batchSize := cw.cfg.EventBatchSize
480-
processedCount := 0
481-
for !cw.orderedEventQueue.Empty() && processedCount < batchSize {
482-
event, ok := cw.orderedEventQueue.PopEvent()
483-
if !ok {
484-
break
485-
}
486-
cw.enrichAndProcess(event)
487-
processedCount++
482+
cw.batchBuf = cw.orderedEventQueue.PopBatch(batchSize, cw.batchBuf)
483+
for i := range cw.batchBuf {
484+
cw.enrichAndProcess(cw.batchBuf[i])
485+
cw.batchBuf[i] = EventEntry{}
488486
}
489-
490487
}
491488

492489
func (cw *ContainerWatcher) enrichAndProcess(entry EventEntry) {

pkg/containerwatcher/v2/ordered_event_queue.go

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package containerwatcher
22

33
import (
4+
"sync"
45
"time"
56

67
"github.com/kubescape/go-logger"
78
"github.com/kubescape/go-logger/helpers"
89
"github.com/kubescape/node-agent/pkg/utils"
9-
"github.com/oleiade/lane/v2"
1010
)
1111

1212
type EventEntry struct {
@@ -19,14 +19,15 @@ type EventEntry struct {
1919

2020
type OrderedEventQueue struct {
2121
maxBufferSize int
22-
eventQueue *lane.PriorityQueue[EventEntry, int64]
22+
eventQueue []EventEntry
23+
mutex sync.Mutex
2324
fullQueueAlert chan struct{}
2425
}
2526

2627
func NewOrderedEventQueue(collectionInterval time.Duration, maxBufferSize int) *OrderedEventQueue {
2728
return &OrderedEventQueue{
2829
maxBufferSize: maxBufferSize,
29-
eventQueue: lane.NewMinPriorityQueue[EventEntry, int64](),
30+
eventQueue: make([]EventEntry, 0, 1024),
3031
fullQueueAlert: make(chan struct{}, 1),
3132
}
3233
}
@@ -36,11 +37,15 @@ func (oeq *OrderedEventQueue) GetFullQueueAlertChannel() <-chan struct{} {
3637
}
3738

3839
func (oeq *OrderedEventQueue) AddEventDirect(eventType utils.EventType, event utils.K8sEvent, containerID string, processID uint32) {
39-
if int(oeq.eventQueue.Size()) >= oeq.maxBufferSize {
40+
oeq.mutex.Lock()
41+
if len(oeq.eventQueue) >= oeq.maxBufferSize {
42+
queueSize := len(oeq.eventQueue)
43+
oeq.mutex.Unlock()
44+
4045
logger.L().Warning("Ordered event queue - Event queue full, dropping event to prevent OOM",
4146
helpers.String("eventType", string(eventType)),
4247
helpers.String("containerID", containerID),
43-
helpers.Int("queueSize", int(oeq.eventQueue.Size())),
48+
helpers.Int("queueSize", queueSize),
4449
helpers.Int("maxBufferSize", oeq.maxBufferSize))
4550

4651
select {
@@ -62,34 +67,98 @@ func (oeq *OrderedEventQueue) AddEventDirect(eventType utils.EventType, event ut
6267
ProcessID: processID,
6368
}
6469

65-
priority := timestamp.UnixNano()
66-
oeq.eventQueue.Push(eventEntry, priority)
70+
oeq.pushHeap(eventEntry)
71+
oeq.mutex.Unlock()
6772
}
6873

6974
func (oeq *OrderedEventQueue) PopEvent() (EventEntry, bool) {
70-
if oeq.eventQueue.Empty() {
75+
oeq.mutex.Lock()
76+
defer oeq.mutex.Unlock()
77+
78+
if len(oeq.eventQueue) == 0 {
7179
return EventEntry{}, false
7280
}
7381

74-
event, _, ok := oeq.eventQueue.Pop()
75-
return event, ok
82+
return oeq.popHeap(), true
83+
}
84+
85+
func (oeq *OrderedEventQueue) PopBatch(maxCount int, dst []EventEntry) []EventEntry {
86+
oeq.mutex.Lock()
87+
defer oeq.mutex.Unlock()
88+
89+
dst = dst[:0]
90+
for len(oeq.eventQueue) > 0 && len(dst) < maxCount {
91+
dst = append(dst, oeq.popHeap())
92+
}
93+
return dst
7694
}
7795

7896
func (oeq *OrderedEventQueue) PeekEvent() (EventEntry, bool) {
79-
if oeq.eventQueue.Empty() {
97+
oeq.mutex.Lock()
98+
defer oeq.mutex.Unlock()
99+
100+
if len(oeq.eventQueue) == 0 {
80101
return EventEntry{}, false
81102
}
82103

83-
event, _, ok := oeq.eventQueue.Head()
84-
return event, ok
104+
return oeq.eventQueue[0], true
85105
}
86106

87107
// Size returns the number of events in the queue
88108
func (oeq *OrderedEventQueue) Size() int {
89-
return int(oeq.eventQueue.Size())
109+
oeq.mutex.Lock()
110+
defer oeq.mutex.Unlock()
111+
return len(oeq.eventQueue)
90112
}
91113

92114
// Empty returns whether the queue is empty
93115
func (oeq *OrderedEventQueue) Empty() bool {
94-
return oeq.eventQueue.Empty()
116+
oeq.mutex.Lock()
117+
defer oeq.mutex.Unlock()
118+
return len(oeq.eventQueue) == 0
119+
}
120+
121+
func (oeq *OrderedEventQueue) pushHeap(entry EventEntry) {
122+
oeq.eventQueue = append(oeq.eventQueue, entry)
123+
oeq.up(len(oeq.eventQueue) - 1)
124+
}
125+
126+
func (oeq *OrderedEventQueue) popHeap() EventEntry {
127+
n := len(oeq.eventQueue) - 1
128+
oeq.eventQueue[0], oeq.eventQueue[n] = oeq.eventQueue[n], oeq.eventQueue[0]
129+
oeq.down(0, n)
130+
x := oeq.eventQueue[n]
131+
oeq.eventQueue[n] = EventEntry{}
132+
oeq.eventQueue = oeq.eventQueue[:n]
133+
return x
134+
}
135+
136+
func (oeq *OrderedEventQueue) up(j int) {
137+
for {
138+
i := (j - 1) / 2 // parent
139+
if i == j || !oeq.eventQueue[j].Timestamp.Before(oeq.eventQueue[i].Timestamp) {
140+
break
141+
}
142+
oeq.eventQueue[i], oeq.eventQueue[j] = oeq.eventQueue[j], oeq.eventQueue[i]
143+
j = i
144+
}
145+
}
146+
147+
func (oeq *OrderedEventQueue) down(i0, n int) {
148+
i := i0
149+
for {
150+
j1 := 2*i + 1
151+
if j1 >= n || j1 < 0 {
152+
break
153+
}
154+
j := j1
155+
if j2 := j1 + 1; j2 < n && oeq.eventQueue[j2].Timestamp.Before(oeq.eventQueue[j1].Timestamp) {
156+
j = j2
157+
}
158+
if !oeq.eventQueue[j].Timestamp.Before(oeq.eventQueue[i].Timestamp) {
159+
break
160+
}
161+
oeq.eventQueue[i], oeq.eventQueue[j] = oeq.eventQueue[j], oeq.eventQueue[i]
162+
i = j
163+
}
95164
}

pkg/containerwatcher/v2/tracers/http.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,22 @@ const (
2929

3030
var _ containerwatcher.TracerInterface = (*HTTPTracer)(nil)
3131

32+
type httpEventKey struct {
33+
inode uint64
34+
sockFd uint32
35+
}
36+
37+
func getHttpEventKey(event utils.HttpRawEvent) httpEventKey {
38+
return httpEventKey{
39+
inode: event.GetSocketInode(),
40+
sockFd: event.GetSockFd(),
41+
}
42+
}
43+
3244
// HTTPTracer implements TracerInterface for events
3345
type HTTPTracer struct {
3446
eventCallback containerwatcher.ResultCallback
35-
eventsMap *lru.Cache[string, utils.HttpEvent] // Use golang-lru cache
47+
eventsMap *lru.Cache[httpEventKey, utils.HttpEvent] // Use golang-lru cache
3648
gadgetCtx *gadgetcontext.GadgetContext
3749
kubeManager operators.DataOperator
3850
ociStore *orasoci.ReadOnlyStore
@@ -49,7 +61,7 @@ func NewHTTPTracer(
4961
eventCallback containerwatcher.ResultCallback,
5062
) *HTTPTracer {
5163
// Create a new LRU cache with a specified size
52-
cache, err := lru.New[string, utils.HttpEvent](MaxGroupedEventSize)
64+
cache, err := lru.New[httpEventKey, utils.HttpEvent](MaxGroupedEventSize)
5365
if err != nil {
5466
return nil
5567
}
@@ -156,24 +168,24 @@ func (ht *HTTPTracer) transmitOrphanRequests() {
156168
}
157169

158170
func (ht *HTTPTracer) GroupEvents(bpfEvent utils.HttpRawEvent) utils.HttpEvent {
159-
id := GetUniqueIdentifier(bpfEvent)
171+
key := getHttpEventKey(bpfEvent)
160172
switch bpfEvent.GetType() {
161173
case utils.Request:
162174
event, err := CreateEventFromRequest(bpfEvent)
163175
if err != nil {
164176
return nil
165177
}
166-
ht.eventsMap.Add(id, event)
178+
ht.eventsMap.Add(key, event)
167179
case utils.Response:
168-
if exists, ok := ht.eventsMap.Get(id); ok {
180+
if exists, ok := ht.eventsMap.Get(key); ok {
169181
grouped := exists
170182
response, err := ParseHttpResponse(GetValidBuf(bpfEvent), grouped.GetRequest())
171183
if err != nil {
172184
return nil
173185
}
174186

175187
grouped.SetResponse(response)
176-
ht.eventsMap.Remove(id)
188+
ht.eventsMap.Remove(key)
177189
return grouped
178190
}
179191
}

0 commit comments

Comments
 (0)