feat(orderpool): 优化订单提交流程及代理获取逻辑

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

View File

@@ -9,7 +9,10 @@
"Bash(for:*)", "Bash(for:*)",
"Bash(do echo:*)", "Bash(do echo:*)",
"Bash(grep:*)", "Bash(grep:*)",
"Bash(done)" "Bash(done)",
"Bash(lsof:*)",
"Bash(go tool pprof:*)",
"Bash(curl:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -22,6 +22,7 @@ import (
"gateway/internal/service/supplier/third_party" "gateway/internal/service/supplier/third_party"
"gateway/internal/utils" "gateway/internal/utils"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"runtime/debug"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -39,9 +40,30 @@ import (
var ( var (
delayPool = gopool.NewPool("delayHandler", 50, gopool.NewConfig()) delayPool = gopool.NewPool("delayHandler", 50, gopool.NewConfig())
submitLimiterPool = gopool.NewPool("submitLimiterPool", 50, gopool.NewConfig()) submitLimiterPool = gopool.NewPool("submitLimiterPool", 50, gopool.NewConfig())
submitPool = gopool.NewPool("submitPool", 500, gopool.NewConfig()) submitPool = gopool.NewPool("submitPool", 100, gopool.NewConfig())
) )
func init() {
// 为 submitPool 设置 panic handler防止 OpenTelemetry panic 导致任务中断
submitPool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("SubmitPool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
delayPool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("DelayPool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
submitLimiterPool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("SubmitLimiterPool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
}
var orderSubmitLimiter sync.Map var orderSubmitLimiter sync.Map
func isAllowed(orderNo string, intervalSec int64) bool { func isAllowed(orderNo string, intervalSec int64) bool {

View File

@@ -19,6 +19,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/dubonzi/otelresty" "github.com/dubonzi/otelresty"
@@ -33,9 +34,12 @@ import (
type SendCardTaskTypeNuclear struct { type SendCardTaskTypeNuclear struct {
sendCardTaskTypeSendCardTaskBase sendCardTaskTypeSendCardTaskBase
mu sync.Mutex
} }
func (s *SendCardTaskTypeNuclear) getRandomId(ctx context.Context) (string, string) { func (s *SendCardTaskTypeNuclear) getRandomId(ctx context.Context) (string, string) {
s.mu.Lock()
defer s.mu.Unlock()
redisClient := cache.GetRedisClient() redisClient := cache.GetRedisClient()
// 检查是否已经有足够的ID在Redis中 // 检查是否已经有足够的ID在Redis中
@@ -44,16 +48,33 @@ func (s *SendCardTaskTypeNuclear) getRandomId(ctx context.Context) (string, stri
otelTrace.Logger.WithContext(ctx).Error("Redis keys error", zap.Error(err)) otelTrace.Logger.WithContext(ctx).Error("Redis keys error", zap.Error(err))
} }
if len(keys) < 20000 { if len(keys) < 2000 {
// 生成2w个随机数 // 批量生成2k个随机数分批处理以减少Redis压力
for i := 0; i < 20000; i++ { batchSize := 100
totalToGenerate := 2000
for batch := 0; batch < totalToGenerate/batchSize; batch++ {
// 使用Pipeline批量设置
pipe := redisClient.Client.Pipeline()
for i := 0; i < batchSize; i++ {
nuclearRandomId := utils.GetMd5Lower(utils.GenerateId()) nuclearRandomId := utils.GetMd5Lower(utils.GenerateId())
// 生成浏览器指纹哈希
fingerprintHash := fingerprint.GenerateRandomBrowserFingerprintHash() fingerprintHash := fingerprint.GenerateRandomBrowserFingerprintHash()
err = redisClient.Set(ctx, "nuclear_random_ids:"+nuclearRandomId, fingerprintHash, time.Hour*time.Duration(rand.Uint64N(24)+1)) ttl := time.Hour * time.Duration(rand.Uint64N(24)+1)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("Failed to set nuclear ID", zap.Error(err)) pipe.Set(ctx, "nuclear_random_ids:"+nuclearRandomId, fingerprintHash, ttl)
} }
// 执行批量操作
_, err := pipe.Exec(ctx)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("Failed to batch set nuclear IDs",
zap.Error(err),
zap.Int("batch", batch+1))
}
// 短暂延迟避免给Redis造成过大压力
time.Sleep(5 * time.Millisecond)
} }
// 重新获取keys // 重新获取keys
keys, err = redisClient.Client.Keys(ctx, "nuclear_random_ids:*").Result() keys, err = redisClient.Client.Keys(ctx, "nuclear_random_ids:*").Result()
@@ -189,7 +210,7 @@ func (s *SendCardTaskTypeNuclear) CreateOrder(ctx context.Context, roadUid strin
func (s *SendCardTaskTypeNuclear) channelOne(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error { func (s *SendCardTaskTypeNuclear) channelOne(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
ctx, span := otelTrace.Span(ctx, "SendCardTaskTypeNuclear", "SendCardTaskTypeNuclear.channelOne") ctx, span := otelTrace.Span(ctx, "SendCardTaskTypeNuclear", "SendCardTaskTypeNuclear.channelOne")
defer span.End() defer otelTrace.SafeEndSpan(span)
queryOrderInfo, err2 := url.Parse(orderItem.PayURL) queryOrderInfo, err2 := url.Parse(orderItem.PayURL)
if err2 != nil { if err2 != nil {
@@ -231,13 +252,18 @@ func (s *SendCardTaskTypeNuclear) channelOne(ctx context.Context, orderItem Orde
proxy, err3 := utils.GetProxy(ctx, needChangeProxyId, SendCardTaskTypeEnumNuclear.String()) proxy, err3 := utils.GetProxy(ctx, needChangeProxyId, SendCardTaskTypeEnumNuclear.String())
if err3 != nil { if err3 != nil {
otelTrace.Logger.WithContext(ctx).Error("获取代理失败", zap.Error(err3)) otelTrace.Logger.WithContext(ctx).Error("获取代理失败", zap.Error(err3))
return err3 return nil
} }
c.SetProxy(proxy) c.SetProxy(proxy)
return nil return nil
}) })
otelresty.TraceClient(webClient) otelresty.TraceClient(webClient)
_, _ = webClient.R().Get(orderItem.PayURL)
// 检查是否能成功访问,如果失败则继续下一次重试
if _, err = webClient.R().Get(orderItem.PayURL); err != nil {
otelTrace.Logger.WithContext(ctx).Warn("访问支付链接失败", zap.Error(err))
continue
}
//添加计数器 //添加计数器
for range 5 { for range 5 {
@@ -351,18 +377,24 @@ func (s *SendCardTaskTypeNuclear) channelOne(ctx context.Context, orderItem Orde
} }
func (s *SendCardTaskTypeNuclear) channelTwo(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error { func (s *SendCardTaskTypeNuclear) channelTwo(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
// 先尝试获取代理
proxy, err := utils.GetProxy(ctx, utils.GenerateId(), string(SendCardTaskTypeEnumNuclear+"two"))
if err != nil {
otelTrace.Logger.WithContext(ctx).Warn("获取代理失败,将不使用代理", zap.Error(err))
}
webClient := resty.New().SetTimeout(10 * time.Second).SetHeaders(map[string]string{ webClient := resty.New().SetTimeout(10 * time.Second).SetHeaders(map[string]string{
"origin": "http://yosyt.hy32.top", "origin": "http://yosyt.hy32.top",
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone), "user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone),
"referer": orderItem.PayURL, "referer": orderItem.PayURL,
"x-requested-with": "XMLHttpRequest", "x-requested-with": "XMLHttpRequest",
}).OnBeforeRequest(func(client *resty.Client, request *resty.Request) error {
proxy, _ := utils.GetProxy(ctx, utils.GenerateId(), string(SendCardTaskTypeEnumNuclear+"two"))
if proxy != "" {
client.SetProxy(proxy)
}
return nil
}) })
// 如果获取到了代理,则使用代理
if proxy != "" {
webClient.SetProxy(proxy)
}
otelresty.TraceClient(webClient) otelresty.TraceClient(webClient)
schemas := strings.Split(orderItem.PayURL, "/") schemas := strings.Split(orderItem.PayURL, "/")
orderId := schemas[len(schemas)-1] orderId := schemas[len(schemas)-1]
@@ -395,18 +427,24 @@ func (s *SendCardTaskTypeNuclear) channelTwo(ctx context.Context, orderItem Orde
} }
func (s *SendCardTaskTypeNuclear) channelThree(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error { func (s *SendCardTaskTypeNuclear) channelThree(ctx context.Context, orderItem OrderPoolItem, task SendCardTask) error {
// 先尝试获取代理
proxy, err := utils.GetProxy(ctx, utils.GenerateId(), string(SendCardTaskTypeEnumNuclear+"three"))
if err != nil {
otelTrace.Logger.WithContext(ctx).Warn("获取代理失败,将不使用代理", zap.Error(err))
}
webClient := resty.New().SetTimeout(10 * time.Second).SetHeaders(map[string]string{ webClient := resty.New().SetTimeout(10 * time.Second).SetHeaders(map[string]string{
"origin": "http://223.4.250.106:22568", "origin": "http://223.4.250.106:22568",
"user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone), "user-agent": useragent.GetUserAgentByPlatform(useragent.PlatformPhone),
"referer": orderItem.PayURL, "referer": orderItem.PayURL,
"x-requested-with": "XMLHttpRequest", "x-requested-with": "XMLHttpRequest",
}).OnBeforeRequest(func(client *resty.Client, request *resty.Request) error {
proxy, _ := utils.GetProxy(ctx, utils.GenerateId(), string(SendCardTaskTypeEnumNuclear+"three"))
if proxy != "" {
client.SetProxy(proxy)
}
return nil
}) })
// 如果获取到了代理,则使用代理
if proxy != "" {
webClient.SetProxy(proxy)
}
otelresty.TraceClient(webClient) otelresty.TraceClient(webClient)
queryUrl, err := url.Parse(orderItem.PayURL) queryUrl, err := url.Parse(orderItem.PayURL)
if err != nil { if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/duke-git/lancet/v2/convertor" "github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/pointer" "github.com/duke-git/lancet/v2/pointer"
"runtime/debug"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -69,6 +70,19 @@ func NewOrderPoolService(config *Config, redisClient *cache.RedisClient) OrderPo
servicePool: gopool.NewPool(utils.GenerateId(), 30, gopool.NewConfig()), servicePool: gopool.NewPool(utils.GenerateId(), 30, gopool.NewConfig()),
} }
// 设置 panic handler
service.taskPool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("OrderPool taskPool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
service.servicePool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("OrderPool servicePool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
// 注册事件处理器 // 注册事件处理器
service.registerEventHandlers(context.Background()) service.registerEventHandlers(context.Background())
@@ -557,54 +571,129 @@ func (s *OrderPoolServiceImpl) unboundPoolOrderId(ctx context.Context, taskEnum
func (s *OrderPoolServiceImpl) SubmitOrder(ctx context.Context, task card_sender.SendCardTask) error { func (s *OrderPoolServiceImpl) SubmitOrder(ctx context.Context, task card_sender.SendCardTask) error {
ctx, span := otelTrace.Span(ctx, "SubmitOrder", "OrderPoolServiceImpl.SubmitOrder", ctx, span := otelTrace.Span(ctx, "SubmitOrder", "OrderPoolServiceImpl.SubmitOrder",
trace.WithAttributes(attribute.String("task", convertor.ToString(task))), trace.WithAttributes(
attribute.String("task", convertor.ToString(task)),
attribute.String("localOrderId", task.LocalOrderID),
),
) )
defer span.End() defer span.End()
var err error var err error
var orderItem card_sender.OrderPoolItem var orderItem card_sender.OrderPoolItem
maxRetries := 3
for range 3 { for attempt := 0; attempt < maxRetries; attempt++ {
span.AddEvent("attempt_start", trace.WithAttributes(attribute.Int("attempt", attempt+1)))
// 清空上一次的orderItem避免残留数据影响
orderItem = card_sender.OrderPoolItem{}
// 1. 创建订单
span.AddEvent("create order") span.AddEvent("create order")
orderItem, err = task.SendCardTaskType.GetSendCardTaskType(). orderItem, err = task.SendCardTaskType.GetSendCardTaskType().
CreateOrder(ctx, task.RoadUid, task.CardInfo.GetFaceTypeFloat(ctx)) CreateOrder(ctx, task.RoadUid, task.CardInfo.GetFaceTypeFloat(ctx))
if err != nil { if err != nil {
span.AddEvent("create order failed") span.AddEvent("create order failed")
otelTrace.Logger.WithContext(ctx).Error("创建订单失败",
zap.Error(err),
zap.Int("attempt", attempt+1),
zap.String("localOrderId", task.LocalOrderID))
return fmt.Errorf("创建订单失败: %v", err) return fmt.Errorf("创建订单失败: %v", err)
} }
// 验证订单创建成功
if orderItem.OrderID == "" {
span.AddEvent("invalid order created")
return fmt.Errorf("创建的订单ID为空")
}
span.SetAttributes(attribute.String("orderId", orderItem.OrderID))
// 2. 绑定订单到数据库
span.AddEvent("bind order") span.AddEvent("bind order")
if err = task.SendCardTaskType.GetSendCardTaskType().BindPoolOrderId(ctx, orderItem, task); err != nil { if err = task.SendCardTaskType.GetSendCardTaskType().BindPoolOrderId(ctx, orderItem, task); err != nil {
otelTrace.Logger.WithContext(ctx).Error("绑定订单ID和卡片信息ID失败", zap.Error(err)) span.AddEvent("bind_order_failed")
otelTrace.Logger.WithContext(ctx).Error("绑定订单ID和卡片信息ID失败",
zap.Error(err),
zap.String("orderId", orderItem.OrderID),
zap.Int("attempt", attempt+1))
// 清理已创建的订单资源
cleanupErr := s.cleanupCreatedOrder(ctx, task, orderItem, "bind_failed")
if cleanupErr != nil {
otelTrace.Logger.WithContext(ctx).Error("清理创建的订单资源失败",
zap.Error(cleanupErr),
zap.String("orderId", orderItem.OrderID))
}
return fmt.Errorf("绑定订单ID和卡片信息ID失败: %v", err) return fmt.Errorf("绑定订单ID和卡片信息ID失败: %v", err)
} }
// 绑定订单ID和卡片信息ID // 3. 绑定到Redis
bindKey := fmt.Sprintf("%s:%s", s.config.OrderBindKey, orderItem.OrderID) bindKey := fmt.Sprintf("%s:%s", s.config.OrderBindKey, orderItem.OrderID)
if err = s.redisClient.Set(ctx, bindKey, task.LocalOrderID, s.config.OrderBindKeyActiveTime); err != nil { if err = s.redisClient.Set(ctx, bindKey, task.LocalOrderID, s.config.OrderBindKeyActiveTime); err != nil {
span.AddEvent("绑定订单ID和卡片信息ID失败") span.AddEvent("redis_bind_failed")
return fmt.Errorf("绑定订单ID和卡片信息ID失败: %v", err) otelTrace.Logger.WithContext(ctx).Error("Redis绑定订单失败",
zap.Error(err),
zap.String("orderId", orderItem.OrderID),
zap.String("bindKey", bindKey),
zap.Int("attempt", attempt+1))
// 回滚数据库绑定操作
cleanupErr := s.unboundPoolOrderId(ctx, task.SendCardTaskType, orderItem, task)
if cleanupErr != nil {
otelTrace.Logger.WithContext(ctx).Error("回滚数据库绑定失败",
zap.Error(cleanupErr),
zap.String("orderId", orderItem.OrderID))
} }
return fmt.Errorf("Redis绑定订单ID和卡片信息ID失败: %v", err)
}
// 4. 处理订单
span.AddEvent("handle order") span.AddEvent("handle order")
err = task.SendCardTaskType.GetSendCardTaskType().HandleSendCardTask(ctx, orderItem, task) err = task.SendCardTaskType.GetSendCardTaskType().HandleSendCardTask(ctx, orderItem, task)
if err != nil { if err != nil {
span.AddEvent("handle order failed") span.AddEvent("handle_order_failed")
if strings.Contains(err.Error(), "重新下单") { otelTrace.Logger.WithContext(ctx).Warn("订单处理失败",
_ = s.unboundPoolOrderId(ctx, task.SendCardTaskType, orderItem, task) zap.Error(err),
zap.String("orderId", orderItem.OrderID),
zap.String("remoteOrderId", orderItem.RemoteOrderID),
zap.Int("attempt", attempt+1))
// 清理失败订单的所有资源(无论什么错误都清理,避免资源泄漏)
cleanupErr := s.cleanupFailedOrder(ctx, task, orderItem, "handle_failed")
if cleanupErr != nil {
otelTrace.Logger.WithContext(ctx).Error("清理失败订单资源时出错",
zap.Error(cleanupErr),
zap.String("orderId", orderItem.OrderID))
}
// 只有标记为"重新下单"的错误才重试
if strings.Contains(err.Error(), "重新下单") && attempt < maxRetries-1 {
otelTrace.Logger.WithContext(ctx).Info("订单需要重新下单,准备重试",
zap.String("orderId", orderItem.OrderID),
zap.Int("attempt", attempt+1))
continue continue
} }
return fmt.Errorf("提交订单失败: %v", err) return fmt.Errorf("提交订单失败: %v", err)
} }
// 成功处理,退出重试循环
span.AddEvent("order_success")
otelTrace.Logger.WithContext(ctx).Info("订单提交成功",
zap.String("orderId", orderItem.OrderID),
zap.String("remoteOrderId", orderItem.RemoteOrderID),
zap.Int("finalAttempt", attempt+1))
break break
} }
if err != nil { // 最终状态检查
return fmt.Errorf("提交订单失败: %v", err)
}
if pointer.IsNil(orderItem) || orderItem.OrderID == "" { if pointer.IsNil(orderItem) || orderItem.OrderID == "" {
return fmt.Errorf("订单创建失败") span.AddEvent("final_validation_failed")
return fmt.Errorf("订单处理失败")
} }
// 5. 处理查询需求
if task.NeedQuery { if task.NeedQuery {
queryEvent := &OrderQueryEvent{ queryEvent := &OrderQueryEvent{
BaseEvent: BaseEvent{ BaseEvent: BaseEvent{
@@ -620,8 +709,32 @@ func (s *OrderPoolServiceImpl) SubmitOrder(ctx context.Context, task card_sender
RemoteOrderID: orderItem.RemoteOrderID, RemoteOrderID: orderItem.RemoteOrderID,
} }
if err := s.eventBus.Publish(ctx, queryEvent); err != nil { if err := s.eventBus.Publish(ctx, queryEvent); err != nil {
otelTrace.Logger.WithContext(ctx).Error("发布订单查询事件失败", zap.Error(err)) otelTrace.Logger.WithContext(ctx).Error("发布订单查询事件失败",
zap.Error(err),
zap.String("orderId", orderItem.OrderID))
} }
} }
return nil return nil
} }
// cleanupCreatedOrder 清理刚创建但绑定失败的订单
func (s *OrderPoolServiceImpl) cleanupCreatedOrder(ctx context.Context, task card_sender.SendCardTask, orderItem card_sender.OrderPoolItem, reason string) error {
otelTrace.Logger.WithContext(ctx).Info("清理创建的订单",
zap.String("orderId", orderItem.OrderID),
zap.String("reason", reason))
// 清理数据库绑定
_ = task.SendCardTaskType.GetSendCardTaskType().UnBindPoolOrderId(ctx, orderItem, task)
return nil
}
// cleanupFailedOrder 清理处理失败的订单所有资源
func (s *OrderPoolServiceImpl) cleanupFailedOrder(ctx context.Context, task card_sender.SendCardTask, orderItem card_sender.OrderPoolItem, reason string) error {
otelTrace.Logger.WithContext(ctx).Info("清理失败的订单",
zap.String("orderId", orderItem.OrderID),
zap.String("reason", reason))
// 使用现有的清理方法
return s.unboundPoolOrderId(ctx, task.SendCardTaskType, orderItem, task)
}

View File

@@ -407,10 +407,16 @@ func (p *DefaultProxyStrategy) checkProxyAvailable(ctx context.Context, proxyIP
// checkProxyAvailable 检查代理IP是否可用 // checkProxyAvailable 检查代理IP是否可用
func (p *OrderBasedProxyStrategy) checkProxyAvailable(ctx context.Context, proxyIP string) error { func (p *OrderBasedProxyStrategy) checkProxyAvailable(ctx context.Context, proxyIP string) error {
// 为代理检测设置独立的短超时,避免阻塞其他操作
ctxWithTimeout, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
client := resty.New() client := resty.New()
otelresty.TraceClient(client) otelresty.TraceClient(client)
client.SetTimeout(3 * time.Second) // 设置客户端超时
client.SetProxy(fmt.Sprintf("socks5://%s:%s@%s", p.authKey, p.authPwd, proxyIP)) client.SetProxy(fmt.Sprintf("socks5://%s:%s@%s", p.authKey, p.authPwd, proxyIP))
response, err := client.R().SetContext(ctx).Get("https://www.qq.com")
response, err := client.R().SetContext(ctxWithTimeout).Get("https://www.qq.com")
if err != nil { if err != nil {
return fmt.Errorf("代理连接测试失败: %v", err) return fmt.Errorf("代理连接测试失败: %v", err)
} }
@@ -437,17 +443,33 @@ func (p *OrderBasedProxyStrategy) startCleanupRoutine() {
// cleanupUnusedProxies 清理未使用的代理 // cleanupUnusedProxies 清理未使用的代理
func (p *OrderBasedProxyStrategy) cleanupUnusedProxies() { func (p *OrderBasedProxyStrategy) cleanupUnusedProxies() {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now() now := time.Now()
maps.DeleteFunc(p.proxies, func(s string, info *ProxyInfo) bool {
return info.expireAt.Before(now)
})
slices.DeleteFunc(p.totalProxies, func(info *ProxyInfo) bool { // 分两阶段清理,减少锁持有时间
// 第一阶段:收集需要删除的键(只持有读锁)
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) return info.expireAt.Before(now)
}) })
p.mu.Unlock()
}
} }
// Stop 停止清理协程 // Stop 停止清理协程
@@ -481,17 +503,52 @@ func GetProxy(ctx context.Context, orderId string, channel string) (string, erro
attribute.String("channel", channel), attribute.String("channel", channel),
)) ))
defer span.End() 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() proxyConfig := config.GetProxy()
var proxy string
var err error
if proxyConfig == "dm" { if proxyConfig == "dm" {
return DMProxyStrategyInstance.GetProxy(ctx, ProxyRequest{ proxy, err = DMProxyStrategyInstance.GetProxy(ctxWithTimeout, ProxyRequest{
OrderNo: orderId,
Channel: channel,
OrderPerIP: 1,
})
} else {
proxy, err = OrderBasedProxyStrategyInstance.GetProxy(ctxWithTimeout, ProxyRequest{
OrderNo: orderId, OrderNo: orderId,
Channel: channel, Channel: channel,
OrderPerIP: 1, OrderPerIP: 1,
}) })
} }
return OrderBasedProxyStrategyInstance.GetProxy(ctx, ProxyRequest{
OrderNo: orderId, select {
Channel: channel, case done <- struct {
OrderPerIP: 1, 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())
}
} }