Files
easyai-ai-gateway/apps/api/internal/runner/proxy.go
T
wangbo ea58d21d03 perf(queue): 限制大媒体任务内存并消除准入惊群
将同步与异步非文本任务的准入唤醒改为按任务和全局队首推进,批量续租等待者,避免大量请求同时争抢 advisory lock 和数据库连接。

对 Base64 请求解析、素材解码、上游媒体执行和结果物化增加分层并发限制,复用请求素材并释放重复 Gemini wire 数据;生产 API 默认入口解析并发 16,媒体物化并发 8。

新增 1000 个同步 Gemini 图像编辑请求的模拟上游压力验收,10 MiB 输入和输出下全部成功,Heap 峰值增长约 2.58 GiB,并验证共享上传哈希、单次 attempt 与本地零落盘。

验证:go test ./... -count=1;go vet ./...;gofmt -l 无输出;kubectl kustomize deploy/kubernetes/production;10 MiB Gemini Base64 千任务压力测试通过。
2026-07-30 13:13:32 +08:00

87 lines
2.5 KiB
Go

package runner
import (
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/netproxy"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type httpClientCache struct {
none *http.Client
global *http.Client
mu sync.Mutex
custom map[string]*http.Client
}
const providerHTTPClientTimeout = 10 * time.Minute
const (
providerHTTPMaxIdleConnections = 2048
providerHTTPMaxIdleConnectionsPerHost = 1024
)
func newHTTPClientCache() *httpClientCache {
return &httpClientCache{
none: newHTTPClient(nil),
global: newHTTPClient(http.ProxyFromEnvironment),
custom: map[string]*http.Client{},
}
}
func (s *Service) httpClientForCandidate(candidate store.RuntimeModelCandidate, simulated bool) (*http.Client, error) {
if simulated {
return s.httpClients.none, nil
}
config, err := netproxy.Normalize(netproxy.FromPlatformConfig(candidate.PlatformConfig))
if err != nil {
return nil, &clients.ClientError{Code: "invalid_proxy", Message: err.Error(), Retryable: false}
}
switch config.Mode {
case netproxy.ModeGlobal:
if strings.TrimSpace(s.cfg.GlobalHTTPProxy) != "" {
return s.httpClients.customClient(s.cfg.GlobalHTTPProxy)
}
return s.httpClients.global, nil
case netproxy.ModeCustom:
return s.httpClients.customClient(config.HTTPProxy)
default:
return s.httpClients.none, nil
}
}
func (c *httpClientCache) customClient(rawProxy string) (*http.Client, error) {
normalized, proxyURL, err := netproxy.ParseHTTPProxy(rawProxy)
if err != nil {
return nil, &clients.ClientError{Code: "invalid_proxy", Message: err.Error(), Retryable: false}
}
c.mu.Lock()
defer c.mu.Unlock()
if client := c.custom[normalized]; client != nil {
return client, nil
}
client := newHTTPClient(http.ProxyURL(proxyURL))
c.custom[normalized] = client
return client, nil
}
func newHTTPClient(proxy func(*http.Request) (*url.URL, error)) *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = proxy
// Media workers may keep hundreds of slow upstream tasks in flight and
// poll the same provider repeatedly. The standard library only retains two
// idle connections per host, which turns a high-concurrency polling load
// into avoidable TCP/TLS handshakes.
transport.MaxIdleConns = providerHTTPMaxIdleConnections
transport.MaxIdleConnsPerHost = providerHTTPMaxIdleConnectionsPerHost
return &http.Client{
Timeout: providerHTTPClientTimeout,
Transport: transport,
}
}