mirror of
https://github.com/XTLS/Xray-core.git
synced 2024-11-18 21:13:02 +00:00
fa5d7a255b
* v5: Health Check & LeastLoad Strategy (rebased from 2c5a71490368500a982018a74a6d519c7e121816) Some changes will be necessary to integrate it into V2Ray * Update proto * parse duration conf with time.Parse() * moving health ping to observatory as a standalone component * moving health ping to observatory as a standalone component: auto generated file * add initialization for health ping * incorporate changes in router implementation * support principle target output * add v4 json support for BurstObservatory & fix balancer reference * update API command * remove cancelled API * return zero length value when observer is not found * remove duplicated targeted dispatch * adjust test with updated structure * bug fix for observer * fix strategy selector * fix strategy least load * Fix ticker usage ticker.Close does not close ticker.C * feat: Replace default Health Ping URL to HTTPS (#1991) * fix selectLeastLoad() returns wrong number of nodes (#2083) * Test: fix leastload strategy unit test * fix(router): panic caused by concurrent map read and write (#2678) * Clean up code --------- Co-authored-by: Jebbs <qjebbs@gmail.com> Co-authored-by: Shelikhoo <xiaokangwang@outlook.com> Co-authored-by: 世界 <i@sekai.icu> Co-authored-by: Bernd Eichelberger <46166740+4-FLOSS-Free-Libre-Open-Source-Software@users.noreply.github.com> Co-authored-by: 秋のかえで <autmaple@protonmail.com> Co-authored-by: Rinka <kujourinka@gmail.com>
201 lines
5.6 KiB
Go
201 lines
5.6 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/xtls/xray-core/app/observatory"
|
|
"github.com/xtls/xray-core/common"
|
|
"github.com/xtls/xray-core/common/dice"
|
|
"github.com/xtls/xray-core/core"
|
|
"github.com/xtls/xray-core/features/extension"
|
|
)
|
|
|
|
// LeastLoadStrategy represents a least load balancing strategy
|
|
type LeastLoadStrategy struct {
|
|
settings *StrategyLeastLoadConfig
|
|
costs *WeightManager
|
|
|
|
observer extension.Observatory
|
|
|
|
ctx context.Context
|
|
}
|
|
|
|
func (l *LeastLoadStrategy) GetPrincipleTarget(strings []string) []string {
|
|
var ret []string
|
|
nodes := l.pickOutbounds(strings)
|
|
for _, v := range nodes {
|
|
ret = append(ret, v.Tag)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// NewLeastLoadStrategy creates a new LeastLoadStrategy with settings
|
|
func NewLeastLoadStrategy(settings *StrategyLeastLoadConfig) *LeastLoadStrategy {
|
|
return &LeastLoadStrategy{
|
|
settings: settings,
|
|
costs: NewWeightManager(
|
|
settings.Costs, 1,
|
|
func(value, cost float64) float64 {
|
|
return value * math.Pow(cost, 0.5)
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
// node is a minimal copy of HealthCheckResult
|
|
// we don't use HealthCheckResult directly because
|
|
// it may change by health checker during routing
|
|
type node struct {
|
|
Tag string
|
|
CountAll int
|
|
CountFail int
|
|
RTTAverage time.Duration
|
|
RTTDeviation time.Duration
|
|
RTTDeviationCost time.Duration
|
|
}
|
|
|
|
func (l *LeastLoadStrategy) InjectContext(ctx context.Context) {
|
|
l.ctx = ctx
|
|
}
|
|
|
|
func (s *LeastLoadStrategy) PickOutbound(candidates []string) string {
|
|
selects := s.pickOutbounds(candidates)
|
|
count := len(selects)
|
|
if count == 0 {
|
|
// goes to fallbackTag
|
|
return ""
|
|
}
|
|
return selects[dice.Roll(count)].Tag
|
|
}
|
|
|
|
func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node {
|
|
qualified := s.getNodes(candidates, time.Duration(s.settings.MaxRTT))
|
|
selects := s.selectLeastLoad(qualified)
|
|
return selects
|
|
}
|
|
|
|
// selectLeastLoad selects nodes according to Baselines and Expected Count.
|
|
//
|
|
// The strategy always improves network response speed, not matter which mode below is configured.
|
|
// But they can still have different priorities.
|
|
//
|
|
// 1. Bandwidth priority: no Baseline + Expected Count > 0.: selects `Expected Count` of nodes.
|
|
// (one if Expected Count <= 0)
|
|
//
|
|
// 2. Bandwidth priority advanced: Baselines + Expected Count > 0.
|
|
// Select `Expected Count` amount of nodes, and also those near them according to baselines.
|
|
// In other words, it selects according to different Baselines, until one of them matches
|
|
// the Expected Count, if no Baseline matches, Expected Count applied.
|
|
//
|
|
// 3. Speed priority: Baselines + `Expected Count <= 0`.
|
|
// go through all baselines until find selects, if not, select none. Used in combination
|
|
// with 'balancer.fallbackTag', it means: selects qualified nodes or use the fallback.
|
|
func (s *LeastLoadStrategy) selectLeastLoad(nodes []*node) []*node {
|
|
if len(nodes) == 0 {
|
|
newError("least load: no qualified outbound").AtInfo().WriteToLog()
|
|
return nil
|
|
}
|
|
expected := int(s.settings.Expected)
|
|
availableCount := len(nodes)
|
|
if expected > availableCount {
|
|
return nodes
|
|
}
|
|
|
|
if expected <= 0 {
|
|
expected = 1
|
|
}
|
|
if len(s.settings.Baselines) == 0 {
|
|
return nodes[:expected]
|
|
}
|
|
|
|
count := 0
|
|
// go through all base line until find expected selects
|
|
for _, b := range s.settings.Baselines {
|
|
baseline := time.Duration(b)
|
|
for i := count; i < availableCount; i++ {
|
|
if nodes[i].RTTDeviationCost >= baseline {
|
|
break
|
|
}
|
|
count = i + 1
|
|
}
|
|
// don't continue if find expected selects
|
|
if count >= expected {
|
|
newError("applied baseline: ", baseline).AtDebug().WriteToLog()
|
|
break
|
|
}
|
|
}
|
|
if s.settings.Expected > 0 && count < expected {
|
|
count = expected
|
|
}
|
|
return nodes[:count]
|
|
}
|
|
|
|
func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) []*node {
|
|
if s.observer == nil {
|
|
common.Must(core.RequireFeatures(s.ctx, func(observatory extension.Observatory) error {
|
|
s.observer = observatory
|
|
return nil
|
|
}))
|
|
}
|
|
observeResult, err := s.observer.GetObservation(s.ctx)
|
|
if err != nil {
|
|
newError("cannot get observation").Base(err).WriteToLog()
|
|
return make([]*node, 0)
|
|
}
|
|
|
|
results := observeResult.(*observatory.ObservationResult)
|
|
|
|
outboundlist := outboundList(candidates)
|
|
|
|
var ret []*node
|
|
|
|
for _, v := range results.Status {
|
|
if v.Alive && (v.Delay < maxRTT.Milliseconds() || maxRTT == 0) && outboundlist.contains(v.OutboundTag) {
|
|
record := &node{
|
|
Tag: v.OutboundTag,
|
|
CountAll: 1,
|
|
CountFail: 1,
|
|
RTTAverage: time.Duration(v.Delay) * time.Millisecond,
|
|
RTTDeviation: time.Duration(v.Delay) * time.Millisecond,
|
|
RTTDeviationCost: time.Duration(s.costs.Apply(v.OutboundTag, float64(time.Duration(v.Delay)*time.Millisecond))),
|
|
}
|
|
|
|
if v.HealthPing != nil {
|
|
record.RTTAverage = time.Duration(v.HealthPing.Average)
|
|
record.RTTDeviation = time.Duration(v.HealthPing.Deviation)
|
|
record.RTTDeviationCost = time.Duration(s.costs.Apply(v.OutboundTag, float64(v.HealthPing.Deviation)))
|
|
record.CountAll = int(v.HealthPing.All)
|
|
record.CountFail = int(v.HealthPing.Fail)
|
|
|
|
}
|
|
ret = append(ret, record)
|
|
}
|
|
}
|
|
|
|
leastloadSort(ret)
|
|
return ret
|
|
}
|
|
|
|
func leastloadSort(nodes []*node) {
|
|
sort.Slice(nodes, func(i, j int) bool {
|
|
left := nodes[i]
|
|
right := nodes[j]
|
|
if left.RTTDeviationCost != right.RTTDeviationCost {
|
|
return left.RTTDeviationCost < right.RTTDeviationCost
|
|
}
|
|
if left.RTTAverage != right.RTTAverage {
|
|
return left.RTTAverage < right.RTTAverage
|
|
}
|
|
if left.CountFail != right.CountFail {
|
|
return left.CountFail < right.CountFail
|
|
}
|
|
if left.CountAll != right.CountAll {
|
|
return left.CountAll > right.CountAll
|
|
}
|
|
return left.Tag < right.Tag
|
|
})
|
|
}
|