From 7e152e4279347ea30bde2022979afd40ce85f768 Mon Sep 17 00:00:00 2001 From: Brisbanehuang Date: Sat, 12 Sep 2026 09:03:52 -0400 Subject: [PATCH] =?UTF-8?q?fix(scheduler):=20=E4=BF=AE=E5=A4=8D=E9=80=89?= =?UTF-8?q?=E5=8F=B7=E8=80=97=E6=97=B6=E8=BF=94=E5=9B=9E=E5=80=BC=E5=92=8C?= =?UTF-8?q?=E7=B2=98=E6=80=A7=E5=91=BD=E4=B8=AD=E7=8E=87=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E8=AE=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用命名返回值传回 defer 中记录的选号耗时;按每次选号累计任意粘性命中,保留两类独立计数。增加成功、失败及双粘性回归,不改变调度策略。 --- .../service/openai_account_scheduler.go | 11 ++- .../openai_account_scheduler_metrics_test.go | 73 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 backend/internal/service/openai_account_scheduler_metrics_test.go diff --git a/backend/internal/service/openai_account_scheduler.go b/backend/internal/service/openai_account_scheduler.go index 5ad63d30f8..4a1dd2a9e8 100644 --- a/backend/internal/service/openai_account_scheduler.go +++ b/backend/internal/service/openai_account_scheduler.go @@ -131,6 +131,7 @@ type openAIAccountSchedulerMetrics struct { selectTotal atomic.Int64 stickyPreviousHitTotal atomic.Int64 stickySessionHitTotal atomic.Int64 + stickyHitTotal atomic.Int64 loadBalanceSelectTotal atomic.Int64 accountSwitchTotal atomic.Int64 latencyMsTotal atomic.Int64 @@ -172,6 +173,9 @@ func (m *openAIAccountSchedulerMetrics) recordSelect(decision OpenAIAccountSched if decision.StickySessionHit { m.stickySessionHitTotal.Add(1) } + if decision.StickyPreviousHit || decision.StickySessionHit { + m.stickyHitTotal.Add(1) + } if decision.Layer == openAIAccountScheduleLayerLoadBalance { m.loadBalanceSelectTotal.Add(1) } @@ -378,12 +382,12 @@ func newDefaultOpenAIAccountScheduler(service *OpenAIGatewayService, stats *open func (s *defaultOpenAIAccountScheduler) Select( ctx context.Context, req OpenAIAccountScheduleRequest, -) (*AccountSelectionResult, OpenAIAccountScheduleDecision, error) { +) (selection *AccountSelectionResult, decision OpenAIAccountScheduleDecision, err error) { if s != nil && s.service != nil && s.service.openAIGroupRequiresPrivacySet(ctx, req.GroupID) { req.RequirePrivacySet = true } - decision := OpenAIAccountScheduleDecision{} start := time.Now() + // 命名返回值保证 defer 写入的耗时同时返回给调用方。 defer func() { decision.LatencyMs = time.Since(start).Milliseconds() s.metrics.recordSelect(decision) @@ -1844,6 +1848,7 @@ func (s *defaultOpenAIAccountScheduler) SnapshotMetrics() OpenAIAccountScheduler selectTotal := s.metrics.selectTotal.Load() prevHit := s.metrics.stickyPreviousHitTotal.Load() sessionHit := s.metrics.stickySessionHitTotal.Load() + stickyHit := s.metrics.stickyHitTotal.Load() switchTotal := s.metrics.accountSwitchTotal.Load() latencyTotal := s.metrics.latencyMsTotal.Load() loadSkewTotal := s.metrics.loadSkewMilliTotal.Load() @@ -1859,7 +1864,7 @@ func (s *defaultOpenAIAccountScheduler) SnapshotMetrics() OpenAIAccountScheduler } if selectTotal > 0 { snapshot.SchedulerLatencyMsAvg = float64(latencyTotal) / float64(selectTotal) - snapshot.StickyHitRatio = float64(prevHit+sessionHit) / float64(selectTotal) + snapshot.StickyHitRatio = float64(stickyHit) / float64(selectTotal) snapshot.AccountSwitchRate = float64(switchTotal) / float64(selectTotal) snapshot.LoadSkewAvg = float64(loadSkewTotal) / 1000 / float64(selectTotal) } diff --git a/backend/internal/service/openai_account_scheduler_metrics_test.go b/backend/internal/service/openai_account_scheduler_metrics_test.go new file mode 100644 index 0000000000..c4fc7ef98d --- /dev/null +++ b/backend/internal/service/openai_account_scheduler_metrics_test.go @@ -0,0 +1,73 @@ +package service + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type schedulerLatencyAccountRepo struct{ schedulerTestOpenAIAccountRepo } + +func (r schedulerLatencyAccountRepo) ListSchedulableByPlatform(ctx context.Context, platform string) ([]Account, error) { + time.Sleep(20 * time.Millisecond) + return r.schedulerTestOpenAIAccountRepo.ListSchedulableByPlatform(ctx, platform) +} + +func (r schedulerLatencyAccountRepo) ListSchedulableUngroupedByPlatform(ctx context.Context, platform string) ([]Account, error) { + return r.ListSchedulableByPlatform(ctx, platform) +} + +func TestOpenAISchedulerSelectReturnsRealLatency(t *testing.T) { + resetOpenAIAdvancedSchedulerSettingCacheForTest() + t.Cleanup(resetOpenAIAdvancedSchedulerSettingCacheForTest) + for _, withAccount := range []bool{false, true} { + name := "no_available_account" + var accounts []Account + if withAccount { + name = "selected_account" + accounts = []Account{{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 5}} + } + t.Run(name, func(t *testing.T) { + scheduler := &defaultOpenAIAccountScheduler{ + service: &OpenAIGatewayService{accountRepo: schedulerLatencyAccountRepo{schedulerTestOpenAIAccountRepo{accounts: accounts}}}, + stats: newOpenAIAccountRuntimeStats(), + } + selection, decision, err := scheduler.Select(context.Background(), OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}) + if withAccount { + require.NoError(t, err) + require.NotNil(t, selection) + require.Equal(t, int64(1), decision.SelectedAccountID) + if selection.ReleaseFunc != nil { + t.Cleanup(selection.ReleaseFunc) + } + } else { + require.Error(t, err) + } + require.GreaterOrEqual(t, decision.LatencyMs, int64(20)) + metrics := scheduler.SnapshotMetrics() + require.Equal(t, decision.LatencyMs, metrics.SchedulerLatencyMsTotal) + require.Equal(t, float64(decision.LatencyMs), metrics.SchedulerLatencyMsAvg) + }) + } +} + +func TestOpenAISchedulerStickyHitRatioCountsSelectionOnce(t *testing.T) { + scheduler := &defaultOpenAIAccountScheduler{stats: newOpenAIAccountRuntimeStats()} + scheduler.metrics.recordSelect(OpenAIAccountScheduleDecision{StickyPreviousHit: true, StickySessionHit: true}) + snapshot := scheduler.SnapshotMetrics() + require.Equal(t, int64(1), snapshot.SelectTotal) + require.Equal(t, int64(1), snapshot.StickyPreviousHitTotal) + require.Equal(t, int64(1), snapshot.StickySessionHitTotal) + require.Equal(t, 1.0, snapshot.StickyHitRatio) + + scheduler.metrics.recordSelect(OpenAIAccountScheduleDecision{StickyPreviousHit: true}) + scheduler.metrics.recordSelect(OpenAIAccountScheduleDecision{StickySessionHit: true}) + scheduler.metrics.recordSelect(OpenAIAccountScheduleDecision{}) + snapshot = scheduler.SnapshotMetrics() + require.Equal(t, int64(4), snapshot.SelectTotal) + require.Equal(t, int64(2), snapshot.StickyPreviousHitTotal) + require.Equal(t, int64(2), snapshot.StickySessionHitTotal) + require.Equal(t, 0.75, snapshot.StickyHitRatio) +}