[Metrics] Measure the SSH round trip past the API server - #10612
Draft
zpoint wants to merge 1 commit into
Draft
Conversation
`sky_apiserver_websocket_ssh_latency_seconds` is widely read as keystroke
latency. It is neither: it is a synthetic PING the client sends every 10s,
and the server echoes it without forwarding, so the kubectl port-forward
tunnel and the pod's sshd are excluded by construction. A short interactive
session can produce zero samples while every keystroke is measurably slow.
Add `sky_apiserver_ssh_backend_turnaround_seconds`, which covers exactly the
excluded leg: API server -> port-forward -> kubelet -> pod sshd -> shell echo
-> back. It costs no bytes on the wire and no client support. The proxy loop
already inspects every frame, so a keystroke-sized write is timestamped and
paired with the next read from the backend; a stream of keystrokes is an
ordered request/response conversation, which is what makes the pairing sound.
Samples are dropped when the write is too large to be a keystroke, or when
the reply takes longer than 2s and is more likely unrelated output.
Add `sky_apiserver_ssh_sessions_total{path}` alongside it. Without a
denominator, an empty histogram is ambiguous: nobody is SSHing, or every
session was redirected away by a plugin hook, or the pairing broke. The
redirect branch increments it before returning, so a handed-off session is
still counted.
Also in this change:
- Rewrite the heartbeat metric's Help string to say what it is, what it
excludes, and when it is empty rather than zero.
- Retitle the "SSH RTT to API Server P95" dashboard panel accordingly, and
add panels for the two new metrics.
- Use `time.monotonic()` for the heartbeat interval instead of
`time.time()`. Both ends are read in one process, so the wall clock buys
nothing and costs correctness: an NTP step or a laptop suspend inside the
10s window is charged to SSH latency, and a backwards step produces a
negative interval that wraps when packed into the unsigned '!Q'.
- Give the new metrics a bucket ladder starting at 1ms. `_LATENCY_BUCKETS`
starts at 5ms because it is sized for request durations; measured on a
local kind cluster this leg runs at a ~3ms median, so every healthy value
would land in one bucket.
Measured end to end against a kind cluster, typing over a real `ssh`: 284
paired samples at a ~3ms median. Pausing the node container for 0.7s mid-
keystroke put exactly three samples in the (0.5s, 1s] bucket while the
heartbeat, over 17 pings in the same window, stayed at a 0.88ms mean --
blind to the stall, which is the point.
zpoint
marked this pull request as draft
August 31, 2026 09:08
Comment on lines
+1573
to
+1577
| "gridPos": { | ||
| "h": 8, | ||
| "w": 12, | ||
| "x": 5, | ||
| "y": 47 |
Contributor
There was a problem hiding this comment.
🟡 SSH panels obscure internal metrics
The new panels reuse gridPos rows occupied by the Internals section. Grafana overlays unrelated charts, making both sections unreadable.
Prompt for agents
Resolve the Grafana grid collision in charts/skypilot/manifests/api-server-overview.json. The new SSH backend panel starts at y=47 and the sessions panel at y=55, while the existing full-width Internals row starts at y=47 and its panels occupy the same ranges. Reposition the Internals row and every panel beneath it far enough downward to preserve the two new eight-row SSH panels, then verify that no gridPos rectangles overlap.
Was this helpful? React with 👍 or 👎 to provide feedback.
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.
Problem
sky_apiserver_websocket_ssh_latency_secondsis widely read as keystroke latency. It is neither keystroke latency nor a measurement of the path to the pod:websocket_proxy.py,HEARTBEAT_INTERVAL_SECONDS), not something a keypress triggers.continues without forwarding it (websocket_utils.py), so thekubectl port-forwardtunnel and the pod's sshd are excluded by construction.The practical consequence: a short interactive session can produce zero samples while every keystroke is measurably slow. Measured on a kind cluster, a ~12s
sshsession recorded..._count = 0on the heartbeat while 29 keystroke round trips were happening.What this adds
sky_apiserver_ssh_backend_turnaround_seconds{path}covers exactly the excluded leg: API server → port-forward → kubelet → pod sshd → shell echo → back.It adds no bytes to the wire and needs no client support. The proxy loop already unpacks a header per frame, so a keystroke-sized write to the backend is timestamped and paired with the next read. A stream of keystrokes is an ordered request/response conversation, which is what makes the pairing sound; when typing pipelines, the oldest outstanding stamp is kept, since the first read answers the first write. Samples are dropped when the write is too large to be a keystroke (a paste also clears any outstanding stamp) or when the reply takes longer than 2s and is more likely unrelated output than a very slow echo.
This is a distribution, not a per-keystroke truth — backend traffic that answers no write can attach a read to the wrong write. The docstring says so.
sky_apiserver_ssh_sessions_total{path}is the denominator. Without it an empty histogram is ambiguous: nobody is SSHing, every session was redirected away by a plugin hook, or the pairing broke. The redirect branch increments it before returning, so a handed-off session is still counted.Also here:
Helpstring now says what it is, what it excludes, and when it is empty rather than zero.time.monotonic()replacestime.time()for the heartbeat interval. Both ends are read in one process, so the wall clock buys nothing and costs correctness: an NTP step or a laptop suspend inside the 10s window is charged to SSH latency, and a backwards step yields a negative interval that wraps when packed into the unsigned!Q._LATENCY_BUCKETSstarts at 5ms because it is sized for request durations; this leg measures a ~3ms median in-cluster, so every healthy value would land in one bucket.Testing
15 unit tests in
tests/unit_tests/test_sky/server/test_websocket_utils.pycover the pairing rules and drive the realrun_websocket_proxyloop, including that a heartbeat PING must not produce a turnaround sample.Measured end to end against a kind cluster, typing over a real
ssh:--deploy, 10 server processes): samples written by a worker process are correctly aggregated by the multiprocess collector into the/metricsserved from the main process.promtool check metricson the live endpoint reports no issues for the new metrics.An earlier version of the pairing discarded a sample whenever a second keystroke arrived before the first was answered. The end-to-end stall test showed that throws away precisely the samples worth having, since a stalled backend is exactly when a user keeps typing into the lag. Hence keeping the oldest stamp.