mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
appview: share one HTTP transport across blob proxy requests
NewProxyBlobStore built a fresh http.Client and http.Transport on every
call, and it is called once per registry request because the routing
repository is created per request. So no connection to the hold or to a
presigned S3 URL was ever reused: every XRPC call and every part upload
paid a TCP and TLS handshake, HTTP/2 was never negotiated, the idle pool
settings were dead config, and each discarded transport kept its idle
sockets open for the full 90s timeout.
One package-level transport and client now back every store. Settings
are unchanged; ForceAttemptHTTP2 is explicit to document intent. The
load balancer negotiates h2 on the frontend (416ba4a) but reaches the
hold over HTTP/1.1, so multiplexing stops at the LB. The win is the
handshake and socket churn, not multiplexing.
The struct field stays per-instance so tests can substitute a client.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yf1ZVA7sXYhQNb9tCo1m5
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
788b9e1053
commit
9228579b07
@@ -32,6 +32,36 @@ var (
|
||||
globalUploadsMu sync.RWMutex
|
||||
)
|
||||
|
||||
// The transport and client below are package-level on purpose. RoutingRepository
|
||||
// (and therefore ProxyBlobStore) is built fresh on every registry request, so a
|
||||
// per-instance transport was thrown away after a single request: nothing was ever
|
||||
// reused, every XRPC call to the hold and every presigned S3 request paid for a
|
||||
// fresh TCP + TLS handshake, the MaxIdleConns settings below were dead config, and
|
||||
// each discarded transport still sat on its idle sockets for the full
|
||||
// IdleConnTimeout. Sharing one transport process-wide is what makes the idle pool
|
||||
// and keep-alives mean anything.
|
||||
//
|
||||
// ForceAttemptHTTP2 is explicit to document intent; no custom DialContext or
|
||||
// TLSClientConfig is set, so Go's automatic HTTP/2 negotiation over ALPN stays on
|
||||
// (see 416ba4a, where the load balancer started speaking HTTP/2 on the frontend).
|
||||
// The LB talks HTTP/1.1 to the hold backend, so multiplexing stops at the LB. The
|
||||
// win here is not multiplexing: it is removing the handshake and socket churn.
|
||||
var (
|
||||
sharedTransport = &http.Transport{
|
||||
DisableKeepAlives: false,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
MaxConnsPerHost: 0, // unlimited
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
}
|
||||
|
||||
sharedHTTPClient = &http.Client{
|
||||
Timeout: 5 * time.Minute, // Timeout for presigned URL requests and uploads
|
||||
Transport: sharedTransport,
|
||||
}
|
||||
)
|
||||
|
||||
// ProxyBlobStore proxies blob requests to an external storage service
|
||||
type ProxyBlobStore struct {
|
||||
ctx *RegistryContext // All context and services
|
||||
@@ -49,16 +79,9 @@ func NewProxyBlobStore(ctx *RegistryContext) *ProxyBlobStore {
|
||||
return &ProxyBlobStore{
|
||||
ctx: ctx,
|
||||
holdURL: holdURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Minute, // Timeout for presigned URL requests and uploads
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: false, // Re-enable keep-alive
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
MaxConnsPerHost: 0, // unlimited
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
},
|
||||
},
|
||||
// Field stays per-instance so tests can substitute a client; the default
|
||||
// points at the process-wide client so connections are actually reused.
|
||||
httpClient: sharedHTTPClient,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user