perf(ssh): reuse one connection per Spark instead of a login per poll - #83
Closed
de1tydev wants to merge 1 commit into
Closed
perf(ssh): reuse one connection per Spark instead of a login per poll#83de1tydev wants to merge 1 commit into
de1tydev wants to merge 1 commit into
Conversation
Collaborator
|
Superseded: content landed on main via the 2026-09-08 backlog integration (main tip 950ac31, full suite 312/312 green). Credit preserved in the merge history. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Every collector poll opens a brand-new SSH connection.
sshExecshells out tossh/sshpassper command, and nothing is reused between commands or betweenticks.
At the default cadence one remote Spark receives 217 logins per minute:
POLL_INTERVAL_CPU)echo ok, thencat /proc/uptime)Measured on a DGX Spark (Ubuntu 24.04, OpenSSH 9.6, 20 cores) monitored by a
Spark Dash instance with 10 units registered —
journalctl -u sshcounted 1085Acceptedin 5 minutes, i.e. exactly 217/min, matching the table.What that costs on the monitored Spark, sampled over 60s via cgroup CPU
accounting:
The metric commands themselves are nowhere near that: the GPU command (three
nvidia-smicalls plus/proc/meminfo) measures 120 ms of CPU, unified memory30 ms, the rest single-digit milliseconds — about 5.5 s/min of actual work
inside 75 s/min of cost. Roughly 90% of what the polling loop burns is
connection setup, and password auth makes it worse because the KDF runs on
every single one of those 217 logins.
The dashboard host pays too: its container measured 106 s of CPU per 30 s
(3.5 cores) with 40–50 concurrent
ssh/sshpassprocesses. And each loginwrites ~3 lines to the target's journal — ~940k lines per Spark per day.
The change
Multiplex.
sshCommandSpecnow addsControlMaster=auto,ControlPath=$TMPDIR/sparkdash-%CandControlPersist=300. The firstcommand connects; the rest open a channel on a socket that is already
authenticated.
%Chashes (host, user, port) to a fixed-length name so thepath can't exceed the ~104-byte
sun_pathlimit. A master that died leavesa stale socket;
ControlMaster=autonotices and replaces it.Opt out with
SSH_MULTIPLEX=0; tune the idle lifetime withSSH_CONTROL_PERSIST. The-Ntunnel inllmTunnel.jspassesmultiplex: false— a forward has to own its connection so that killing theprocess tears it down.
Fold the network link-speed read into the network round trip. It was a
second
sshExecfor/sys/class/net/<primary>/speed, issued after parsingthe route table. The command now dumps speeds for every interface alongside
the operstate loop it already runs, and the primary is picked from that map.
Fold the uptime read into the liveness check.
_checkOnline()ransshTest()(echo ok) and then_readUptime()(cat /proc/uptime) — twologins to learn one thing. Reading
/proc/uptimealready proves the sessioncame up, so it is now the liveness probe. The local-Spark path is unchanged.
Together 2 and 3 remove 42 of the 217 sessions on their own; multiplexing
removes essentially all of what remains.
Results
Same Spark, same cadence, after the change:
Same Spark, same cadence, after the change (60s cgroup samples, sshd
Acceptedcounted from the journal):ssh.serviceCPU / 60suser-1000.sliceCPU / 60s(The "before" column is already at a slowed-down 5s cadence; against the stock
2s cadence it is 217 logins/min and 1.25 cores, so end to end this is 1.25 →
0.037.) A second Spark measured 0.93 → 0.051 core. The dashboard host went from
3.5 cores to 0.167, and from 40–50 short-lived
sshprocesses to ~20 mostlyidle masters.
ssh.servicedropping to a flat zero is the direct evidence: preauth and thepassword KDF stop happening entirely. What is left on the Spark is close to the
cost of the metric commands themselves. Only one
Acceptedshows up in threeminutes — the master being established.
Snapshot fields all still populate, including the two that moved:
uptime,network.primaryInterface,network.linkSpeedMbps,network.wolMac,and
onlinefor 8 of 10 registered units (the other two are genuinely down).The
/proc/statdiffing still works at the slower cadence — busy units read94% / 91% GPU, idle ones read 0.
Tests
npx tsc --noEmitclean;npm test192 passed / 0 failed (190 before, plustwo new cases in
server/collectors/__tests__/ssh.test.jscovering the sharedmaster and the
multiplex: falseopt-out).Risk
A wedged master would stall that host's collectors instead of just one poll.
The existing per-command timeouts (
execFile10s, 8s for the NV_ERR scan) arethe backstop, and
ControlPersist=300keeps a socket from outliving a rebootedSpark for long.
MaxSessions(default 10) bounds concurrent channels; thecollector loop's in-flight guards keep it to a handful.
SSH_MULTIPLEX=0restores the previous behaviour without a rebuild.