perf: TCP_NODELAY + cheap allocs/lookups on the engine_* hot path#27
perf: TCP_NODELAY + cheap allocs/lookups on the engine_* hot path#27KA-ROM wants to merge 23 commits into
Conversation
| let tcp_nodelay = actix_tls::connect::Connector::new( | ||
| actix_tls::connect::Resolver::default(), | ||
| ) | ||
| .service() | ||
| .map(|conn: actix_tls::connect::Connection<awc::http::Uri, tokio::net::TcpStream>| { | ||
| let _ = conn.io_ref().set_nodelay(true); | ||
| let _ = socket2::SockRef::from(conn.io_ref()).set_tcp_keepalive( | ||
| &socket2::TcpKeepalive::new().with_time(Duration::from_secs(60)), | ||
| ); | ||
| conn | ||
| }); |
There was a problem hiding this comment.
question:
w.d.y.t. about putting this behind CLI-switch? (so that the user could choose on case-by-case).
(TCP_NODELAY could still be the default).
There was a problem hiding this comment.
imo the CLI surface bloat doesn't seem worth it. rproxy's workload class is request-response JSON-RPC (small writes, latency-sensitive). Nagle on doesn't help anything in that shape. Nagle's actual use case is bundling many small writes into bigger packets to reduce per-packet overhead on slow/packet-counted links, which doesn't match the kind of traffic rproxy is built for. but I am open to more pushback, maybe you have use-cases in mind I didn't consider
| let mut clnt_res = Self::to_client_response(&bknd_res); | ||
|
|
||
| let preallocate = this.shared.config().prealloacated_response_buffer_size(); | ||
| let content_encoding = bknd_res.headers().get(header::CONTENT_ENCODING).cloned(); |
There was a problem hiding this comment.
question:
are we sure content-encoding is the only header we want to send back to the client?
There was a problem hiding this comment.
we still forward every header to the client (the loop in to_client_response). this change only affects what we keep around internally for our own processing. before, it copied the whole header bag, now we only copy the one header (content-encoding), to decompress the body for logging. nothing the client sees should be different
Summary
Hot-path tuning for the
engine_*proxy path: removes per-request allocations andredundant lookups, sets TCP_NODELAY on both client- and backend-facing connections,
and folds the cached metric handles into a single
ProxyHttpMetricsstruct. Severalpost-process and TCP-tuning experiments were explored and reverted in-branch
Removed from the hot path
is_hop_by_hop_headermatchesHeaderNameconstants directly; the per-call lowercasedStringis gone. (Set is unchanged from before: connection/host/keep-alive/transfer-encoding.)Gauge/Counter+proxy_failure_counthandles resolved once per worker, bundled intoProxyHttpMetricsbehindArcso cloning a handle to span anawaitis one ref-count bump (scc::HashMap::cloneis a deep copy).scc::HashMap<String, Counter>keyed by&str. The cache is soft-capped at 100 entries to bound the lookup map.get_or_createruns unconditionally on a miss). Safe because the authrpc client side is a closed network (sequencer/peer-mirror) with tiny UA cardinality. A safety comment on the field flags that a public-facing endpoint must gateget_or_createinstead.connection_info()bound once/request instead of 5x: skips redundant RefCell re-borrow + extensions lookup per call.headers_mut().append(...)skips reparsing names through awc's builder. Also fixes a latent multi-value drop (insert_headerreplaced same-name values).HeaderName::from_str(backend names already valid; the removed branch could only ever silently drop a header).HeaderMapclone: capture onlycontent-encoding(the one field read downstream for decompress-on-logging). Client-facing headers are still forwarded verbatim.Behavior changes (observable on wire / in ops)
ConnectionGuard::on_connectalongside the existing keepalive setup.Refactor (no behavior change)
ProxyHttpMetricsstruct: extracts cached handles + UA cache out ofProxyHttpinto anArc<ProxyHttpMetrics>withbump_client_info,inc/dec_in_flight_*,record_proxy_failure.Explored and reverted (kept in history)
Each showed no measurable benefit on our envs / flag sets, or introduced a structural concern. Kept as separate revert commits so any can be re-explored.
JrpcRequestMetaMaybeBatchinfinalise_proxying: only fires on the dedup-on path (off in all our envs), so dead code in practice.postprocess_client_requestoff the critical path: introduced a dependency on actix-web's body-poll ordering; structural fragility not worth the unmeasured saving.disconnect_timeoutto 100ms: awc callsno_disconnect_timeout()for plaintext TCP pools, so the knob was inert for ourhttp://backends. A configurable flag is deferred to a follow-up PR.conn_keep_alive(2 * backend_timeout)is also 60s, so the pool drops the idle conn at the same idleness threshold; no-op on loopback.Unchanged