perf: Upstream transports keep only 2 idle connections per host
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 80/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- go
- 領域
- backend, networking
調査の方向性
issueで指定されているトランスポート設定箇所で、core/tlsbridge/upstream.go と core/listener/forwardproxy/server.go を読みます。両方の箇所に、含まれている接続プール設定を追加し、その後 core/tlsbridge/ から提供された TestUpstreamPoolReuse の実験を繰り返し実行します。worker あたり約1つの upstream 接続が開かれ、その数倍にはならないことを完了の条件とします。
索引モデルが issue の本文から書いたものです。
説明
What happens
The proxy keeps only 2 idle upstream connections per host. When more than two requests to one host overlap, each extra request opens a new TCP+TLS connection rather than reusing one, and that connection is closed afterwards instead of being kept for the next request.
This hurts most on a gateway that speaks only HTTP/1.1, where each connection carries one request at a time. LiteLLM is one: it offers no h2 in ALPN. Each extra handshake costs about 40-90 ms on the laptop's VPN-reached gateways.
Reproduction
The bridge's own upstream client (tlsbridge.NewUpstreamClient), callers running at the same time, 10 sequential requests each, against one h1-only TLS origin. We counted the TCP connections the origin accepted, over three runs:
| Concurrent callers | Ideal | Today | With MaxIdleConnsPerHost: 100 |
|---|---|---|---|
| 4 | 4 | 5 / 16 / 19 | 5 / 5 / 5 |
| 10 | 10 | 36 / 39 / 45 | 18 / 19 / 23 |
| 20 | 20 | 66 / 76 / 79 | 46 / 49 / 49 |
Through the forward proxy end to end (10 bridged clients × 10 requests), the origin accepted 69 connections.
Test source (drop into core/tlsbridge/)
package tlsbridge
import (
"crypto/tls"
"io"
"net"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
)
func TestUpstreamPoolReuse(t *testing.T) {
var conns atomic.Int32
origin := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "ok") }))
origin.Config.ConnState = func(_ net.Conn, s http.ConnState) {
if s == http.StateNew {
conns.Add(1)
}
}
origin.TLS = &tls.Config{NextProtos: []string{"http/1.1"}} // h1-only, like LiteLLM
origin.StartTLS()
defer origin.Close()
const workers = 10
c, _ := NewUpstreamClient(nil, true)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
if resp, err := c.Get(origin.URL); err == nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
}()
}
wg.Wait()
t.Logf("%d callers x 10 requests: %d upstream connections (ideal %d)", workers, conns.Load(), workers)
}
Why
Both transports set MaxIdleConns: 100 (the total across all hosts) but not MaxIdleConnsPerHost, so Go's DefaultMaxIdleConnsPerHost of 2 applies:
core/tlsbridge/upstream.go:31: every decrypted (bridged) request goes through this transport.core/listener/forwardproxy/server.go:212: plain-HTTP forward-proxy requests.
Possible fix
Add MaxIdleConnsPerHost: 100 next to MaxIdleConns in both transports. Two lines.
Even with the fix, concurrent callers still open more connections than the ideal; Go dials a new connection whenever none is idle at that moment. A MaxConnsPerHost cap would bound that too, but it makes extra requests wait in a queue, so it is a separate decision.
Acceptance
The test above opens about workers connections, not several times that, across repeated runs.
Related, not verified
proxy.log on a laptop shows 65 fast EOF failures (128-221 ms) on LiteLLM POSTs. That is the usual sign of reusing a connection the server had just closed. Go retries such a request only when it can replay the body, and the forward proxy never sets r.GetBody after buffering the body (setBody, server.go:1306). We did not reproduce this, so it is noted here rather than filed.
Found by an experiment-driven audit of main at 43cf5539; every number above was re-measured on that commit.
Assisted-By: Claude (Anthropic AI) [email protected]
- 主要言語
- Go
- スター
- 21
- フォーク
- 43
- 平均マージ
- 13時間 8分
- マージ済み PR(30日)
- 267
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
rossoctl/cortex のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
UI shows costs for all sessions even when drilled into a session対応中かも @esnible が 16 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
rossoctl/cortex#1116 · コメント 2 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 半日 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 1週間以上 初心者へのやさしさ 18/100
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 45/100
メンテナーはふだん 1 日以内に返信
rossoctl/cortex の issue をすべて見る
似ている issue
-
area:recipes event:contribfest format:human-first source:agent status:needs-triage type:recipe
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
cadence-workflow/cadence-ai-samples#140 ·
メンテナーはふだん 1 日以内に返信
-
ux
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
evcc-io/evcc#35605 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
project-setup
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
bug report
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
type:bug
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
nightgauge/nightgauge#2732 ·
メンテナーはふだん 1 日以内に返信