From c47bb7295e60bec8a0b34c874464001cac7239c3 Mon Sep 17 00:00:00 2001 From: mikewong23571 Date: Sat, 22 Nov 2025 05:01:46 -0800 Subject: [PATCH] perf(proxy): cache agents with opt-in pooling --- .env.example | 4 +++ config/config.example.js | 24 ++++++++++++++ docker-compose.yml | 6 +++- src/utils/proxyHelper.js | 68 +++++++++++++++++++++++++++++++++++++--- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index c5e7e2e6..704d0a8a 100644 --- a/.env.example +++ b/.env.example @@ -53,6 +53,10 @@ DEFAULT_PROXY_TIMEOUT=600000 MAX_PROXY_RETRIES=3 # IP协议族配置:true=IPv4, false=IPv6, 默认IPv4(兼容性更好) PROXY_USE_IPV4=true +# 代理连接池 / Keep-Alive 配置(默认关闭,如需启用请取消注释) +# PROXY_KEEP_ALIVE=true +# PROXY_MAX_SOCKETS=50 +# PROXY_MAX_FREE_SOCKETS=10 # ⏱️ 请求超时配置 REQUEST_TIMEOUT=600000 # 请求超时设置(毫秒),默认10分钟 diff --git a/config/config.example.js b/config/config.example.js index adb17ec6..5395142a 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -73,6 +73,30 @@ const config = { proxy: { timeout: parseInt(process.env.DEFAULT_PROXY_TIMEOUT) || 600000, // 10分钟 maxRetries: parseInt(process.env.MAX_PROXY_RETRIES) || 3, + // 连接池与 Keep-Alive 配置(默认关闭,需要显式开启) + keepAlive: (() => { + if (process.env.PROXY_KEEP_ALIVE === undefined || process.env.PROXY_KEEP_ALIVE === '') { + return false + } + return process.env.PROXY_KEEP_ALIVE === 'true' + })(), + maxSockets: (() => { + if (process.env.PROXY_MAX_SOCKETS === undefined || process.env.PROXY_MAX_SOCKETS === '') { + return undefined + } + const parsed = parseInt(process.env.PROXY_MAX_SOCKETS) + return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined + })(), + maxFreeSockets: (() => { + if ( + process.env.PROXY_MAX_FREE_SOCKETS === undefined || + process.env.PROXY_MAX_FREE_SOCKETS === '' + ) { + return undefined + } + const parsed = parseInt(process.env.PROXY_MAX_FREE_SOCKETS) + return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined + })(), // IP协议族配置:true=IPv4, false=IPv6, 默认IPv4(兼容性更好) useIPv4: process.env.PROXY_USE_IPV4 !== 'false' // 默认 true,只有明确设置为 'false' 才使用 IPv6 }, diff --git a/docker-compose.yml b/docker-compose.yml index 608284e1..79b9afb8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,6 +46,10 @@ services: # 🌐 代理配置 - DEFAULT_PROXY_TIMEOUT=${DEFAULT_PROXY_TIMEOUT:-60000} - MAX_PROXY_RETRIES=${MAX_PROXY_RETRIES:-3} + - PROXY_USE_IPV4=${PROXY_USE_IPV4:-true} + - PROXY_KEEP_ALIVE=${PROXY_KEEP_ALIVE:-} + - PROXY_MAX_SOCKETS=${PROXY_MAX_SOCKETS:-} + - PROXY_MAX_FREE_SOCKETS=${PROXY_MAX_FREE_SOCKETS:-} # 📈 使用限制 - DEFAULT_TOKEN_LIMIT=${DEFAULT_TOKEN_LIMIT:-1000000} @@ -162,4 +166,4 @@ volumes: networks: claude-relay-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/src/utils/proxyHelper.js b/src/utils/proxyHelper.js index ecaa6b25..d8256383 100644 --- a/src/utils/proxyHelper.js +++ b/src/utils/proxyHelper.js @@ -8,6 +8,9 @@ const config = require('../../config/config') * 支持 SOCKS5 和 HTTP/HTTPS 代理,可配置 IPv4/IPv6 */ class ProxyHelper { + // 缓存代理 Agent,避免重复创建浪费连接 + static _agentCache = new Map() + /** * 创建代理 Agent * @param {object|string|null} proxyConfig - 代理配置对象或 JSON 字符串 @@ -33,34 +36,91 @@ class ProxyHelper { // 获取 IPv4/IPv6 配置 const useIPv4 = ProxyHelper._getIPFamilyPreference(options.useIPv4) + // 配置连接池与 Keep-Alive + const proxySettings = config.proxy || {} + const agentCommonOptions = {} + + if (typeof proxySettings.keepAlive === 'boolean') { + agentCommonOptions.keepAlive = proxySettings.keepAlive + } + + if ( + typeof proxySettings.maxSockets === 'number' && + Number.isFinite(proxySettings.maxSockets) && + proxySettings.maxSockets > 0 + ) { + agentCommonOptions.maxSockets = proxySettings.maxSockets + } + + if ( + typeof proxySettings.maxFreeSockets === 'number' && + Number.isFinite(proxySettings.maxFreeSockets) && + proxySettings.maxFreeSockets >= 0 + ) { + agentCommonOptions.maxFreeSockets = proxySettings.maxFreeSockets + } + + if ( + typeof proxySettings.timeout === 'number' && + Number.isFinite(proxySettings.timeout) && + proxySettings.timeout > 0 + ) { + agentCommonOptions.timeout = proxySettings.timeout + } + + // 缓存键:保证相同配置的代理可复用 + const cacheKey = JSON.stringify({ + type: proxy.type, + host: proxy.host, + port: proxy.port, + username: proxy.username, + password: proxy.password, + family: useIPv4, + keepAlive: agentCommonOptions.keepAlive, + maxSockets: agentCommonOptions.maxSockets, + maxFreeSockets: agentCommonOptions.maxFreeSockets, + timeout: agentCommonOptions.timeout + }) + + if (ProxyHelper._agentCache.has(cacheKey)) { + return ProxyHelper._agentCache.get(cacheKey) + } + // 构建认证信息 const auth = proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : '' + let agent = null // 根据代理类型创建 Agent if (proxy.type === 'socks5') { const socksUrl = `socks5h://${auth}${proxy.host}:${proxy.port}` - const socksOptions = {} + const socksOptions = { ...agentCommonOptions } // 设置 IP 协议族(如果指定) if (useIPv4 !== null) { socksOptions.family = useIPv4 ? 4 : 6 } - return new SocksProxyAgent(socksUrl, socksOptions) + agent = new SocksProxyAgent(socksUrl, socksOptions) } else if (proxy.type === 'http' || proxy.type === 'https') { const proxyUrl = `${proxy.type}://${auth}${proxy.host}:${proxy.port}` - const httpOptions = {} + const httpOptions = { ...agentCommonOptions } // HttpsProxyAgent 支持 family 参数(通过底层的 agent-base) if (useIPv4 !== null) { httpOptions.family = useIPv4 ? 4 : 6 } - return new HttpsProxyAgent(proxyUrl, httpOptions) + agent = new HttpsProxyAgent(proxyUrl, httpOptions) } else { logger.warn(`⚠️ Unsupported proxy type: ${proxy.type}`) return null } + + if (agent) { + ProxyHelper._agentCache.set(cacheKey, agent) + } + + return agent } catch (error) { logger.warn('⚠️ Failed to create proxy agent:', error.message) return null