feat: parallel OID walks with per-module walk_concurrency config#1652
feat: parallel OID walks with per-module walk_concurrency config#1652amuntean-godaddy wants to merge 9 commits into
Conversation
|
Not a maintainer, but took a look at the PR as it's a feature i've considered doing a PoC for but never got around to it. I think there's some significant issues with the PR as it stands:
Suggestion: a fixed pool of |
Sequential walking of N OID subtrees takes sum(N) seconds. On devices
with large interface tables (e.g. 347 interfaces at 45ms RTT), a
19-OID if-mib walk can exceed the scrape timeout. This change walks
OID subtrees concurrently within a single module scrape.
Implementation:
- Add Clone() to SNMPScraper so each goroutine gets its own SNMP
connection (gosnmp.GoSNMP is not goroutine-safe). Clone does a
field-by-field copy of GoSNMP, omitting the embedded sync.Mutex,
the OnSent/OnRecv/OnRetry callbacks (which capture per-scrape
counters by reference), and deep-copies SecurityParameters to
avoid shared AES/DES IV state across connections.
- Replace the sequential walk loop in ScrapeTarget with a goroutine
fan-out bounded by a semaphore channel. Goroutines are spawned for
all subtrees; the semaphore limits live connections to
WalkParams.WalkConcurrency. walkConcurrency <= 1 keeps the original
single-connection sequential path.
- Add WalkConcurrency int (yaml: walk_concurrency) to WalkParams with
default 1. Existing configs get sequential walks unchanged. Modules
with large tables can opt in via generator.yml or snmp.yml:
if_mib:
walk_concurrency: 4 # walk 4 OID subtrees in parallel
Signed-off-by: Alex Muntean <amuntean@godaddy.com>
When --snmp.source-address=host:port is set, Clone() previously copied the port verbatim. Every clone's Connect() then fails with 'bind: address already in use' because only one socket can hold that port. Strip the port so clones use the host address with an ephemeral port. Also copy ConnectError to mockSNMPScraper.Clone() so tests can simulate connect failure on clones. Signed-off-by: Alex Muntean <amuntean@godaddy.com>
Fixes all concurrency issues raised in review: 1. Race: the old design returned ScrapeTarget on first error while goroutines were still running and could Clone() the shared client mid-SetOptions. errgroup.WithContext + g.Wait() ensures no goroutine outlives ScrapeTarget. 2. Orphan connections: old design opened one connection per subtree (N). New design opens exactly walk_concurrency connections regardless of the number of subtrees. 3. Cancellation: on first walk error errgroup cancels ctx which is propagated to all worker connections via SetContext, making in-flight BulkWalkAll calls abort promptly. 4. Metrics: packets/retries counters are now atomic.Uint64 so the callbacks are safe to call from goroutines if they are ever set on clones in future. Promote golang.org/x/sync to direct dependency. Also fix pre-existing go vet warnings in collector_test.go: unkeyed config.Regexp struct literals now use the Regexp field name. Signed-off-by: Alex Muntean <amuntean@godaddy.com>
…ang.org/x/sync to direct - In ScrapeTarget parallel walk path, replace shared mu+append with workerResults[i] per-goroutine slices to avoid contention and unnecessary reallocation under the lock. Slices are merged after g.Wait() with no concurrency. - Capture loop variable i for closure correctness. - Remove now-unused var mu sync.Mutex. - Run go mod tidy: golang.org/x/sync is imported directly via errgroup and no longer carries // indirect. Signed-off-by: Alex Muntean <amuntean@godaddy.com>
Add TestScrapeTargetParallelWalk (walk_concurrency=3, verifies all 6 PDUs present and correct) and TestScrapeTargetParallelWalkConnectError (clone connect failure propagates without hang). The race detector now exercises the actual concurrent code path. Signed-off-by: Alex Muntean <amuntean@godaddy.com>
…ing metric help Fix 1 (Critical): GoSNMPWrapper.Clone() was silently dropping ExponentialTimeout, Control, and TrapSecurityParametersTable. Add all missing fields. Document why a raw struct copy cannot be used (GoSNMP embeds sync.Mutex, which go vet copylocks rejects). Add explicit comments listing fields intentionally omitted (Conn, OnSent/OnRecv/OnRetry/OnFinish/PreSend). Fix 2 (Critical): ScrapeTarget lacked a context.Context parameter, so the child context created by Collect() (ctx, cancel := context.WithCancel(c.ctx)) could not be threaded to the errgroup inside ScrapeTarget. Add ctx as first param to ScrapeTarget and collect(); have Collect() pass the child ctx (not c.ctx) to collect(), and collect() pass it to ScrapeTarget. Replace snmp.Context() / worker.SetContext() inside ScrapeTarget with the explicit gCtx derived from the passed ctx. Update the three ScrapeTarget call sites in collector_test.go. Fix 3 (Important): snmp_scrape_packets_sent and snmp_scrape_packets_retried had help strings claiming walk packets were counted. When walk_concurrency > 1, OnSent/OnRetry are not set on clone workers, so only GET-phase packets are counted. Update help strings to document this limitation accurately. Signed-off-by: Alex Muntean <amuntean@godaddy.com>
Context() and SetContext() on SNMPScraper have zero call sites — they were re-introduced by a previous fix subagent after commit 5a347df had already removed them cleanly. Drop them from the interface and their implementations in GoSNMPWrapper and mockSNMPScraper; also remove the now-unused "context" import from scraper.go and mock.go. go build ./... and go test ./... -race both pass. Signed-off-by: Alex Muntean <amuntean@godaddy.com> Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
f06da2c to
e5ebc0e
Compare
|
@SuperQ could you please review the current changes ? |
|
Looks like some CI issues. |
- Fix gci import grouping in collector.go, collector_test.go, scraper/gosnmp.go - Use errors.Is() in TestMockCloneCopiesConnectError (errorlint) - Remove deprecated NonRepeaters field from Clone() (staticcheck) Signed-off-by: Alex Muntean <amuntean@godaddy.com>
|
Hi @SuperQ @bastischubert @RichiH — addressed all the lint issues raised by CI:
The Build generator failure is an infrastructure flake (503 fetching Dell MIBs from an external URL — unrelated to this PR). The branch is ready for re-review. Thanks for your time! |
- gofumpt: reformat collector/collector.go and scraper/mock.go - README: remove sentence about partial-metric behaviour that was added for a separate abandoned PR Signed-off-by: Alex Muntean <amuntean@godaddy.com>
457682e to
a5e6556
Compare
|
@lukeod Want to give this another pass? |
Summary
Sequential walking of N OID subtrees in a module takes sum(N) seconds. On devices with large interface tables (e.g. a 48-port switch with hundreds of sub-interfaces, 19-OID
if-mibat 45ms RTT), the walk can exceed the configured scrape timeout.This PR adds parallel OID walks within a single module, controlled by a new
walk_concurrencyfield in the module config. The default is1— identical to the current sequential behaviour — so existing configs are completely unaffected.Changes
New config field:
walk_concurrencyPropagates through
generator.yml→snmp.ymllikemax_repetitionsandtimeout.Implementation
Fixed worker pool (
collector/collector.go):ScrapeTargetnow acceptsctx context.Contextas its first parameter. Whenwalk_concurrency > 1, a fixed pool ofmin(walk_concurrency, len(walk))workers is created, each with its own cloned SNMP connection. Workers pull OID subtrees from a pre-filled buffered channel.errgroup.WithContext(ctx)cancels all workers on the first error andg.Wait()guaranteesScrapeTargetnever returns with live goroutines.Clone()onSNMPScraper(scraper/): Each worker gets its own connection viaClone(). The implementation does a field-by-field copy ofGoSNMPand explicitly:OnSent,OnRecv,OnRetry,OnFinish,PreSend) — they capture per-scrape counters by referenceLocalAddrviahostOnly()— preventsbind: address already in usewith--snmp.source-address=host:portSecurityParametersvia.Copy()— avoids shared AES/DES IV state across connectionsConn— connections are not sharedMetrics:
packets/retriescounters incollect()usesync/atomictypes for correctness. Walk-phase packets are not counted whenwalk_concurrency > 1(clone callbacks are intentionally nil); the metric help strings document this.Testing
All packages pass. New tests:
TestScrapeTargetParallelWalk—walk_concurrency=3, 3 subtrees × 2 PDUs, verifies all 6 PDUs presentTestScrapeTargetParallelWalkConnectError— clone connect failure propagates cleanlyTestHostOnly,TestCloneStripsLocalAddrPort,TestMockCloneCopiesConnectError—Clone()correctness@SuperQ @bastischubert @RichiH