Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
820bb16c
Commit
820bb16c
authored
Dec 31, 2025
by
yangjianbo
Browse files
fix(网关): 防止连接池缓存失控
超限且无可淘汰条目时拒绝新建 规范化代理地址并更新失败时的访问时间 补充连接池上限与代理规范化测试
parent
d1c98896
Changes
2
Show whitespace changes
Inline
Side-by-side
backend/internal/repository/http_upstream.go
View file @
820bb16c
package
repository
package
repository
import
(
import
(
"errors"
"fmt"
"fmt"
"io"
"io"
"net"
"net/http"
"net/http"
"net/url"
"net/url"
"strings"
"strings"
...
@@ -40,6 +42,8 @@ const (
...
@@ -40,6 +42,8 @@ const (
defaultClientIdleTTLSeconds
=
900
defaultClientIdleTTLSeconds
=
900
)
)
var
errUpstreamClientLimitReached
=
errors
.
New
(
"upstream client cache limit reached"
)
// poolSettings 连接池配置参数
// poolSettings 连接池配置参数
// 封装 Transport 所需的各项连接池参数
// 封装 Transport 所需的各项连接池参数
type
poolSettings
struct
{
type
poolSettings
struct
{
...
@@ -116,13 +120,17 @@ func NewHTTPUpstream(cfg *config.Config) service.HTTPUpstream {
...
@@ -116,13 +120,17 @@ func NewHTTPUpstream(cfg *config.Config) service.HTTPUpstream {
// - inFlight > 0 的客户端不会被淘汰,确保活跃请求不被中断
// - inFlight > 0 的客户端不会被淘汰,确保活跃请求不被中断
func
(
s
*
httpUpstreamService
)
Do
(
req
*
http
.
Request
,
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
)
(
*
http
.
Response
,
error
)
{
func
(
s
*
httpUpstreamService
)
Do
(
req
*
http
.
Request
,
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
)
(
*
http
.
Response
,
error
)
{
// 获取或创建对应的客户端,并标记请求占用
// 获取或创建对应的客户端,并标记请求占用
entry
:=
s
.
acquireClient
(
proxyURL
,
accountID
,
accountConcurrency
)
entry
,
err
:=
s
.
acquireClient
(
proxyURL
,
accountID
,
accountConcurrency
)
if
err
!=
nil
{
return
nil
,
err
}
// 执行请求
// 执行请求
resp
,
err
:=
entry
.
client
.
Do
(
req
)
resp
,
err
:=
entry
.
client
.
Do
(
req
)
if
err
!=
nil
{
if
err
!=
nil
{
// 请求失败,立即减少计数
// 请求失败,立即减少计数
atomic
.
AddInt64
(
&
entry
.
inFlight
,
-
1
)
atomic
.
AddInt64
(
&
entry
.
inFlight
,
-
1
)
atomic
.
StoreInt64
(
&
entry
.
lastUsed
,
time
.
Now
()
.
UnixNano
())
return
nil
,
err
return
nil
,
err
}
}
...
@@ -138,8 +146,8 @@ func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID i
...
@@ -138,8 +146,8 @@ func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID i
// acquireClient 获取或创建客户端,并标记为进行中请求
// acquireClient 获取或创建客户端,并标记为进行中请求
// 用于请求路径,避免在获取后被淘汰
// 用于请求路径,避免在获取后被淘汰
func
(
s
*
httpUpstreamService
)
acquireClient
(
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
)
*
upstreamClientEntry
{
func
(
s
*
httpUpstreamService
)
acquireClient
(
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
)
(
*
upstreamClientEntry
,
error
)
{
return
s
.
getClientEntry
(
proxyURL
,
accountID
,
accountConcurrency
,
true
)
return
s
.
getClientEntry
(
proxyURL
,
accountID
,
accountConcurrency
,
true
,
true
)
}
}
// getOrCreateClient 获取或创建客户端
// getOrCreateClient 获取或创建客户端
...
@@ -158,12 +166,14 @@ func (s *httpUpstreamService) acquireClient(proxyURL string, accountID int64, ac
...
@@ -158,12 +166,14 @@ func (s *httpUpstreamService) acquireClient(proxyURL string, accountID int64, ac
// - account: 按账户隔离,同一账户共享客户端(代理变更时重建)
// - account: 按账户隔离,同一账户共享客户端(代理变更时重建)
// - account_proxy: 按账户+代理组合隔离,最细粒度
// - account_proxy: 按账户+代理组合隔离,最细粒度
func
(
s
*
httpUpstreamService
)
getOrCreateClient
(
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
)
*
upstreamClientEntry
{
func
(
s
*
httpUpstreamService
)
getOrCreateClient
(
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
)
*
upstreamClientEntry
{
return
s
.
getClientEntry
(
proxyURL
,
accountID
,
accountConcurrency
,
false
)
entry
,
_
:=
s
.
getClientEntry
(
proxyURL
,
accountID
,
accountConcurrency
,
false
,
false
)
return
entry
}
}
// getClientEntry 获取或创建客户端条目
// getClientEntry 获取或创建客户端条目
// markInFlight=true 时会标记进行中请求,用于请求路径防止被淘汰
// markInFlight=true 时会标记进行中请求,用于请求路径防止被淘汰
func
(
s
*
httpUpstreamService
)
getClientEntry
(
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
,
markInFlight
bool
)
*
upstreamClientEntry
{
// enforceLimit=true 时会限制客户端数量,超限且无法淘汰时返回错误
func
(
s
*
httpUpstreamService
)
getClientEntry
(
proxyURL
string
,
accountID
int64
,
accountConcurrency
int
,
markInFlight
bool
,
enforceLimit
bool
)
(
*
upstreamClientEntry
,
error
)
{
// 获取隔离模式
// 获取隔离模式
isolation
:=
s
.
getIsolationMode
()
isolation
:=
s
.
getIsolationMode
()
// 标准化代理 URL 并解析
// 标准化代理 URL 并解析
...
@@ -184,7 +194,7 @@ func (s *httpUpstreamService) getClientEntry(proxyURL string, accountID int64, a
...
@@ -184,7 +194,7 @@ func (s *httpUpstreamService) getClientEntry(proxyURL string, accountID int64, a
atomic
.
AddInt64
(
&
entry
.
inFlight
,
1
)
atomic
.
AddInt64
(
&
entry
.
inFlight
,
1
)
}
}
s
.
mu
.
RUnlock
()
s
.
mu
.
RUnlock
()
return
entry
return
entry
,
nil
}
}
s
.
mu
.
RUnlock
()
s
.
mu
.
RUnlock
()
...
@@ -197,11 +207,22 @@ func (s *httpUpstreamService) getClientEntry(proxyURL string, accountID int64, a
...
@@ -197,11 +207,22 @@ func (s *httpUpstreamService) getClientEntry(proxyURL string, accountID int64, a
atomic
.
AddInt64
(
&
entry
.
inFlight
,
1
)
atomic
.
AddInt64
(
&
entry
.
inFlight
,
1
)
}
}
s
.
mu
.
Unlock
()
s
.
mu
.
Unlock
()
return
entry
return
entry
,
nil
}
}
s
.
removeClientLocked
(
cacheKey
,
entry
)
s
.
removeClientLocked
(
cacheKey
,
entry
)
}
}
// 超出缓存上限时尝试淘汰,无法淘汰则拒绝新建
if
enforceLimit
&&
s
.
maxUpstreamClients
()
>
0
{
s
.
evictIdleLocked
(
now
)
if
len
(
s
.
clients
)
>=
s
.
maxUpstreamClients
()
{
if
!
s
.
evictOldestIdleLocked
()
{
s
.
mu
.
Unlock
()
return
nil
,
errUpstreamClientLimitReached
}
}
}
// 缓存未命中或需要重建,创建新客户端
// 缓存未命中或需要重建,创建新客户端
settings
:=
s
.
resolvePoolSettings
(
isolation
,
accountConcurrency
)
settings
:=
s
.
resolvePoolSettings
(
isolation
,
accountConcurrency
)
client
:=
&
http
.
Client
{
Transport
:
buildUpstreamTransport
(
settings
,
parsedProxy
)}
client
:=
&
http
.
Client
{
Transport
:
buildUpstreamTransport
(
settings
,
parsedProxy
)}
...
@@ -220,7 +241,7 @@ func (s *httpUpstreamService) getClientEntry(proxyURL string, accountID int64, a
...
@@ -220,7 +241,7 @@ func (s *httpUpstreamService) getClientEntry(proxyURL string, accountID int64, a
s
.
evictIdleLocked
(
now
)
s
.
evictIdleLocked
(
now
)
s
.
evictOverLimitLocked
()
s
.
evictOverLimitLocked
()
s
.
mu
.
Unlock
()
s
.
mu
.
Unlock
()
return
entry
return
entry
,
nil
}
}
// shouldReuseEntry 判断缓存条目是否可复用
// shouldReuseEntry 判断缓存条目是否可复用
...
@@ -277,15 +298,8 @@ func (s *httpUpstreamService) evictIdleLocked(now time.Time) {
...
@@ -277,15 +298,8 @@ func (s *httpUpstreamService) evictIdleLocked(now time.Time) {
}
}
}
}
// evictOverLimitLocked 淘汰超出数量限制的客户端(需持有锁)
// evictOldestIdleLocked 淘汰最久未使用且无活跃请求的客户端(需持有锁)
// 使用 LRU 策略,优先淘汰最久未使用且无活跃请求的客户端
func
(
s
*
httpUpstreamService
)
evictOldestIdleLocked
()
bool
{
func
(
s
*
httpUpstreamService
)
evictOverLimitLocked
()
{
maxClients
:=
s
.
maxUpstreamClients
()
if
maxClients
<=
0
{
return
}
// 循环淘汰直到满足数量限制
for
len
(
s
.
clients
)
>
maxClients
{
var
(
var
(
oldestKey
string
oldestKey
string
oldestEntry
*
upstreamClientEntry
oldestEntry
*
upstreamClientEntry
...
@@ -306,10 +320,28 @@ func (s *httpUpstreamService) evictOverLimitLocked() {
...
@@ -306,10 +320,28 @@ func (s *httpUpstreamService) evictOverLimitLocked() {
}
}
// 所有客户端都有活跃请求,无法淘汰
// 所有客户端都有活跃请求,无法淘汰
if
oldestEntry
==
nil
{
if
oldestEntry
==
nil
{
return
return
false
}
}
s
.
removeClientLocked
(
oldestKey
,
oldestEntry
)
s
.
removeClientLocked
(
oldestKey
,
oldestEntry
)
return
true
}
// evictOverLimitLocked 淘汰超出数量限制的客户端(需持有锁)
// 使用 LRU 策略,优先淘汰最久未使用且无活跃请求的客户端
func
(
s
*
httpUpstreamService
)
evictOverLimitLocked
()
bool
{
maxClients
:=
s
.
maxUpstreamClients
()
if
maxClients
<=
0
{
return
false
}
evicted
:=
false
// 循环淘汰直到满足数量限制
for
len
(
s
.
clients
)
>
maxClients
{
if
!
s
.
evictOldestIdleLocked
()
{
return
evicted
}
evicted
=
true
}
}
return
evicted
}
}
// getIsolationMode 获取连接池隔离模式
// getIsolationMode 获取连接池隔离模式
...
@@ -443,7 +475,26 @@ func normalizeProxyURL(raw string) (string, *url.URL) {
...
@@ -443,7 +475,26 @@ func normalizeProxyURL(raw string) (string, *url.URL) {
if
err
!=
nil
{
if
err
!=
nil
{
return
directProxyKey
,
nil
return
directProxyKey
,
nil
}
}
return
proxyURL
,
parsed
parsed
.
Scheme
=
strings
.
ToLower
(
parsed
.
Scheme
)
parsed
.
Host
=
strings
.
ToLower
(
parsed
.
Host
)
parsed
.
Path
=
""
parsed
.
RawPath
=
""
parsed
.
RawQuery
=
""
parsed
.
Fragment
=
""
parsed
.
ForceQuery
=
false
if
hostname
:=
parsed
.
Hostname
();
hostname
!=
""
{
port
:=
parsed
.
Port
()
if
(
parsed
.
Scheme
==
"http"
&&
port
==
"80"
)
||
(
parsed
.
Scheme
==
"https"
&&
port
==
"443"
)
{
port
=
""
}
hostname
=
strings
.
ToLower
(
hostname
)
if
port
!=
""
{
parsed
.
Host
=
net
.
JoinHostPort
(
hostname
,
port
)
}
else
{
parsed
.
Host
=
hostname
}
}
return
parsed
.
String
(),
parsed
}
}
// defaultPoolSettings 获取默认连接池配置
// defaultPoolSettings 获取默认连接池配置
...
...
backend/internal/repository/http_upstream_test.go
View file @
820bb16c
...
@@ -64,6 +64,31 @@ func (s *HTTPUpstreamSuite) TestGetOrCreateClient_InvalidURLFallsBackToDirect()
...
@@ -64,6 +64,31 @@ func (s *HTTPUpstreamSuite) TestGetOrCreateClient_InvalidURLFallsBackToDirect()
require
.
Equal
(
s
.
T
(),
directProxyKey
,
entry
.
proxyKey
,
"expected direct proxy fallback"
)
require
.
Equal
(
s
.
T
(),
directProxyKey
,
entry
.
proxyKey
,
"expected direct proxy fallback"
)
}
}
// TestNormalizeProxyURL_Canonicalizes 测试代理 URL 规范化
// 验证等价地址能够映射到同一缓存键
func
(
s
*
HTTPUpstreamSuite
)
TestNormalizeProxyURL_Canonicalizes
()
{
key1
,
_
:=
normalizeProxyURL
(
"http://proxy.local:8080"
)
key2
,
_
:=
normalizeProxyURL
(
"http://proxy.local:8080/"
)
require
.
Equal
(
s
.
T
(),
key1
,
key2
,
"expected normalized proxy keys to match"
)
}
// TestAcquireClient_OverLimitReturnsError 测试连接池缓存上限保护
// 验证超限且无可淘汰条目时返回错误
func
(
s
*
HTTPUpstreamSuite
)
TestAcquireClient_OverLimitReturnsError
()
{
s
.
cfg
.
Gateway
=
config
.
GatewayConfig
{
ConnectionPoolIsolation
:
config
.
ConnectionPoolIsolationAccountProxy
,
MaxUpstreamClients
:
1
,
}
svc
:=
s
.
newService
()
entry1
,
err
:=
svc
.
acquireClient
(
"http://proxy-a:8080"
,
1
,
1
)
require
.
NoError
(
s
.
T
(),
err
,
"expected first acquire to succeed"
)
require
.
NotNil
(
s
.
T
(),
entry1
,
"expected entry"
)
entry2
,
err
:=
svc
.
acquireClient
(
"http://proxy-b:8080"
,
2
,
1
)
require
.
Error
(
s
.
T
(),
err
,
"expected error when cache limit reached"
)
require
.
Nil
(
s
.
T
(),
entry2
,
"expected nil entry when cache limit reached"
)
}
// TestDo_WithoutProxy_GoesDirect 测试无代理时直连
// TestDo_WithoutProxy_GoesDirect 测试无代理时直连
// 验证空代理 URL 时请求直接发送到目标服务器
// 验证空代理 URL 时请求直接发送到目标服务器
func
(
s
*
HTTPUpstreamSuite
)
TestDo_WithoutProxy_GoesDirect
()
{
func
(
s
*
HTTPUpstreamSuite
)
TestDo_WithoutProxy_GoesDirect
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment