feat(proxy,failover,transport): Hyper 上游客户端精细计时、连续失败退避与连接泄漏修复

Proxy:
- 将上游 HTTP 客户端从 reqwest 替换为 hyper,新增 InstrumentedConnector
  实现 TCP 连接/TLS 握手级别的独立计时,上报 connection_reused 等指标
- 前端展示细粒度代理计时(连接复用、等待响应头等)

Failover:
- 引入连续失败退避机制,每 10 次失败递增退避间隔
- 检测 H2 max outbound streams 错误并触发上游客户端重建
- 新增 HTTPClientPool.reset_upstream_client 支持按需重建缓存客户端

连接泄漏修复:
- Handler 异常路径确保 response_ctx 被正确关闭
- HubResponseStream 迭代结束后在 finally 块中清理 stream_id
- HubTunnelTransport.handle_request 捕获所有异常并清理流状态
This commit is contained in:
fawney19
2026-03-06 17:55:14 +08:00
parent 2269617a9f
commit 050cba9563
15 changed files with 902 additions and 72 deletions

View File

@@ -566,7 +566,12 @@ const proxyTimingBreakdown = (proxy: Record<string, unknown>): string => {
}
const ttfbMs = t.ttfb_ms ?? t.upstream_ms
const processingMs = t.upstream_processing_ms ?? (
const responseWaitMs = t.response_wait_ms ?? (
t.connection_acquire_ms != null && ttfbMs != null
? Math.max(0, (ttfbMs as number) - (t.connection_acquire_ms as number))
: null
)
const legacyWaitMs = t.upstream_processing_ms ?? (
ttfbMs != null && t.connect_ms != null && t.tls_ms != null
? Math.max(0, (ttfbMs as number) - (t.connect_ms as number) - (t.tls_ms as number))
: null
@@ -575,6 +580,9 @@ const proxyTimingBreakdown = (proxy: Record<string, unknown>): string => {
if (t.dns_ms != null && (t.dns_ms as number) > 0) {
parts.push(`DNS ${formatLatency(t.dns_ms as number)}`)
}
if (t.connection_reused === true) {
parts.push('复用连接')
}
if (t.connect_ms != null && (t.connect_ms as number) > 0) {
parts.push(`连接 ${formatLatency(t.connect_ms as number)}`)
}
@@ -584,8 +592,10 @@ const proxyTimingBreakdown = (proxy: Record<string, unknown>): string => {
if (ttfbMs != null && (ttfbMs as number) > 0) {
parts.push(`TTFB ${formatLatency(ttfbMs as number)}`)
}
if (processingMs != null && (processingMs as number) > 0) {
parts.push(`上游处理 ${formatLatency(Math.round(processingMs as number))}`)
if (responseWaitMs != null && (responseWaitMs as number) > 0) {
parts.push(`等待响应头 ${formatLatency(Math.round(responseWaitMs as number))}`)
} else if (legacyWaitMs != null && (legacyWaitMs as number) > 0) {
parts.push(`等待响应头(旧版估算) ${formatLatency(Math.round(legacyWaitMs as number))}`)
}
// 计算 Aether→代理 之间无法解释的耗时差