You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Investigate process-agent throughput degradation on high-density Kubernetes nodes #354
Process-agent is reported to reduce network throughput to approximately 35–40 Gb/s on high-density Kubernetes nodes with more than 3,000 connections. Stopping only process-agent restores substantially higher throughput under otherwise unchanged test conditions. Lower-density nodes perform well with it running.
Reported environment: SUSE Observability 2.10.0, 4000-ha backend profile, two bonded 100 Gb/s NICs per host. Confirm the actual agent chart/image digest, kernel, CNI, bond mode and iperf commands. Use measured agent-off throughput as the baseline; the bond does not guarantee 200 Gb/s for a given test.
Root cause is unconfirmed. Source inspection identifies repeated protocol classification of unrecognized TCP payloads as a plausible lead. Conntrack lookup overhead is another candidate; neither establishes a 3,000-connection limit or a known fix.
Next steps:
Obtain the targeted customer diagnostics; their security policy prevents running collection scripts.
Reproduce on an isolated pair with the same load: normal agent → nodeAgent.protocolInspection.enabled=false → process-agent disabled → original configuration restored.
Vary pod count, connection count, packet rate and connection churn independently. Measure host system/softirq CPU, container throttling, retransmits and per-NIC counters.
Identify the responsible path and validate a mitigation or fix against the same workload, including its effect on observability coverage.
Chart configurations were render-validated; runtime performance reproduction remains pending. Closure requires repeatable evidence of the cause and a validated mitigation/fix with its visibility tradeoffs documented.
Source-level analysis of process-agent CPU/tracer cost (from a separate investigation)
Context: this came out of a different case (reported high CPU from the SUSE Observability agent), not the throughput regression in this issue. The attached diagnostics for that case turned out to be a backend storage/network benchmark with no agent telemetry, so I went through stackstate-process-agent (HEAD 6fb3bf18) and the 7.78.x-based stackstate-agent tree instead. Several of the findings below are directly relevant to the >3,000-connection / throughput problem, in particular the default-on protocol classification path that this issue already flags as the main lead. No claim here that CPU and throughput share a root cause — but the same code paths are the suspects, and the instrumentation below will settle it.
What the tracer is actually configured to do by default
From config/config.go (NewDefaultAgentConfig) and config/tracer_config.go (TracerConfig):
connections shorter than 60 s are filtered after the fact, they are still observed
EnableMonotonicCount
true
EnableRootNetNs
true
Process check (checks/process.go): runs every 30 s (default), enumerates every PID via process.AllProcesses(), scrubs command lines, and calls the shared container provider with a hard 2*time.Second timeout; it emits an increment every cycle and a full snapshot every 60 s (IncrementalPublishingRefreshInterval). In the node agent, if process/container collection is enabled there instead, the intervals are 10 s (2 s realtime) for both.
Concrete leads worth testing against the reproduction
Protocol classification of unrecognized payloads (ProtocolClassificationEnabled=true). This matches the lead already in the issue. Test with network_tracer_config.protocol_inspection_enabled: false and, separately, disabled_protocols: ["http","http2","mongo","amqp","postgres","tls"] to see whether the cost is classification in general or a specific parser.
MaxTrackedHTTPConnections: 1024. On a node with >3,000 connections, if a large fraction are HTTP/HTTP2, the HTTP map is permanently saturated: notification/eviction churn every cleaner interval. Worth instrumenting before/after raising it to e.g. 4096 in a canary build.
EnableGatewayLookup: true. Route-table lookups per connection; this is a known CPU-heavy path on hosts with large routing tables / many interfaces (bonded 100 GbE setups are exactly where this shows up). A/B it independently of protocol inspection.
Conntrack: EnableConntrackAllNamespaces=true + ConntrackRateLimit=500/3s. Under high connection churn, registrations can hit the rate limit and fall back, adding work. Compare conntrack -S / nf_conntrack_count with the agent on/off.
Per-cycle volume, not per-connection: 30 s process cycle × full container enumeration (2 s timeout) + 60 s full snapshot is a fixed cost that grows with pod/process count, independent of the network regression.
Instrumentation (already built in — no collection scripts needed)
The process agent always serves pprof/expvar on :6062 (cmd/agent/main_common.go, profile_process_agent.sh):
Look for pkg/network/tracer, cilium/ebpf, protocols/*, conntrack, gopsutil/process, containers, encoding/json. A 30 s profile taken during the iperf/throughput run is the single most decisive artifact — it will show whether time is in the eBPF datapath, protocol classification, conntrack, or userspace collection/serialization.
agent status reports AverageExecutionTime per check (pkg/status/types.go, pkg/collector/check/stats/stats.go), which separates “one expensive check” from “many cheap ones”. For JMX, agent jmx list collected shows the effective bean/attribute count.
Ranked suspects for agent-side CPU (same paths that matter for throughput)
protocol_inspection_enabled: false, disabled_protocols, raise HTTP map cap
2
Process/container collection volume
checks/process.go:120,165,186; 30 s cycle, 60 s snapshot, 2 s container timeout
pprof (gopsutil/process, containers); collected N processes and M containers in …
raise intervals; disable process or container collection independently
3
Log level / payload logging
log_payloads default false; guarded hot-path logs in check_handler_transactional.go:136-180
agent config get log_level, log_payloads; pprof (encoding/json, log I/O)
DD_LOG_LEVEL=INFO, log_payloads: false
4
Cluster-agent topology snapshots
every component is JSON-marshalled at least twice (TRACE in collectors, DEBUG in kubernetes_topology_config.go:122) plus payload; secrets SHA-256 + x509 parse per snapshot
JMX-Fetch is a JVM inside the agent; 25 % RAM default, 3 restarts, 3-thread pool
ps shows java; agent jmx list collected; per-check time
narrow bean scope; move JMX to its own container
6
Disk/container corechecks on dense nodes
diskv2 enumerates mounts per cycle; cgroup reads per container
pprof (corechecks/system/disk, containers)
exclude CSI mounts; raise interval
Not a suspect in this build: GOMAXPROCS vs CPU limit. Go 1.26 sizes GOMAXPROCS from the cgroup quota and the agent also calls automaxprocs (pkg/runtime/runtime.go); only an explicit GOMAXPROCS env override breaks it.
Suggested measurement order for the reproduction
Baseline agent-on, 30 s pprof on the process-agent container + agent status + host softirq/system CPU split.
protocol_inspection_enabled=false only → re-measure. This isolates the issue's stated lead without touching anything else.
Re-enable, then EnableGatewayLookup=false in a canary build (config-only via a patched image, since it is not exposed as a chart value).
Raise MaxTrackedHTTPConnections in a canary if step 1 shows HTTP map eviction/notification churn.
Only then disable process/container collection, to separate network-datapath cost from userspace collection cost.
Measure per-NIC counters, retransmits, nf_conntrack_count, and CFS throttling at each step, so a throughput change can be attributed rather than inferred.
Happy to turn steps 1–6 into a scripted canary runbook if useful. Note: the original CPU case also had a backend I/O benchmark attached (HDFS/Kafka dd/nc/producer-perf) that showed a ~4.3–4.7× buffered-vs-direct disk ratio and a ~35 % cross-node Kafka leader throughput drop; that data is de-identified here and can be shared separately if it is relevant to this issue.
Reproduced locally with the customer's 2.10.0 process-agent image (stackstate-k8s-process-agent:4898d630). The chart 1.3.22 DaemonSet spec was run under Docker, measuring TCP bulk throughput between two container netns over veth.
State
0 conns
3000 idle
3000 active
Agent stopped
41.8
42.4
43.5 Gbps
Chart defaults
26.7
26.3
26.7
protocolInspection.enabled=false
41.7
41.7
42.7
httpTracing.enabled=false
25.8
only http protocol enabled
27.8
Cause: USM protocol classification. socket__classifier_entry (~1 µs) and the net_dev_queue tracepoint (~0.8 µs) run on every transmitted packet. On top of that, socket__protocol_dispatcher runs per tap. protocol_classifier_entrypoint never records a negative result, so unknown-protocol flows (iperf, storage, replication) are re-classified on every packet for their whole lifetime.
Density: Idle connections don't matter. Active connections raise node-wide BPF CPU from ~0.7 to ~3.2 cores at ~150k pps background, and per-run cost doubles. That cost lands in softirq/sender context, not the process-agent cgroup (~12% CPU), so container metrics hide it.
Mitigation:nodeAgent.protocolInspection.enabled=false recovers throughput to within ~2% of agent-off. Disabling HTTP tracing or individual protocols does not help.
Fix direction: cache a negative classification per connection, or cap classification attempts, so unclassified flows stop paying per-packet cost.
Side findings:
Start() doesn't seed NetNsMonitor pids, so exec/exit churn repeatedly drops and re-attaches netns dispatchers.
The image HEALTHCHECK (process-agent health) starts a full agent that never exits. This is Docker-only: k8s ignores it.
Details: official-plans/process-agent-throughput-2026-10-23/README.md in mission-control. Caveat: this was measured on a veth path, not the 2×100G bond, so the size of the effect on the customer's NICs needs their paired A/B measurement.
Design notes for the classification cap, following Bram's three points (upstream, false negatives, in-flight connections).
Upstream DataDog:main has no negative result or attempt cap for flows whose protocol is never recognised. DataDog/datadog-agent#56780 (merged 2026-10-01, from a customer performance report) stops per-packet re-classification of long-lived TLS flows and moves early exits ahead of the payload copy. It does not cover unknown protocols, but is worth backporting. Earlier TLS-only attempts DataDog/datadog-agent#52578 and DataDog/datadog-agent#52581 were closed unmerged.
False negatives: classifiers are stateless per packet and look at the first CLASSIFICATION_MAX_BUFFER (24) bytes of the current segment. The budget should therefore count data segments, not bytes: what matters is how many segments pass before one starts on a message boundary.
HTTP, Postgres, MySQL, AMQP, Redis, Kafka and TLS records can be recognised mid-stream at a message boundary.
HTTP/2 is only recognised from the preface or server SETTINGS frame, so mid-stream HTTP/2 is never classified today.
Mongo already caps attempts per connection (MONGO_MAX_CLASSIFICATION_TRIES, mongo_connection_classification_tries LRU map) to avoid false positives — an in-tree precedent.
In-flight connections:initial_tcp_seq records whether the handshake was seen, but the socket-filter classifier cannot read it. Options: record SYN sightings in the socket filter; use the small budget only when the first observed data segment starts the stream; or use one larger budget for all.
Proposal
Per-connection attempt counter alongside the existing protocol state (protocol_stack_wrapper_t or a separate LRU map); never in protocol_stack_t, which is embedded in conn_stats_t.
On exhaustion set a "classification gave up" flag (bit 7 is free) so later packets skip classification.
Check it in both protocol_classifier_entrypoint and the dispatcher; both re-classify unknown flows per packet.
Check before usm_context_init/payload copy, or the saving is lost.
Separate, configurable budgets for handshake-seen and in-flight connections.
Telemetry: give-ups per handshake state, so false negatives are debuggable.
New entries for unknown flows grow connection_protocol; the stale-entry GC must cover them.
Validation: rerun the local throughput reproduction; USM classification tests; a false-negative harness with real HTTP/Postgres/Kafka/Redis clients started mid-stream, measuring segments-to-classification per protocol.
Each connection now has a budget of classification attempts: 64 packet inspections when the handshake was seen, 1024 when we joined mid-stream. A connection that uses up its budget skips classification for 5 minutes.
Local reproduction (iperf3, 3000 active connections, Gbps):
Agent off
Before
With cap
42
27.5
31–33
In-kernel classification time dropped from 49 s to 21 s per 15 s run.
The remaining gap is the fixed per-packet hook cost, which this change does not address.
In telemetry from the live agent, every connection that was classified needed at most 15 inspections.
A new regression test checks that a protocol appearing within the budget is still classified and one appearing after it is not.
This lowers the overhead; it does not remove it. For throughput benchmarking, the main advice still stands: use representative load, or disable protocol inspection.
Activity
dottorblaster commentedon Oct 23, 2026
Source-level analysis of process-agent CPU/tracer cost (from a separate investigation)
Context: this came out of a different case (reported high CPU from the SUSE Observability agent), not the throughput regression in this issue. The attached diagnostics for that case turned out to be a backend storage/network benchmark with no agent telemetry, so I went through
stackstate-process-agent(HEAD6fb3bf18) and the 7.78.x-basedstackstate-agenttree instead. Several of the findings below are directly relevant to the >3,000-connection / throughput problem, in particular the default-on protocol classification path that this issue already flags as the main lead. No claim here that CPU and throughput share a root cause — but the same code paths are the suspects, and the instrumentation below will settle it.What the tracer is actually configured to do by default
From
config/config.go(NewDefaultAgentConfig) andconfig/tracer_config.go(TracerConfig):protocol_inspection_enabled→ProtocolClassificationEnabledEnableHTTPMonitoring/EnableHTTP2MonitoringEnableMongoMonitoring/EnableAMQPMonitoring/EnablePostgresMonitoringEnableNativeTLSMonitoringEnableKafkaMonitoringMaxTrackedHTTPConnectionsHTTPNotificationThresholdHTTPMapCleanerInterval/HTTPIdleConnectionTTLEnableGatewayLookupEnableConntrack/EnableEbpfConntrackerEnableConntrackAllNamespacesConntrackMaxStateSize/ConntrackRateLimitMaxTrackedConnectionsMaxConnectionsStateBuffered/MaxClosedConnectionsBufferedTCPConnTimeout/ClientStateExpiryEnableShortLivedNetworkRelationFilterEnableMonotonicCountEnableRootNetNsProcess check (
checks/process.go): runs every 30 s (default), enumerates every PID viaprocess.AllProcesses(), scrubs command lines, and calls the shared container provider with a hard2*time.Secondtimeout; it emits an increment every cycle and a full snapshot every 60 s (IncrementalPublishingRefreshInterval). In the node agent, if process/container collection is enabled there instead, the intervals are 10 s (2 s realtime) for both.Concrete leads worth testing against the reproduction
ProtocolClassificationEnabled=true). This matches the lead already in the issue. Test withnetwork_tracer_config.protocol_inspection_enabled: falseand, separately,disabled_protocols: ["http","http2","mongo","amqp","postgres","tls"]to see whether the cost is classification in general or a specific parser.MaxTrackedHTTPConnections: 1024. On a node with >3,000 connections, if a large fraction are HTTP/HTTP2, the HTTP map is permanently saturated: notification/eviction churn every cleaner interval. Worth instrumenting before/after raising it to e.g. 4096 in a canary build.EnableGatewayLookup: true. Route-table lookups per connection; this is a known CPU-heavy path on hosts with large routing tables / many interfaces (bonded 100 GbE setups are exactly where this shows up). A/B it independently of protocol inspection.EnableConntrackAllNamespaces=true+ConntrackRateLimit=500/3s. Under high connection churn, registrations can hit the rate limit and fall back, adding work. Compareconntrack -S/nf_conntrack_countwith the agent on/off.Instrumentation (already built in — no collection scripts needed)
The process agent always serves pprof/expvar on
:6062(cmd/agent/main_common.go,profile_process_agent.sh):Look for
pkg/network/tracer,cilium/ebpf,protocols/*,conntrack,gopsutil/process,containers,encoding/json. A 30 s profile taken during the iperf/throughput run is the single most decisive artifact — it will show whether time is in the eBPF datapath, protocol classification, conntrack, or userspace collection/serialization.For the rest of the agent stack:
agent statusreportsAverageExecutionTimeper check (pkg/status/types.go,pkg/collector/check/stats/stats.go), which separates “one expensive check” from “many cheap ones”. For JMX,agent jmx list collectedshows the effective bean/attribute count.Ranked suspects for agent-side CPU (same paths that matter for throughput)
config/tracer_config.go:53-70;MaxTrackedHTTPConnections: 1024,EnableGatewayLookup: true:6062pprof during load; A/B each flagprotocol_inspection_enabled: false,disabled_protocols, raise HTTP map capchecks/process.go:120,165,186; 30 s cycle, 60 s snapshot, 2 s container timeoutgopsutil/process,containers);collected N processes and M containers in …log_payloadsdefault false; guarded hot-path logs incheck_handler_transactional.go:136-180agent config get log_level,log_payloads; pprof (encoding/json, log I/O)DD_LOG_LEVEL=INFO,log_payloads: falsekubernetes_topology_config.go:122) plus payload; secrets SHA-256 + x509 parse per snapshottopologycollectors,encoding/json); #secrets/#configmapsresources.*, lower snapshot ratepsshowsjava;agent jmx list collected; per-check timecorechecks/system/disk,containers)Not a suspect in this build: GOMAXPROCS vs CPU limit. Go 1.26 sizes GOMAXPROCS from the cgroup quota and the agent also calls
automaxprocs(pkg/runtime/runtime.go); only an explicitGOMAXPROCSenv override breaks it.Suggested measurement order for the reproduction
agent status+ hostsoftirq/systemCPU split.protocol_inspection_enabled=falseonly → re-measure. This isolates the issue's stated lead without touching anything else.EnableGatewayLookup=falsein a canary build (config-only via a patched image, since it is not exposed as a chart value).MaxTrackedHTTPConnectionsin a canary if step 1 shows HTTP map eviction/notification churn.nf_conntrack_count, and CFS throttling at each step, so a throughput change can be attributed rather than inferred.Happy to turn steps 1–6 into a scripted canary runbook if useful. Note: the original CPU case also had a backend I/O benchmark attached (HDFS/Kafka
dd/nc/producer-perf) that showed a ~4.3–4.7× buffered-vs-direct disk ratio and a ~35 % cross-node Kafka leader throughput drop; that data is de-identified here and can be shared separately if it is relevant to this issue.LouisParkin commentedon Oct 23, 2026
Reproduced locally with the customer's 2.10.0 process-agent image (
stackstate-k8s-process-agent:4898d630). The chart 1.3.22 DaemonSet spec was run under Docker, measuring TCP bulk throughput between two container netns over veth.protocolInspection.enabled=falsehttpTracing.enabled=falsehttpprotocol enabledsocket__classifier_entry(~1 µs) and thenet_dev_queuetracepoint (~0.8 µs) run on every transmitted packet. On top of that,socket__protocol_dispatcherruns per tap.protocol_classifier_entrypointnever records a negative result, so unknown-protocol flows (iperf, storage, replication) are re-classified on every packet for their whole lifetime.nodeAgent.protocolInspection.enabled=falserecovers throughput to within ~2% of agent-off. Disabling HTTP tracing or individual protocols does not help.Side findings:
Start()doesn't seedNetNsMonitorpids, so exec/exit churn repeatedly drops and re-attaches netns dispatchers.HEALTHCHECK(process-agent health) starts a full agent that never exits. This is Docker-only: k8s ignores it.Details:
official-plans/process-agent-throughput-2026-10-23/README.mdin mission-control. Caveat: this was measured on a veth path, not the 2×100G bond, so the size of the effect on the customer's NICs needs their paired A/B measurement.LouisParkin commentedon Oct 26, 2026
Design notes for the classification cap, following Bram's three points (upstream, false negatives, in-flight connections).
Upstream DataDog:
mainhas no negative result or attempt cap for flows whose protocol is never recognised. DataDog/datadog-agent#56780 (merged 2026-10-01, from a customer performance report) stops per-packet re-classification of long-lived TLS flows and moves early exits ahead of the payload copy. It does not cover unknown protocols, but is worth backporting. Earlier TLS-only attempts DataDog/datadog-agent#52578 and DataDog/datadog-agent#52581 were closed unmerged.False negatives: classifiers are stateless per packet and look at the first
CLASSIFICATION_MAX_BUFFER(24) bytes of the current segment. The budget should therefore count data segments, not bytes: what matters is how many segments pass before one starts on a message boundary.MONGO_MAX_CLASSIFICATION_TRIES,mongo_connection_classification_triesLRU map) to avoid false positives — an in-tree precedent.In-flight connections:
initial_tcp_seqrecords whether the handshake was seen, but the socket-filter classifier cannot read it. Options: record SYN sightings in the socket filter; use the small budget only when the first observed data segment starts the stream; or use one larger budget for all.Proposal
protocol_stack_wrapper_tor a separate LRU map); never inprotocol_stack_t, which is embedded inconn_stats_t.protocol_classifier_entrypointand the dispatcher; both re-classify unknown flows per packet.usm_context_init/payload copy, or the saving is lost.connection_protocol; the stale-entry GC must cover them.Validation: rerun the local throughput reproduction; USM classification tests; a false-negative harness with real HTTP/Postgres/Kafka/Redis clients started mid-stream, measuring segments-to-classification per protocol.
LouisParkin commentedon Oct 27, 2026
Proposed fix: StackVista/datadog-agent-upstream-for-process-agent#854 and #376.
Each connection now has a budget of classification attempts: 64 packet inspections when the handshake was seen, 1024 when we joined mid-stream. A connection that uses up its budget skips classification for 5 minutes.
Local reproduction (iperf3, 3000 active connections, Gbps):
This lowers the overhead; it does not remove it. For throughput benchmarking, the main advice still stands: use representative load, or disable protocol inspection.