feat(routing): 引入多执行池智能调度
将 Worker 发现、路由画像、容量与执行传输抽象为平台无关接口,新增 Kubernetes 和静态容量适配器,并以 shadow 模式接入生产配置。 实现网络与容量评分、路由防抖、池队列、同步 Worker 租约、一次性执行令牌,以及提交状态不明时禁止重复分配的安全语义。 新增 0105 兼容迁移、管理接口、指标、OpenAPI 和回归测试。已执行全量 Go 测试、go vet、OpenAPI、迁移安全、Compose 与 Kustomize 验证。
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user