Files
kami_gateway/internal/utils/proxy_pool.go
danial 863dc33ba3 feat(orderpool): 优化订单提交流程及代理获取逻辑
- 为 SubmitOrder 添加重试机制,增强订单创建、绑定和处理的鲁棒性
- 提供订单创建失败和处理失败时的资源清理方法,避免资源泄漏
- 统一订单处理各阶段的日志记录,增加失败场景的上下文信息
- 调整 Nuclear 任务中随机ID生成逻辑,使用 Pipeline 批量写 Redis 降低压力
- 发送请求时增加访问异常处理,避免无代理情况下报错
- 为各 channel 接口添加获取代理失败的容错处理,防止服务中断
- proxy_pool 中代理可用性检测新增独立超时,提升检测稳定性
- 优化代理过期清理逻辑,缩短锁持有时间,避免性能瓶颈
- GetProxy 增加超时控制,异步获取防止阻塞调用线程
- scan_controller 和 service 添加 gopool panic 处理,防止任务异常崩溃
- Nuclear.go 中添加锁机制保证随机ID生成线程安全
- 减少 submitPool 线程池数量,优化资源使用
- 统一并增强日志和追踪,导入 runtime/debug 用于堆栈信息打印
2025-12-14 21:24:02 +08:00

555 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package utils
import (
"context"
"errors"
"fmt"
"github.com/dubonzi/otelresty"
"github.com/go-resty/resty/v2"
"maps"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"time"
"gateway/internal/cache"
"gateway/internal/config"
"gateway/internal/otelTrace"
"github.com/duke-git/lancet/v2/slice"
"github.com/duke-git/lancet/v2/strutil"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
// ProxyStrategy 代理策略接口
type ProxyStrategy interface {
GetProxy(ctx context.Context, proxyRequest ProxyRequest) (string, error)
GetHTTPClient(ctx context.Context, proxyRequest ProxyRequest) (*http.Client, error)
}
// ProxyInfo 代理信息
type ProxyInfo struct {
proxy string
expireAt time.Time
authKey string
authPwd string
channel string // 添加通道字段
orderID string
lastUsedAt time.Time // 添加最后使用时间字段
usageCount int // 添加使用次数字段
}
// ProxyRequest 代理请求信息
type ProxyRequest struct {
OrderNo string
Channel string // 添加通道字段
OrderPerIP int
}
// DefaultProxyStrategy 默认代理策略
type DefaultProxyStrategy struct {
proxyURL string
authKey string
authPwd string
mu sync.RWMutex
current *ProxyInfo
}
// OrderBasedProxyStrategy 基于订单号和通道的代理策略
type OrderBasedProxyStrategy struct {
Id string
authKey string
proxyURL string
authPwd string
OrderPerIP int
mu sync.RWMutex
totalProxies []*ProxyInfo
proxies map[string]*ProxyInfo // key: channel_orderID
stopCh chan struct{} // 用于停止清理协程
}
// NewDefaultProxyStrategy 创建默认代理策略
func NewDefaultProxyStrategy(proxyURL, authKey, authPwd string) *DefaultProxyStrategy {
return &DefaultProxyStrategy{
proxyURL: proxyURL,
authKey: authKey,
authPwd: authPwd,
}
}
// NewOrderBasedProxyStrategy 创建基于订单号和通道的代理策略
func NewOrderBasedProxyStrategy(proxyURL, authKey, authPwd string) *OrderBasedProxyStrategy {
strategy := &OrderBasedProxyStrategy{
proxyURL: proxyURL,
authKey: authKey,
authPwd: authPwd,
totalProxies: make([]*ProxyInfo, 0),
proxies: make(map[string]*ProxyInfo),
stopCh: make(chan struct{}),
}
// 启动清理协程
go strategy.startCleanupRoutine()
return strategy
}
// GetProxy 获取代理(默认策略)
func (p *DefaultProxyStrategy) GetProxy(ctx context.Context, _ ProxyRequest) (string, error) {
proxy, err := p.innerGetProxy(ctx, ProxyRequest{})
if err != nil {
return "", err
}
return fmt.Sprintf("http://%s:%s@%s", p.authKey, p.authPwd, proxy), nil
}
// GetProxy 获取代理(默认策略)
func (p *DefaultProxyStrategy) innerGetProxy(ctx context.Context, _ ProxyRequest) (string, error) {
p.mu.RLock()
if p.current != nil && time.Now().Before(p.current.expireAt) {
if err := p.checkProxyAvailable(ctx, p.current.proxy); err == nil {
proxy := p.current.proxy
p.mu.RUnlock()
otelTrace.Logger.WithContext(ctx).Info("使用缓存代理", zap.String("proxy", proxy))
return proxy, nil
}
}
p.mu.RUnlock()
return p.getNewProxy(ctx)
}
// GetProxy 获取代理(基于订单号和通道策略)
func (p *OrderBasedProxyStrategy) GetProxy(ctx context.Context, proxyRequest ProxyRequest) (string, error) {
proxy, err := p.getInnerProxy(ctx, proxyRequest)
if err != nil {
return "", err
}
return fmt.Sprintf("socks5://%s:%s@%s", p.authKey, p.authPwd, proxy), nil
}
// GetProxy 获取代理(基于订单号和通道策略)
func (p *OrderBasedProxyStrategy) getInnerProxy(ctx context.Context, proxyRequest ProxyRequest) (string, error) {
if proxyRequest.OrderPerIP == 0 {
proxyRequest.OrderPerIP = 1
}
p.mu.RLock()
// 生成代理缓存键channel_orderID
cacheKey := fmt.Sprintf("%s:%s", proxyRequest.Channel, proxyRequest.OrderNo)
if proxy, exists := p.proxies[cacheKey]; exists {
p.mu.RUnlock()
if err := p.checkProxyAvailable(ctx, proxy.proxy); err == nil {
// 更新最后使用时间和使用次数
p.mu.Lock()
proxy.lastUsedAt = time.Now()
proxy.usageCount++
p.mu.Unlock()
otelTrace.Logger.WithContext(ctx).Info("使用订单缓存代理",
zap.String("proxy", proxy.proxy),
zap.String("orderID", proxyRequest.OrderNo),
zap.String("channel", proxyRequest.Channel),
zap.Int("usageCount", proxy.usageCount))
return proxy.proxy, nil
}
} else {
p.mu.RUnlock()
}
return p.getNewProxy(ctx, proxyRequest)
}
// getNewProxy 获取新代理(默认策略)
func (p *DefaultProxyStrategy) getNewProxy(ctx context.Context) (string, error) {
var lastErr error
for i := range 3 {
proxy, err := p.tryGetProxy(ctx)
if err == nil {
p.mu.Lock()
p.current = &ProxyInfo{
proxy: proxy,
expireAt: time.Now().Add(50 * time.Second),
authKey: p.authKey,
authPwd: p.authPwd,
}
p.mu.Unlock()
otelTrace.Logger.WithContext(ctx).Info("获取新代理", zap.String("proxy", proxy))
return proxy, nil
}
lastErr = err
otelTrace.Logger.WithContext(ctx).Warn("获取代理失败,准备重试",
zap.Int("重试次数", i+1),
zap.Error(err))
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(time.Second * time.Duration(i+1)):
continue
}
}
return "", fmt.Errorf("获取代理失败已重试3次: %v", lastErr)
}
// 查找使用次数少于指定次数的 ip跨通道查找
func (p *OrderBasedProxyStrategy) getUnderLimitedProxy(ctx context.Context, perIP int, excludeChannels string) (string, error) {
p.mu.RLock()
//需要找到指定通道所有的代理
counts := map[*ProxyInfo]int{}
for channel, info := range p.proxies {
if strings.Contains(channel, excludeChannels) && info.usageCount > perIP {
counts[info]++
}
}
proxies := slice.Filter(p.totalProxies, func(index int, item *ProxyInfo) bool {
for info := range counts {
if info.proxy == item.proxy {
return false
}
}
return true
})
p.mu.RUnlock()
for _, proxy := range proxies {
if err := p.checkProxyAvailable(ctx, proxy.proxy); err == nil {
return proxy.proxy, nil
} else {
// 清理不可用的代理
func() {
p.mu.Lock()
defer p.mu.Unlock()
maps.DeleteFunc(p.proxies, func(key string, value *ProxyInfo) bool {
return value.proxy == proxy.proxy
})
p.totalProxies = slices.DeleteFunc(p.totalProxies, func(info *ProxyInfo) bool {
return info.proxy == proxy.proxy
})
otelTrace.Logger.WithContext(ctx).Warn("代理IP不可用", zap.String("proxy", proxy.proxy), zap.Error(err))
}()
}
}
return "", fmt.Errorf("没有找到使用次数少于指定次数的 ip")
}
// getNewProxy 获取新代理(基于订单号和通道策略)
func (p *OrderBasedProxyStrategy) getNewProxy(ctx context.Context, proxyRequest ProxyRequest) (string, error) {
var lastErr error
// 如果订单 ip设置使用次数大于1则获取使用次数少于指定次数的 ip
if proxyRequest.OrderPerIP >= 1 {
// 排除当前通道,允许跨通道复用代理
excludeChannels := proxyRequest.Channel
proxy, err := p.getUnderLimitedProxy(ctx, proxyRequest.OrderPerIP, excludeChannels)
if err == nil {
cacheKey := fmt.Sprintf("%s:%s", proxyRequest.Channel, proxyRequest.OrderNo)
p.mu.Lock()
p.proxies[cacheKey] = &ProxyInfo{
proxy: proxy,
expireAt: time.Now().Add(50 * time.Second),
authKey: p.authKey,
authPwd: p.authPwd,
channel: proxyRequest.Channel,
orderID: proxyRequest.OrderNo,
lastUsedAt: time.Now(),
usageCount: 1,
}
p.mu.Unlock()
return proxy, nil
}
}
tmpProxies := make([]*ProxyInfo, 0)
var err error
for i := range 3 {
proxies, err2 := p.tryGetProxy(ctx)
if err2 != nil {
otelTrace.Logger.WithContext(ctx).Warn("获取代理失败,准备重试",
zap.Int("retryCount", i+1),
zap.Error(err2))
time.Sleep(time.Second * 2)
lastErr = err2
continue
}
for _, proxy := range proxies {
p.mu.Lock()
proxyInfo := &ProxyInfo{
proxy: proxy,
expireAt: time.Now().Add(50 * time.Second),
authKey: p.authKey,
authPwd: p.authPwd,
channel: proxyRequest.Channel,
orderID: proxyRequest.OrderNo,
lastUsedAt: time.Now(),
usageCount: 1,
}
tmpProxies = append(tmpProxies, proxyInfo)
p.mu.Unlock()
otelTrace.Logger.WithContext(ctx).Info("获取新代理",
zap.String("proxy", proxy),
zap.String("orderID", proxyRequest.OrderNo),
zap.String("channel", proxyRequest.Channel))
}
lastErr = err
break
}
p.totalProxies = append(p.totalProxies, tmpProxies...)
for _, proxy := range tmpProxies {
if err2 := p.checkProxyAvailable(ctx, proxy.proxy); err2 == nil {
cacheKey := fmt.Sprintf("%s:%s", proxyRequest.Channel, proxyRequest.OrderNo)
p.proxies[cacheKey] = proxy
return proxy.proxy, nil
}
}
return "", fmt.Errorf("获取代理失败已重试2次: %v", lastErr)
}
// GetHTTPClient 获取HTTP客户端默认策略
func (p *DefaultProxyStrategy) GetHTTPClient(ctx context.Context, _ ProxyRequest) (*http.Client, error) {
proxy, err := p.GetProxy(ctx, ProxyRequest{})
if err != nil {
return nil, err
}
return p.createHTTPClient(proxy)
}
// GetHTTPClient 获取HTTP客户端基于订单号和通道策略
func (p *OrderBasedProxyStrategy) GetHTTPClient(ctx context.Context, proxyRequest ProxyRequest) (*http.Client, error) {
proxy, err := p.GetProxy(ctx, proxyRequest)
if err != nil {
return nil, err
}
return p.createHTTPClient(proxy)
}
// createHTTPClient 创建HTTP客户端
func (p *DefaultProxyStrategy) createHTTPClient(proxy string) (*http.Client, error) {
proxyURL, err := url.Parse(proxy)
if err != nil {
return nil, fmt.Errorf("解析代理URL失败: %v", err)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}, nil
}
// createHTTPClient 创建HTTP客户端
func (p *OrderBasedProxyStrategy) createHTTPClient(proxy string) (*http.Client, error) {
proxyURL, err := url.Parse(proxy)
if err != nil {
return nil, fmt.Errorf("解析代理URL失败: %v", err)
}
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 2 * time.Second,
}, nil
}
// tryGetProxy 尝试获取代理IP
func (p *DefaultProxyStrategy) tryGetProxy(ctx context.Context) (string, error) {
client := resty.New()
otelresty.TraceClient(client)
proxyURL, err := client.R().SetContext(ctx).Get(p.proxyURL)
if err != nil {
return "", fmt.Errorf("请求代理服务失败: %v", err)
}
return proxyURL.String(), nil
}
// tryGetProxy 尝试获取代理IP
func (p *OrderBasedProxyStrategy) tryGetProxy(ctx context.Context) ([]string, error) {
client := resty.New()
otelresty.TraceClient(client)
proxyURL, err := client.R().SetContext(ctx).Get(p.proxyURL)
if err != nil {
return []string{}, fmt.Errorf("请求代理服务失败: %v", err)
}
proxyIPs := strutil.SplitAndTrim(proxyURL.String(), "\n")
slice.ForEach(proxyIPs, func(index int, item string) {
proxyIPs[index] = strutil.Trim(item)
})
return proxyIPs, nil
}
// checkProxyAvailable 检查代理IP是否可用
func (p *DefaultProxyStrategy) checkProxyAvailable(ctx context.Context, proxyIP string) error {
client := resty.New()
otelresty.TraceClient(client)
client.SetProxy(fmt.Sprintf("socks5://%s:%s@%s", p.authKey, p.authPwd, proxyIP))
response, err := client.R().SetContext(ctx).Get("https://www.qq.com")
if err != nil {
return fmt.Errorf("代理连接测试失败: %v", err)
}
if response.IsSuccess() {
return nil
}
return errors.New("代理响应状态码异常")
}
// checkProxyAvailable 检查代理IP是否可用
func (p *OrderBasedProxyStrategy) checkProxyAvailable(ctx context.Context, proxyIP string) error {
// 为代理检测设置独立的短超时,避免阻塞其他操作
ctxWithTimeout, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
client := resty.New()
otelresty.TraceClient(client)
client.SetTimeout(3 * time.Second) // 设置客户端超时
client.SetProxy(fmt.Sprintf("socks5://%s:%s@%s", p.authKey, p.authPwd, proxyIP))
response, err := client.R().SetContext(ctxWithTimeout).Get("https://www.qq.com")
if err != nil {
return fmt.Errorf("代理连接测试失败: %v", err)
}
if response.IsSuccess() {
return nil
}
return errors.New("代理响应状态码异常")
}
// startCleanupRoutine 启动清理协程
func (p *OrderBasedProxyStrategy) startCleanupRoutine() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
p.cleanupUnusedProxies()
case <-p.stopCh:
return
}
}
}
// cleanupUnusedProxies 清理未使用的代理
func (p *OrderBasedProxyStrategy) cleanupUnusedProxies() {
now := time.Now()
// 分两阶段清理,减少锁持有时间
// 第一阶段:收集需要删除的键(只持有读锁)
var keysToDelete []string
p.mu.RLock()
for key, info := range p.proxies {
if info.expireAt.Before(now) {
keysToDelete = append(keysToDelete, key)
}
}
p.mu.RUnlock()
// 第二阶段:快速删除(只短暂持有写锁)
if len(keysToDelete) > 0 {
p.mu.Lock()
for _, key := range keysToDelete {
delete(p.proxies, key)
}
// 同时清理 totalProxies 中的过期代理
p.totalProxies = slices.DeleteFunc(p.totalProxies, func(info *ProxyInfo) bool {
return info.expireAt.Before(now)
})
p.mu.Unlock()
}
}
// Stop 停止清理协程
func (p *OrderBasedProxyStrategy) Stop() {
close(p.stopCh)
}
var OrderBasedProxyStrategyInstance *OrderBasedProxyStrategy
var DMProxyStrategyInstance *DMProxyStrategy
func StartProxyPool() {
proxyConfig := config.GetProxyInfo()
//// 初始化订单代理策略
OrderBasedProxyStrategyInstance = NewOrderBasedProxyStrategy(
proxyConfig.Url,
proxyConfig.AuthKey,
proxyConfig.AuthPwd,
)
// 初始化大漠代理策略
DMProxyStrategyInstance = NewDMProxyStrategy(
"http://need1.dmdaili.com:7771/dmgetip.asp?apikey=42bab0ac&pwd=1b567c2f286a08e391b5805565fa0882&getnum=20&httptype=1&geshi=&fenge=1&fengefu=&operate=all",
cache.GetRedisClient().Client,
)
}
func GetProxy(ctx context.Context, orderId string, channel string) (string, error) {
ctx, span := otelTrace.Span(ctx, "GetProxy", "GetProxy", trace.WithAttributes(
attribute.String("orderId", orderId),
attribute.String("channel", channel),
))
defer span.End()
// 为代理获取添加超时机制,防止在 OnBeforeRequest 中长时间阻塞
ctxWithTimeout, cancel := context.WithTimeout(ctx, 8*time.Second)
defer cancel()
done := make(chan struct {
proxy string
err error
}, 1)
go func() {
proxyConfig := config.GetProxy()
var proxy string
var err error
if proxyConfig == "dm" {
proxy, err = DMProxyStrategyInstance.GetProxy(ctxWithTimeout, ProxyRequest{
OrderNo: orderId,
Channel: channel,
OrderPerIP: 1,
})
} else {
proxy, err = OrderBasedProxyStrategyInstance.GetProxy(ctxWithTimeout, ProxyRequest{
OrderNo: orderId,
Channel: channel,
OrderPerIP: 1,
})
}
select {
case done <- struct {
proxy string
err error
}{proxy, err}:
case <-ctxWithTimeout.Done():
}
}()
select {
case result := <-done:
return result.proxy, result.err
case <-ctxWithTimeout.Done():
otelTrace.Logger.WithContext(ctx).Warn("获取代理超时",
zap.String("orderId", orderId),
zap.String("channel", channel),
zap.Error(ctxWithTimeout.Err()))
return "", fmt.Errorf("获取代理超时: %v", ctxWithTimeout.Err())
}
}