This is an automated email from the ASF dual-hosted git repository.
mrproliu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-rover.git
The following commit(s) were added to refs/heads/main by this push:
new b77436d Fix some issue from logs, panics (#212)
b77436d is described below
commit b77436d99566aae0f766358d359145c77073ef11
Author: mrproliu <[email protected]>
AuthorDate: Mon Jul 6 13:30:25 2026 +0800
Fix some issue from logs, panics (#212)
---
CHANGES.md | 6 +
configs/rover_configs.yaml | 6 +-
docs/en/setup/configuration/pprof.md | 9 +-
pkg/accesslog/collector/protocols/http1.go | 5 +-
pkg/accesslog/collector/protocols/http2.go | 12 +-
pkg/accesslog/collector/protocols/http2_test.go | 186 +++++++++++++++++++++
pkg/accesslog/collector/protocols/protocol.go | 18 +-
pkg/pprof/config.go | 5 +-
pkg/pprof/module.go | 2 +-
pkg/process/api/process.go | 2 +
pkg/process/finders/base/tool.go | 10 ++
pkg/process/finders/kubernetes/finder.go | 2 +-
pkg/process/finders/kubernetes/process.go | 41 +++--
pkg/tools/btf/linker.go | 4 +-
pkg/tools/btf/lostsamples.go | 75 +++++++++
.../config.go => tools/btf/lostsamples_test.go} | 23 ++-
pkg/tools/process/process.go | 34 ++--
17 files changed, 394 insertions(+), 46 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 6b72e40..63f97d4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -27,10 +27,16 @@ Release Notes.
* Support detect ztunnel environment in the inbound request.
* Increase the transmit buffer size in the network profiling and access log
module.
* Reduce eBPF verifier complexity by converting hot inline helpers to
BPF-to-BPF calls, fixing the "program too large" failure when loading access
log programs on newer kernels.
+* Reduce memory by not retaining ELF symbols for processes that are not being
profiled.
+* Increase the default access log protocol per-CPU buffer to 1MB to reduce
dropped samples.
+* Aggregate dropped perf event sample warnings periodically instead of logging
every burst.
+* Lower the log level for connection query errors of short-lived exited
processes.
+* Enable pprof by default but bind it to 127.0.0.1 so it is not exposed on the
network.
#### Bug Fixes
* Fix the base image cannot run in the arm64.
* Fix process fork tracepoint reporting thread TID instead of process TGID,
causing repeated process detected/dead churn.
+* Fix panic in the access log module when handling HTTP/2 streams without a
body.
#### Documentation
* Add a dead link checker in the CI.
diff --git a/configs/rover_configs.yaml b/configs/rover_configs.yaml
index c497c16..a5a6b0c 100644
--- a/configs/rover_configs.yaml
+++ b/configs/rover_configs.yaml
@@ -151,7 +151,7 @@ access_log:
queue_size: ${ROVER_ACCESS_LOG_CONNECTION_ANALYZE_QUEUE_SIZE:2000}
protocol_analyze:
# The size of socket data buffer on each CPU
- per_cpu_buffer: ${ROVER_ACCESS_LOG_PROTOCOL_ANALYZE_PER_CPU_BUFFER:400KB}
+ per_cpu_buffer: ${ROVER_ACCESS_LOG_PROTOCOL_ANALYZE_PER_CPU_BUFFER:1MB}
# The count of parallel protocol event parse
parse_parallels: ${ROVER_ACCESS_LOG_PROTOCOL_ANALYZE_PARSE_PARALLELS:2}
# The count of parallel protocol analyzer
@@ -161,6 +161,8 @@ access_log:
pprof:
# Is active the pprof
- active: ${ROVER_PPROF_ACTIVE:false}
+ active: ${ROVER_PPROF_ACTIVE:true}
+ # The bind host of the pprof HTTP server, defaults to 127.0.0.1 so it only
listens on the local host
+ host: ${ROVER_PPROF_HOST:127.0.0.1}
# The bind port of the pprof HTTP server
port: ${ROVER_PPROF_PORT:6060}
\ No newline at end of file
diff --git a/docs/en/setup/configuration/pprof.md
b/docs/en/setup/configuration/pprof.md
index 98e5675..51c5f59 100644
--- a/docs/en/setup/configuration/pprof.md
+++ b/docs/en/setup/configuration/pprof.md
@@ -4,10 +4,11 @@ Pprof is a feature to collect self runtime profiling data
through `pprof` module
## Configuration
-| Name | Default | Environment Key | Description
|
-|-----------|---------|----------------------|-------------------------------------|
-| `enabled` | `false` | `ROVER_PPROF_ACTIVE` | Enable pprof module.
|
-| `port` | `6060` | `ROVER_PPROF_PORT` | The HTTP port to expose pprof
data. |
+| Name | Default | Environment Key | Description
|
+|-----------|-------------|----------------------|------------------------------------------------------------------------------------|
+| `enabled` | `true` | `ROVER_PPROF_ACTIVE` | Enable pprof module.
|
+| `host` | `127.0.0.1` | `ROVER_PPROF_HOST` | The bind host of the pprof
HTTP server, only listens on the local host by default. |
+| `port` | `6060` | `ROVER_PPROF_PORT` | The HTTP port to expose
pprof data. |
## Expose Paths
diff --git a/pkg/accesslog/collector/protocols/http1.go
b/pkg/accesslog/collector/protocols/http1.go
index 8a76a2c..c24b1bc 100644
--- a/pkg/accesslog/collector/protocols/http1.go
+++ b/pkg/accesslog/collector/protocols/http1.go
@@ -220,10 +220,11 @@ func (p *HTTP1Protocol) HandleHTTPData(metrics
*HTTP1Metrics, _ *PartitionConnec
details, idRange, allInclude = AppendSocketDetailsFromBuffer(details,
response.HeaderBuffer(), idRange, allInclude)
details, idRange, allInclude = AppendSocketDetailsFromBuffer(details,
response.BodyBuffer(), idRange, allInclude)
- if !allInclude {
+ if !allInclude || idRange == nil || len(details) == 0 {
+ from, to := DataIDRangeBounds(idRange)
return fmt.Errorf("cannot found full detail events for HTTP/1.x
protocol, "+
"data id: %d-%d, current details count: %d",
- idRange.From, idRange.To, len(details))
+ from, to, len(details))
}
http1Log.Debugf("found fully HTTP1 request and response, contains %d
detail events, "+
diff --git a/pkg/accesslog/collector/protocols/http2.go
b/pkg/accesslog/collector/protocols/http2.go
index 0db9741..3e3aeb3 100644
--- a/pkg/accesslog/collector/protocols/http2.go
+++ b/pkg/accesslog/collector/protocols/http2.go
@@ -245,17 +245,19 @@ func (r *HTTP2Protocol)
validateIsStreamOpenTooLong(connection *PartitionConnect
func (r *HTTP2Protocol) HandleWholeStream(_ *PartitionConnection, stream
*HTTP2Streaming) error {
details := make([]events.SocketDetail, 0)
- var allInclude = true
+ allInclude := true
var idRange *buffer.DataIDRange
details, idRange, allInclude = AppendSocketDetailsFromBuffer(details,
stream.ReqHeaderBuffer, idRange, allInclude)
details, idRange, allInclude = AppendSocketDetailsFromBuffer(details,
stream.ReqBodyBuffer, idRange, allInclude)
details, idRange, allInclude = AppendSocketDetailsFromBuffer(details,
stream.RespHeaderBuffer, idRange, allInclude)
details, idRange, allInclude = AppendSocketDetailsFromBuffer(details,
stream.RespBodyBuffer, idRange, allInclude)
- if !allInclude {
- return fmt.Errorf("cannot found any detail events for HTTP/2
protocol, data id: %d-%d, current details count: %d",
- stream.ReqHeaderBuffer.FirstSocketBuffer().DataID(),
stream.RespBodyBuffer.LastSocketBuffer().DataID(),
- len(details))
+ // idRange is nil / details empty when no buffer contributed any event
(e.g. all buffers absent);
+ // guard it so the error path never dereferences a nil buffer as it
used to (which panicked).
+ if !allInclude || idRange == nil || len(details) == 0 {
+ from, to := DataIDRangeBounds(idRange)
+ return fmt.Errorf("cannot found full detail events for HTTP/2
protocol, data id: %d-%d, current details count: %d",
+ from, to, len(details))
}
idRange.DeleteDetails(stream.ReqHeaderBuffer)
diff --git a/pkg/accesslog/collector/protocols/http2_test.go
b/pkg/accesslog/collector/protocols/http2_test.go
new file mode 100644
index 0000000..9d80187
--- /dev/null
+++ b/pkg/accesslog/collector/protocols/http2_test.go
@@ -0,0 +1,186 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package protocols
+
+import (
+ "testing"
+ "time"
+
+ "github.com/apache/skywalking-rover/pkg/accesslog/common"
+ aclevents "github.com/apache/skywalking-rover/pkg/accesslog/events"
+ analyzeevents
"github.com/apache/skywalking-rover/pkg/profiling/task/network/analyze/events"
+ "github.com/apache/skywalking-rover/pkg/tools/buffer"
+
+ v3 "skywalking.apache.org/repo/goapi/collect/ebpf/accesslog/v3"
+)
+
+// buildDetailBuffer builds a minimal, fully-captured buffer that holds a
single detail event with
+// the given data id, so BuildDetails() is non-empty and
BuildTotalDataIDRange() covers it.
+func buildDetailBuffer(dataID, start, end uint64) *buffer.Buffer {
+ b := buffer.NewBuffer()
+ b.AppendDataEvent(&analyzeevents.SocketDataUploadEvent{
+ DataID0: dataID, DataLen: 1, StartTime0: start, EndTime0: end,
Finished: 1,
+ })
+ b.AppendDetailEvent(&aclevents.SocketDetailEvent{DataID0: dataID,
StartTime: start, EndTime: end})
+ b.PrepareForReading()
+ return b
+}
+
+type noopQueueConsumer struct{}
+
+func (noopQueueConsumer) Consume(_ chan common.KernelLog, _ chan
common.ProtocolLog) {}
+
+// TestAppendSocketDetailsFromBuffer covers the buffer-classification logic at
the heart of the panic
+// fix: a nil (absent) buffer is skipped, a present-but-empty buffer is
incomplete, and a
+// fully-captured buffer contributes its details.
+func TestAppendSocketDetailsFromBuffer(t *testing.T) {
+ // a nil buffer with allInclude=true must be skipped, not marked
incomplete (the core fix).
+ if _, idRange, include := AppendSocketDetailsFromBuffer(nil, nil, nil,
true); !include || idRange != nil {
+ t.Fatalf("nil buffer should be skipped: include=%v idRange=%v",
include, idRange)
+ }
+ // once incomplete, a following nil buffer keeps the stream incomplete.
+ if _, _, include := AppendSocketDetailsFromBuffer(nil, nil, nil,
false); include {
+ t.Fatal("incomplete state must be propagated through a nil
buffer")
+ }
+ // a present-but-empty buffer is treated as incomplete.
+ if _, _, include := AppendSocketDetailsFromBuffer(nil,
buffer.NewBuffer(), nil, true); include {
+ t.Fatal("empty buffer should be marked incomplete")
+ }
+ // a present buffer whose details fall outside its captured data-id
range (e.g. dropped perf
+ // samples left a gap) is incomplete, not skipped.
+ incomplete := buffer.NewBuffer()
+
incomplete.AppendDataEvent(&analyzeevents.SocketDataUploadEvent{DataID0: 1,
DataLen: 1, Finished: 1})
+ incomplete.AppendDetailEvent(&aclevents.SocketDetailEvent{DataID0: 5})
+ incomplete.PrepareForReading()
+ if _, _, include := AppendSocketDetailsFromBuffer(nil, incomplete, nil,
true); include {
+ t.Fatal("buffer with details outside its data-id range should
be incomplete")
+ }
+
+ // a fully-captured buffer contributes its detail and stays complete.
+ details, idRange, include := AppendSocketDetailsFromBuffer(nil,
buildDetailBuffer(1, 10, 20), nil, true)
+ if !include || len(details) != 1 || idRange == nil || idRange.From != 1
|| idRange.To != 1 {
+ t.Fatalf("complete buffer: include=%v len=%d idRange=%v",
include, len(details), idRange)
+ }
+}
+
+func TestDataIDRangeBounds(t *testing.T) {
+ if from, to := DataIDRangeBounds(nil); from != 0 || to != 0 {
+ t.Fatalf("nil range must be (0,0), got (%d,%d)", from, to)
+ }
+ if from, to := DataIDRangeBounds(&buffer.DataIDRange{From: 3, To: 9});
from != 3 || to != 9 {
+ t.Fatalf("expected (3,9), got (%d,%d)", from, to)
+ }
+}
+
+// TestHTTP2HandleWholeStreamIncompleteNoPanic guards against the SIGSEGV that
used to happen when a
+// stream reached HandleWholeStream without a fully-captured set of detail
events. The error path
+// previously dereferenced possibly-nil buffers
(stream.RespBodyBuffer.LastSocketBuffer().DataID());
+// it must now return an error safely instead of panicking.
+func TestHTTP2HandleWholeStreamIncompleteNoPanic(t *testing.T) {
+ r := &HTTP2Protocol{}
+ tests := []struct {
+ name string
+ stream *HTTP2Streaming
+ }{
+ {
+ name: "all buffers absent",
+ stream: &HTTP2Streaming{ReqHeader: map[string]string{},
RespHeader: map[string]string{}},
+ },
+ {
+ name: "present but empty request header buffer, bodies
absent",
+ stream: &HTTP2Streaming{
+ ReqHeader: map[string]string{},
+ RespHeader: map[string]string{},
+ ReqHeaderBuffer: buffer.NewBuffer(),
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ err := r.HandleWholeStream(nil, test.stream)
+ if err == nil {
+ t.Fatal("expected an error when detail events
are incomplete, got nil")
+ }
+ })
+ }
+}
+
+// TestHTTP2HandleWholeStreamReportsWhenBodiless proves the root-cause fix: a
stream with request and
+// response headers but no bodies (e.g. a GET with a 204/304 response that
ends on the HEADERS frame)
+// must now be reported instead of being dropped/panicking on the nil body
buffers.
+func TestHTTP2HandleWholeStreamReportsWhenBodiless(t *testing.T) {
+ r := &HTTP2Protocol{ctx: &common.AccessLogContext{
+ Queue: common.NewQueue(100, time.Minute, noopQueueConsumer{}),
+ }}
+ stream := &HTTP2Streaming{
+ ReqHeader: map[string]string{":path": "/", ":authority":
"example"},
+ RespHeader: map[string]string{":status": "204"},
+ ReqHeaderBuffer: buildDetailBuffer(1, 10, 20),
+ RespHeaderBuffer: buildDetailBuffer(2, 30, 40),
+ // ReqBodyBuffer and RespBodyBuffer are intentionally nil
(bodiless).
+ }
+ if err := r.HandleWholeStream(nil, stream); err != nil {
+ t.Fatalf("bodiless stream should be reported, got error: %v",
err)
+ }
+}
+
+func TestHTTP2ParseHTTPMethod(t *testing.T) {
+ r := &HTTP2Protocol{}
+ tests := []struct {
+ method string
+ expect v3.AccessLogHTTPProtocolRequestMethod
+ }{
+ {"", v3.AccessLogHTTPProtocolRequestMethod_Get}, // absent
method defaults to GET
+ {"post", v3.AccessLogHTTPProtocolRequestMethod_Post}, //
lower-cased method is upper-cased first
+ {"patch", v3.AccessLogHTTPProtocolRequestMethod_Patch},
+ {"weird", v3.AccessLogHTTPProtocolRequestMethod_Get}, //
unknown method falls back to GET
+ }
+ for _, test := range tests {
+ stream := &HTTP2Streaming{ReqHeader: map[string]string{}}
+ if test.method != "" {
+ stream.ReqHeader[":method"] = test.method
+ }
+ if got := r.ParseHTTPMethod(stream); got != test.expect {
+ t.Fatalf("method %q: expected %v, got %v", test.method,
test.expect, got)
+ }
+ }
+}
+
+func TestHTTP2FirstDetail(t *testing.T) {
+ r := &HTTP2Protocol{}
+ def := &aclevents.SocketDetailEvent{DataID0: 99}
+ if got := r.FirstDetail(nil, def); got != def {
+ t.Fatal("nil buffer should return the default detail")
+ }
+ if got := r.FirstDetail(buffer.NewBuffer(), def); got != def {
+ t.Fatal("empty buffer should return the default detail")
+ }
+ if got := r.FirstDetail(buildDetailBuffer(7, 1, 2), def); got == def ||
got.DataID() != 7 {
+ t.Fatalf("present buffer should return its own first detail,
got DataID=%d", got.DataID())
+ }
+}
+
+func TestHTTP2BufferSizeOfZero(t *testing.T) {
+ r := &HTTP2Protocol{}
+ if size := r.BufferSizeOfZero(nil); size != 0 {
+ t.Fatalf("nil buffer size must be 0, got %d", size)
+ }
+ if size := r.BufferSizeOfZero(buildDetailBuffer(1, 1, 2)); size == 0 {
+ t.Fatal("present buffer size must be non-zero")
+ }
+}
diff --git a/pkg/accesslog/collector/protocols/protocol.go
b/pkg/accesslog/collector/protocols/protocol.go
index 673796c..63d68d8 100644
--- a/pkg/accesslog/collector/protocols/protocol.go
+++ b/pkg/accesslog/collector/protocols/protocol.go
@@ -58,9 +58,16 @@ type Protocol interface {
func AppendSocketDetailsFromBuffer(result []events.SocketDetail, buf
*buffer.Buffer, dataIDRange *buffer.DataIDRange,
allDetailInclude bool) ([]events.SocketDetail, *buffer.DataIDRange,
bool) {
- if buf == nil || !allDetailInclude {
+ // a previous buffer was already incomplete: keep propagating the
incompleteness.
+ if !allDetailInclude {
return result, dataIDRange, false
}
+ // a nil buffer means this part of the message is legitimately absent
(a request without body,
+ // or a response that ends the stream on the HEADERS frame such as
204/304/redirect or a gRPC
+ // trailers-only response): skip it rather than treating the whole
message as incomplete.
+ if buf == nil {
+ return result, dataIDRange, true
+ }
details := buf.BuildDetails()
if details == nil || details.Len() == 0 {
return result, dataIDRange, false
@@ -81,6 +88,15 @@ func AppendSocketDetailsFromBuffer(result
[]events.SocketDetail, buf *buffer.Buf
return result, dataIDRange.Append(currentDataIDRange), true
}
+// DataIDRangeBounds returns the from/to data id bounds of the range, or (0,
0) when it is nil, so
+// callers can build an error message without dereferencing a possibly-nil
range.
+func DataIDRangeBounds(dataIDRange *buffer.DataIDRange) (from, to uint64) {
+ if dataIDRange != nil {
+ from, to = dataIDRange.From, dataIDRange.To
+ }
+ return
+}
+
func AnalyzeTraceInfo(fetcher func(key string) string, protocolLog
*logger.Logger) *v3.AccessLogTraceInfo {
context, err := tracing.AnalyzeTracingContext(func(key string) string {
return fetcher(key)
diff --git a/pkg/pprof/config.go b/pkg/pprof/config.go
index 496957c..9f29e83 100644
--- a/pkg/pprof/config.go
+++ b/pkg/pprof/config.go
@@ -22,7 +22,10 @@ import "github.com/apache/skywalking-rover/pkg/module"
type Config struct {
module.Config `mapstructure:",squash"`
- Port int `mapstructure:"port"`
+ // Host is the bind address of the pprof HTTP server. It defaults to
127.0.0.1 so the debug
+ // endpoints are only reachable from the local host and are not exposed
on the network.
+ Host string `mapstructure:"host"`
+ Port int `mapstructure:"port"`
}
func (c *Config) IsActive() bool {
diff --git a/pkg/pprof/module.go b/pkg/pprof/module.go
index 8505db9..39684b6 100644
--- a/pkg/pprof/module.go
+++ b/pkg/pprof/module.go
@@ -67,7 +67,7 @@ func (m *Module) Start(_ context.Context, mgr
*module.Manager) error {
defer m.mutex.Unlock()
m.server = &http.Server{
- Addr: fmt.Sprintf(":%d", m.config.Port),
+ Addr: fmt.Sprintf("%s:%d", m.config.Host,
m.config.Port),
ReadHeaderTimeout: 3 * time.Second,
Handler: mux,
}
diff --git a/pkg/process/api/process.go b/pkg/process/api/process.go
index aed6d18..75a09ba 100644
--- a/pkg/process/api/process.go
+++ b/pkg/process/api/process.go
@@ -78,6 +78,8 @@ type DetectedProcess interface {
DetectType() ProcessDetectType
// ProfilingStat of process
ProfilingStat() *profiling.Info
+ // SupportProfiling reports whether the process can be profiled without
retaining its symbols
+ SupportProfiling() bool
// ExposePorts define which ports are exposed
ExposePorts() []int
// ExposeHosts define which hosts are exposed
diff --git a/pkg/process/finders/base/tool.go b/pkg/process/finders/base/tool.go
index f2413be..725cf61 100644
--- a/pkg/process/finders/base/tool.go
+++ b/pkg/process/finders/base/tool.go
@@ -44,6 +44,16 @@ func BuildProfilingStat(ps *process.Process)
(*profiling.Info, error) {
return process_tool.ProfilingStat(ps.Pid, exePath)
}
+// SupportProfiling reports whether the process's executable can be profiled
without retaining the
+// parsed symbol data in memory (see process_tool.SupportProfiling).
+func SupportProfiling(ps *process.Process) (bool, error) {
+ exePath := tryToFindFileExecutePath(ps)
+ if exePath == "" {
+ return false, fmt.Errorf("could not found executable file")
+ }
+ return process_tool.SupportProfiling(exePath)
+}
+
func tryToFindFileExecutePath(ps *process.Process) string {
exe, err := ps.Exe()
if err != nil {
diff --git a/pkg/process/finders/kubernetes/finder.go
b/pkg/process/finders/kubernetes/finder.go
index 21eae18..2edfbbf 100644
--- a/pkg/process/finders/kubernetes/finder.go
+++ b/pkg/process/finders/kubernetes/finder.go
@@ -350,7 +350,7 @@ func (f *ProcessFinder) BuildNecessaryProperties(ps
api.DetectedProcess) []*comm
return []*commonv3.KeyStringValuePair{
{
Key: "support_ebpf_profiling",
- Value: strconv.FormatBool(ps.ProfilingStat() != nil),
+ Value: strconv.FormatBool(ps.SupportProfiling()),
},
}
}
diff --git a/pkg/process/finders/kubernetes/process.go
b/pkg/process/finders/kubernetes/process.go
index bf20683..c96e47a 100644
--- a/pkg/process/finders/kubernetes/process.go
+++ b/pkg/process/finders/kubernetes/process.go
@@ -18,6 +18,9 @@
package kubernetes
import (
+ "errors"
+ "os"
+ "strings"
"sync"
"github.com/shirou/gopsutil/process"
@@ -31,11 +34,11 @@ type Process struct {
original *process.Process
// process data
- pid int32
- cmd string
- profilingOnce sync.Once
- profiling *profiling.Info
- podContainer *PodContainer
+ pid int32
+ cmd string
+ supportProfilingOnce sync.Once
+ supportProfiling bool
+ podContainer *PodContainer
// entity for the backend
entity *api.ProcessEntity
@@ -67,12 +70,22 @@ func (p *Process) DetectType() api.ProcessDetectType {
return api.Kubernetes
}
+// ProfilingStat builds the full profiling info (heavy: parses and retains the
ELF symbol tables).
+// It intentionally does NOT cache the result on the process: the info is only
needed while a
+// profiling task runs and is held by that task's runner, so it is released
once the task ends.
func (p *Process) ProfilingStat() *profiling.Info {
- p.profilingOnce.Do(func() {
- stat, _ := base.BuildProfilingStat(p.original)
- p.profiling = stat
+ stat, _ := base.BuildProfilingStat(p.original)
+ return stat
+}
+
+// SupportProfiling reports whether the process can be profiled, WITHOUT
retaining the symbol data.
+// The result is a bool that is cheap to cache, so keep-alive reporting does
not re-parse the ELF
+// symbol tables of every discovered process on each report.
+func (p *Process) SupportProfiling() bool {
+ p.supportProfilingOnce.Do(func() {
+ p.supportProfiling, _ = base.SupportProfiling(p.original)
})
- return p.profiling
+ return p.supportProfiling
}
func (p *Process) PodContainer() *PodContainer {
@@ -89,7 +102,15 @@ func (p *Process) ExposePorts() []int {
}
connections, err := p.original.Connections()
if err != nil {
- log.Warnf("query the process connection error: pid: %d, error:
%v", p.pid, err)
+ // A short-lived process may exit between discovery and this
query, making its
+ // /proc/<pid>/net/* files disappear. That is an expected race
for ephemeral processes
+ // (e.g. curl/sleep from load generators), not a real error, so
keep it at debug level
+ // to avoid flooding the log.
+ if errors.Is(err, os.ErrNotExist) ||
strings.Contains(err.Error(), "no such file or directory") {
+ log.Debugf("skip querying connections for the exited
process, pid: %d, error: %v", p.pid, err)
+ } else {
+ log.Warnf("query the process connection error: pid: %d,
error: %v", p.pid, err)
+ }
return result
}
for _, c := range connections {
diff --git a/pkg/tools/btf/linker.go b/pkg/tools/btf/linker.go
index 3abb43b..271a10d 100644
--- a/pkg/tools/btf/linker.go
+++ b/pkg/tools/btf/linker.go
@@ -199,7 +199,9 @@ func (m *Linker) asyncReadEvent(rd *perf.Reader, emap
*ebpf.Map, recordPool *per
}
if record.LostSamples != 0 {
- log.Warnf("perf event queue(%s) full, dropped
%d samples", emap.String(), record.LostSamples)
+ // accumulate and report in an aggregated,
periodic summary instead of logging
+ // every burst on its own (see lostsamples.go).
+ recordLostSamples(emap.String(),
record.LostSamples)
recordPool.PutRecord(record)
continue
}
diff --git a/pkg/tools/btf/lostsamples.go b/pkg/tools/btf/lostsamples.go
new file mode 100644
index 0000000..8114fd9
--- /dev/null
+++ b/pkg/tools/btf/lostsamples.go
@@ -0,0 +1,75 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package btf
+
+import (
+ "sync"
+ "time"
+)
+
+// lostSampleLogInterval is how often the aggregated dropped-sample summary is
logged. The kernel
+// drops samples in bursts, so logging every single occurrence floods the log;
instead the counts
+// are accumulated and reported once per interval.
+const lostSampleLogInterval = 30 * time.Second
+
+var lostSamples = &lostSampleCounter{counts: make(map[string]uint64)}
+
+// lostSampleCounter accumulates the number of perf event samples the kernel
dropped (because the
+// per-CPU buffer was full and userspace did not drain it in time), keyed by
the perf map name.
+type lostSampleCounter struct {
+ mutex sync.Mutex
+ counts map[string]uint64
+ loggerStart sync.Once
+}
+
+// recordLostSamples accumulates dropped samples for a perf map and lazily
starts the periodic
+// aggregated logger on first loss.
+func recordLostSamples(mapName string, count uint64) {
+ lostSamples.mutex.Lock()
+ lostSamples.counts[mapName] += count
+ lostSamples.mutex.Unlock()
+
+ lostSamples.loggerStart.Do(func() {
+ go lostSamples.logPeriodically()
+ })
+}
+
+// drain atomically returns the samples accumulated since the last drain and
resets the counter, so
+// each reporting interval directly reads the newly-dropped counts without
tracking previous totals,
+// and the map does not grow unbounded.
+func (c *lostSampleCounter) drain() map[string]uint64 {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+ if len(c.counts) == 0 {
+ return nil
+ }
+ drained := c.counts
+ c.counts = make(map[string]uint64)
+ return drained
+}
+
+func (c *lostSampleCounter) logPeriodically() {
+ ticker := time.NewTicker(lostSampleLogInterval)
+ defer ticker.Stop()
+ for range ticker.C {
+ for name, dropped := range c.drain() {
+ log.Warnf("perf event queue(%s) dropped %d samples in
the last %s, "+
+ "consider increasing the per_cpu_buffer or
parse_parallels", name, dropped, lostSampleLogInterval)
+ }
+ }
+}
diff --git a/pkg/pprof/config.go b/pkg/tools/btf/lostsamples_test.go
similarity index 61%
copy from pkg/pprof/config.go
copy to pkg/tools/btf/lostsamples_test.go
index 496957c..35028d1 100644
--- a/pkg/pprof/config.go
+++ b/pkg/tools/btf/lostsamples_test.go
@@ -15,16 +15,23 @@
// specific language governing permissions and limitations
// under the License.
-package pprof
+package btf
-import "github.com/apache/skywalking-rover/pkg/module"
+import "testing"
-type Config struct {
- module.Config `mapstructure:",squash"`
+func TestLostSampleCounterDrain(t *testing.T) {
+ c := &lostSampleCounter{counts: make(map[string]uint64)}
+ c.counts["q1"] += 3
+ c.counts["q1"] += 2
+ c.counts["q2"] += 7
- Port int `mapstructure:"port"`
-}
+ drained := c.drain()
+ if drained["q1"] != 5 || drained["q2"] != 7 {
+ t.Fatalf("unexpected drained counts: %v", drained)
+ }
-func (c *Config) IsActive() bool {
- return c.Active
+ // after a drain the counter must be reset, so a subsequent drain with
no new samples is empty
+ if again := c.drain(); again != nil {
+ t.Fatalf("expected nil after reset, got: %v", again)
+ }
}
diff --git a/pkg/tools/process/process.go b/pkg/tools/process/process.go
index 87930e2..31dac89 100644
--- a/pkg/tools/process/process.go
+++ b/pkg/tools/process/process.go
@@ -61,26 +61,40 @@ func KernelFileProfilingStat() (*profiling.Info, error) {
return kernelFinder.Analyze(profiling.KernelProcSymbolFilePath)
}
-// ProfilingStat is validating the exe file could be profiling and get info
-func ProfilingStat(pid int32, exePath string) (*profiling.Info, error) {
+// SupportProfiling reports whether the executable can be profiled (not
excluded and has a usable
+// symbol table) WITHOUT retaining the parsed symbol/module data in memory.
Use this for the
+// discovery-time "support_ebpf_profiling" flag: the heavy symbol data is only
needed while an
+// actual profiling task runs and is built on demand by ProfilingStat.
+func SupportProfiling(exePath string) (bool, error) {
stat, err := os.Stat(exePath)
if err != nil {
- return nil, fmt.Errorf("check file error: %v", err)
+ return false, fmt.Errorf("check file error: %v", err)
}
for _, notSupport := range NotSupportProfilingExe {
if strings.HasPrefix(stat.Name(), notSupport) {
- return nil, fmt.Errorf("not support %s language
profiling", notSupport)
+ return false, fmt.Errorf("not support %s language
profiling", notSupport)
}
}
- context := newAnalyzeContext()
- // the executable file must have the symbols
- symbols, err := context.GetFinder(exePath).AnalyzeSymbols(exePath)
- if err != nil || len(symbols) == 0 {
- return nil, fmt.Errorf("could not found any symbol in the
execute file: %s, error: %v", exePath, err)
+ // the executable file must have the symbols; the parsed symbols are
discarded here so they
+ // are not retained in memory for every discovered process.
+ symbols, err :=
newAnalyzeContext().GetFinder(exePath).AnalyzeSymbols(exePath)
+ if err != nil {
+ return false, fmt.Errorf("could not analyze symbols in the
executable file %s: %w", exePath, err)
+ }
+ if len(symbols) == 0 {
+ return false, fmt.Errorf("could not find any symbol in the
executable file: %s", exePath)
+ }
+ return true, nil
+}
+
+// ProfilingStat is validating the exe file could be profiling and get info
+func ProfilingStat(pid int32, exePath string) (*profiling.Info, error) {
+ if support, err := SupportProfiling(exePath); !support {
+ return nil, err
}
- return analyzeProfilingInfo(context, pid)
+ return analyzeProfilingInfo(newAnalyzeContext(), pid)
}
// Modules Read the profiling info of the process, without the symbol check