Files
easyai-ai-gateway/apps/api/internal/runner/passive_route_observer.go
T
wangbo 7786692d32 feat(routing): 引入多执行池智能调度
将 Worker 发现、路由画像、容量与执行传输抽象为平台无关接口,新增 Kubernetes 和静态容量适配器,并以 shadow 模式接入生产配置。

实现网络与容量评分、路由防抖、池队列、同步 Worker 租约、一次性执行令牌,以及提交状态不明时禁止重复分配的安全语义。

新增 0105 兼容迁移、管理接口、指标、OpenAPI 和回归测试。已执行全量 Go 测试、go vet、OpenAPI、迁移安全、Compose 与 Kustomize 验证。
2026-08-05 22:25:37 +08:00

138 lines
4.1 KiB
Go

package runner
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptrace"
"sort"
"sync"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/executionpool"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type passiveRouteObserver struct {
mu sync.Mutex
samples int
successes int
connectTLS []time.Duration
uploadBytesRate []float64
}
type passiveRouteRoundTripper struct {
base http.RoundTripper
observer *passiveRouteObserver
}
func (observer *passiveRouteObserver) wrap(client *http.Client) *http.Client {
if client == nil {
client = http.DefaultClient
}
cloned := *client
base := cloned.Transport
if base == nil {
base = http.DefaultTransport
}
cloned.Transport = passiveRouteRoundTripper{base: base, observer: observer}
return &cloned
}
func (transport passiveRouteRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
startedAt := time.Now()
var connectStartedAt time.Time
var connectDuration time.Duration
var tlsStartedAt time.Time
var tlsDuration time.Duration
var wroteRequestAt time.Time
trace := &httptrace.ClientTrace{
ConnectStart: func(_, _ string) { connectStartedAt = time.Now() },
ConnectDone: func(_, _ string, _ error) {
if !connectStartedAt.IsZero() {
connectDuration = time.Since(connectStartedAt)
}
},
TLSHandshakeStart: func() { tlsStartedAt = time.Now() },
TLSHandshakeDone: func(tls.ConnectionState, error) {
if !tlsStartedAt.IsZero() {
tlsDuration = time.Since(tlsStartedAt)
}
},
WroteRequest: func(httptrace.WroteRequestInfo) { wroteRequestAt = time.Now() },
}
traced := request.Clone(httptrace.WithClientTrace(request.Context(), trace))
response, err := transport.base.RoundTrip(traced)
transport.observer.record(
err == nil,
connectDuration+tlsDuration,
request.ContentLength,
startedAt,
wroteRequestAt,
)
return response, err
}
func (observer *passiveRouteObserver) record(success bool, connectTLS time.Duration, contentLength int64, startedAt, wroteRequestAt time.Time) {
observer.mu.Lock()
defer observer.mu.Unlock()
observer.samples++
if success {
observer.successes++
}
if connectTLS > 0 {
observer.connectTLS = append(observer.connectTLS, connectTLS)
}
if contentLength > 0 && !wroteRequestAt.IsZero() && wroteRequestAt.After(startedAt) {
observer.uploadBytesRate = append(observer.uploadBytesRate, float64(contentLength)/wroteRequestAt.Sub(startedAt).Seconds())
}
}
func (observer *passiveRouteObserver) snapshot(poolID, routeProfileKey string) executionpool.RouteObservation {
observer.mu.Lock()
defer observer.mu.Unlock()
return executionpool.RouteObservation{
PoolID: poolID, RouteProfileKey: routeProfileKey,
SampleCount: observer.samples, SuccessCount: observer.successes,
ConnectTLSP95: durationP95(observer.connectTLS),
UploadBytesPerSecond: floatP95(observer.uploadBytesRate),
}
}
func durationP95(values []time.Duration) time.Duration {
if len(values) == 0 {
return 0
}
copyOfValues := append([]time.Duration(nil), values...)
sort.Slice(copyOfValues, func(i, j int) bool { return copyOfValues[i] < copyOfValues[j] })
return copyOfValues[(len(copyOfValues)*95+99)/100-1]
}
func floatP95(values []float64) float64 {
if len(values) == 0 {
return 0
}
copyOfValues := append([]float64(nil), values...)
sort.Float64s(copyOfValues)
return copyOfValues[(len(copyOfValues)*95+99)/100-1]
}
func (s *Service) recordPassiveRouteObservation(task store.GatewayTask, observer *passiveRouteObserver) {
if observer == nil || !s.routingEnabled() || task.RouteProfileKey == "" {
return
}
poolID := task.AssignedPoolID
if poolID == "" && s.cfg.RunsAsyncExecutionWorker() {
poolID = s.cfg.ExecutionPoolID
}
observation := observer.snapshot(poolID, task.RouteProfileKey)
if observation.PoolID == "" || observation.SampleCount == 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
if err := s.coordinationStore.RecordRouteObservation(ctx, observation); err != nil && s.logger != nil {
s.logger.Warn("record passive route observation failed", "error_category", "route_observation_failed")
}
}