perf(net): set TCP_NODELAY on all tunnel and proxy TCP hops#2220
Conversation
The sandbox tunnel added ~44 ms of latency to every small request/response because no socket in the path disabled Nagle's algorithm, so sub-MSS writes waited on delayed ACKs at each hop. Set TCP_NODELAY on every latency-sensitive TCP socket: - gateway: accepted connections on the public listener (gRPC relay frames and WS tunnel writes) - CLI: edge tunnel local accept + underlying WebSocket TCP stream, insecure TLS connector (tonic's default connector already does this), and service-forward accepted sockets - supervisor: direct-tcpip connect into the sandbox netns, TCP relay target dials, egress proxy accepted connections, and all upstream CONNECT/HTTP dials (via a new connect_upstream helper) Setting TCP_NODELAY on connect is best-effort: a failure only costs latency, so we log and continue rather than fail the connection. The sandbox SSH transport rides a unix domain socket and gRPC client channels use tonic defaults (nodelay on), so no change is needed there. Fixes NVIDIA#2219 Signed-off-by: Jim Meyer <jim@meyer4hire.com>
Address review feedback on the TCP_NODELAY change: - Move the shared set-nodelay helper out of supervisor_session into a new crate-private `net` module in openshell-supervisor-process, so ssh and supervisor_session no longer reach across modules through a pub(crate) item. - Make the best-effort comments at each call site terse and consistent. No behavior change; the benchmark ladder reproduces the same numbers. Signed-off-by: Jim Meyer <jim@meyer4hire.com>
Resolve conflicts from NVIDIA#2162 (fail closed when credential placeholders cannot be rewritten), which restructured handle_tcp_connection to validate addresses, run the fail-closed TLS gate, then connect upstream: - proxy.rs: keep main's validate-then-connect ordering and route the post-gate dial through our connect_upstream helper, so TCP_NODELAY is still set without connecting before the fail-closed gate. - ssh.rs: keep both our TCP_NODELAY regression test and main's new socket-permission tests in the same mod tests block. Signed-off-by: Jim Meyer <jim@meyer4hire.com>
|
Still in progress. I've been using this to learn more about OpenShell, and what I've learned is that this fix is not central enough yet. I think one more good iteration from me and Claude will get us to a decent place. |
|
Sounds good @purp ! |
Move the best-effort TCP_NODELAY helpers into the shared openshell_core::net module so every crate dials and configures sockets the same way: - Add set_tcp_nodelay_best_effort (accepted/existing streams) and connect_tcp_nodelay_best_effort (dial + set) with unit tests. - Migrate all call sites in openshell-cli, openshell-server, and the supervisor crates to the shared helpers. - Remove the crate-private net module from openshell-supervisor-process. - Document socket guidance in AGENTS.md (Network Sockets). Signed-off-by: Jim Meyer <jim@meyer4hire.com>
|
Okay, I think I'm happy with this for the moment. Initial implementation was a bit too shallow and copy-paste-y across many files, so we drove the change down into Some notes: The Claude's take:
It's trivial to imagine someone adding a new Claude's take:
Ready for review. |
Summary
Every small request/response through the sandbox relay path paid a fixed
~44 ms penalty because no socket in the path disabled Nagle's algorithm, so
sub-MSS ("tinygram") writes stalled waiting on TCP delayed ACKs at each hop.
This sets
TCP_NODELAYon every latency-sensitive TCP socket in the tunneland egress-proxy path, cutting the marginal cost of a small governed
request from ~44 ms to ~0.3–0.5 ms — within a few hundred microseconds of
the host loopback floor — with no regression on bulk transfers.
Opening as a draft to invite discussion of the approach before it's marked
ready.
Related Issue
Fixes #2219
Changes
TCP_NODELAYon accepted connections on the publiclistener (gRPC relay frames and WS tunnel writes).
stream, the insecure TLS connector (matching tonic's default connector),
and service-forward accepted sockets.
target dials (
connect_tcp_target), egress-proxy accepted connections,and all upstream CONNECT/HTTP dials (via a new
connect_upstreamhelper).TCP_NODELAYon connect is best-effort: a failure only costslatency, so we log and continue rather than fail the connection.
TCP_NODELAYis set on the proxy upstream,direct-tcpip, and TCP-relay connect paths.
The sandbox SSH transport rides a unix domain socket and gRPC client
channels use tonic defaults (nodelay on), so no change is needed there.
Testing
mise run pre-commitpassesconnect hops)
with a component-isolation benchmark ladder instead (below)
A/B benchmark ladder (medians over 30 reps/cell; macOS arm64, Docker
compute driver). Tools and full results are on a temporary branch:
relay-latency-benchmarking.Keep-alive marginal cost per small request:
No regression (worst cases): 10 MB response 58.6 → 59.0 ms (unchanged);
10 MB request 90.4 → 46.0 ms (Nagle-throttled uploads got ~2× faster);
L7 body-rewrite rung 1 KB 63.7 → 14.9 ms.
Checklist
socket-option tuning with no change to subsystem boundaries or data flow