perf(queue): 限制大媒体任务内存并消除准入惊群
将同步与异步非文本任务的准入唤醒改为按任务和全局队首推进,批量续租等待者,避免大量请求同时争抢 advisory lock 和数据库连接。 对 Base64 请求解析、素材解码、上游媒体执行和结果物化增加分层并发限制,复用请求素材并释放重复 Gemini wire 数据;生产 API 默认入口解析并发 16,媒体物化并发 8。 新增 1000 个同步 Gemini 图像编辑请求的模拟上游压力验收,10 MiB 输入和输出下全部成功,Heap 峰值增长约 2.58 GiB,并验证共享上传哈希、单次 attempt 与本地零落盘。 验证:go test ./... -count=1;go vet ./...;gofmt -l 无输出;kubectl kustomize deploy/kubernetes/production;10 MiB Gemini Base64 千任务压力测试通过。
This commit is contained in:
@@ -9,9 +9,12 @@ import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
@@ -41,6 +44,11 @@ type requestAssetOptions struct {
|
||||
Source string
|
||||
}
|
||||
|
||||
type requestAssetLock struct {
|
||||
mu sync.Mutex
|
||||
refs int
|
||||
}
|
||||
|
||||
func (s *Server) prepareTaskRequest(ctx context.Context, r *http.Request, user *auth.User, body map[string]any) (preparedTaskRequest, error) {
|
||||
preparedBody, err := s.prepareRequestAssetRefs(ctx, body)
|
||||
if err != nil {
|
||||
@@ -120,13 +128,9 @@ func (s *Server) prepareRequestAssetValue(ctx context.Context, value any, path [
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
item := typed[key]
|
||||
if decoded, ok, err := requestAssetFromValue(key, path, item, typed); err != nil {
|
||||
if ref, ok, err := s.prepareRequestAssetField(ctx, key, path, item, typed); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
ref, err := s.ensureRequestAsset(ctx, decoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next[key] = requestAssetWrapper(ref)
|
||||
continue
|
||||
}
|
||||
@@ -152,6 +156,67 @@ func (s *Server) prepareRequestAssetValue(ctx context.Context, value any, path [
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) prepareRequestAssetField(ctx context.Context, key string, path []string, value any, siblings map[string]any) (map[string]any, bool, error) {
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
raw := strings.TrimSpace(text)
|
||||
if raw == "" || mediaURLString(raw) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(raw), "data:") &&
|
||||
!strictRequestBase64Field(key, path) &&
|
||||
!likelyRequestBase64MediaField(key, path, raw) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err := s.acquireMediaRequestSlot(ctx); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer s.releaseMediaRequestSlot()
|
||||
decoded, ok, err := requestAssetFromValue(key, path, value, siblings)
|
||||
if err != nil || !ok {
|
||||
return nil, ok, err
|
||||
}
|
||||
ref, err := s.ensureRequestAsset(ctx, decoded)
|
||||
return ref, true, err
|
||||
}
|
||||
|
||||
func (s *Server) acquireMediaRequestSlot(ctx context.Context) error {
|
||||
if s.mediaRequestSlots == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case s.mediaRequestSlots <- struct{}{}:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) releaseMediaRequestSlot() {
|
||||
if s.mediaRequestSlots != nil {
|
||||
<-s.mediaRequestSlots
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) acquireMediaRequestBodySlot(ctx context.Context) (func(), error) {
|
||||
if s.mediaRequestBodySlots == nil {
|
||||
return func() {}, nil
|
||||
}
|
||||
select {
|
||||
case s.mediaRequestBodySlots <- struct{}{}:
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
<-s.mediaRequestBodySlots
|
||||
})
|
||||
}, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func requestAssetFromValue(key string, path []string, value any, siblings map[string]any) (decodedRequestAsset, bool, error) {
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
@@ -215,6 +280,8 @@ func (s *Server) ensureRequestAssetWithOptions(ctx context.Context, decoded deco
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
release := s.acquireRequestAssetLock(sha + "\x00" + contentType + "\x00" + options.UploadScene + "\x00" + strconv.FormatBool(options.RequirePublicURL))
|
||||
defer release()
|
||||
now := time.Now()
|
||||
if existing, ok, err := s.store.FindRequestAsset(ctx, sha, contentType); err != nil && !store.IsUndefinedDatabaseObject(err) {
|
||||
return nil, err
|
||||
@@ -286,6 +353,31 @@ func (s *Server) ensureRequestAssetWithOptions(ctx context.Context, decoded deco
|
||||
return requestAssetRef(asset), nil
|
||||
}
|
||||
|
||||
func (s *Server) acquireRequestAssetLock(key string) func() {
|
||||
s.requestAssetLocksMu.Lock()
|
||||
if s.requestAssetLocks == nil {
|
||||
s.requestAssetLocks = map[string]*requestAssetLock{}
|
||||
}
|
||||
entry := s.requestAssetLocks[key]
|
||||
if entry == nil {
|
||||
entry = &requestAssetLock{}
|
||||
s.requestAssetLocks[key] = entry
|
||||
}
|
||||
entry.refs++
|
||||
s.requestAssetLocksMu.Unlock()
|
||||
|
||||
entry.mu.Lock()
|
||||
return func() {
|
||||
entry.mu.Unlock()
|
||||
s.requestAssetLocksMu.Lock()
|
||||
entry.refs--
|
||||
if entry.refs == 0 {
|
||||
delete(s.requestAssetLocks, key)
|
||||
}
|
||||
s.requestAssetLocksMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func requestConversationKey(r *http.Request, body map[string]any) string {
|
||||
if r != nil {
|
||||
if value := strings.TrimSpace(r.Header.Get("X-EasyAI-Conversation-ID")); value != "" {
|
||||
@@ -397,6 +489,16 @@ func requestAssetStillUsable(asset store.RequestAsset, now time.Time) bool {
|
||||
if asset.ExpiresAt != nil && !asset.ExpiresAt.After(now) {
|
||||
return false
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(asset.StorageProvider), "local_static") {
|
||||
localPath := strings.TrimSpace(asset.LocalPath)
|
||||
info, err := os.Stat(localPath)
|
||||
if localPath == "" || err != nil || !info.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
if asset.ByteSize > 0 && info.Size() != asset.ByteSize {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(asset.URL) != ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user