mirror of
https://github.com/XTLS/Xray-core.git
synced 2024-09-12 05:46:25 +00:00
* 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>
144 lines
3.2 KiB
Go
144 lines
3.2 KiB
Go
package burst
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// HealthPingStats is the statistics of HealthPingRTTS
|
|
type HealthPingStats struct {
|
|
All int
|
|
Fail int
|
|
Deviation time.Duration
|
|
Average time.Duration
|
|
Max time.Duration
|
|
Min time.Duration
|
|
}
|
|
|
|
// HealthPingRTTS holds ping rtts for health Checker
|
|
type HealthPingRTTS struct {
|
|
idx int
|
|
cap int
|
|
validity time.Duration
|
|
rtts []*pingRTT
|
|
|
|
lastUpdateAt time.Time
|
|
stats *HealthPingStats
|
|
}
|
|
|
|
type pingRTT struct {
|
|
time time.Time
|
|
value time.Duration
|
|
}
|
|
|
|
// NewHealthPingResult returns a *HealthPingResult with specified capacity
|
|
func NewHealthPingResult(cap int, validity time.Duration) *HealthPingRTTS {
|
|
return &HealthPingRTTS{cap: cap, validity: validity}
|
|
}
|
|
|
|
// Get gets statistics of the HealthPingRTTS
|
|
func (h *HealthPingRTTS) Get() *HealthPingStats {
|
|
return h.getStatistics()
|
|
}
|
|
|
|
// GetWithCache get statistics and write cache for next call
|
|
// Make sure use Mutex.Lock() before calling it, RWMutex.RLock()
|
|
// is not an option since it writes cache
|
|
func (h *HealthPingRTTS) GetWithCache() *HealthPingStats {
|
|
lastPutAt := h.rtts[h.idx].time
|
|
now := time.Now()
|
|
if h.stats == nil || h.lastUpdateAt.Before(lastPutAt) || h.findOutdated(now) >= 0 {
|
|
h.stats = h.getStatistics()
|
|
h.lastUpdateAt = now
|
|
}
|
|
return h.stats
|
|
}
|
|
|
|
// Put puts a new rtt to the HealthPingResult
|
|
func (h *HealthPingRTTS) Put(d time.Duration) {
|
|
if h.rtts == nil {
|
|
h.rtts = make([]*pingRTT, h.cap)
|
|
for i := 0; i < h.cap; i++ {
|
|
h.rtts[i] = &pingRTT{}
|
|
}
|
|
h.idx = -1
|
|
}
|
|
h.idx = h.calcIndex(1)
|
|
now := time.Now()
|
|
h.rtts[h.idx].time = now
|
|
h.rtts[h.idx].value = d
|
|
}
|
|
|
|
func (h *HealthPingRTTS) calcIndex(step int) int {
|
|
idx := h.idx
|
|
idx += step
|
|
if idx >= h.cap {
|
|
idx %= h.cap
|
|
}
|
|
return idx
|
|
}
|
|
|
|
func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
|
|
stats := &HealthPingStats{}
|
|
stats.Fail = 0
|
|
stats.Max = 0
|
|
stats.Min = rttFailed
|
|
sum := time.Duration(0)
|
|
cnt := 0
|
|
validRTTs := make([]time.Duration, 0)
|
|
for _, rtt := range h.rtts {
|
|
switch {
|
|
case rtt.value == 0 || time.Since(rtt.time) > h.validity:
|
|
continue
|
|
case rtt.value == rttFailed:
|
|
stats.Fail++
|
|
continue
|
|
}
|
|
cnt++
|
|
sum += rtt.value
|
|
validRTTs = append(validRTTs, rtt.value)
|
|
if stats.Max < rtt.value {
|
|
stats.Max = rtt.value
|
|
}
|
|
if stats.Min > rtt.value {
|
|
stats.Min = rtt.value
|
|
}
|
|
}
|
|
stats.All = cnt + stats.Fail
|
|
if cnt == 0 {
|
|
stats.Min = 0
|
|
return stats
|
|
}
|
|
stats.Average = time.Duration(int(sum) / cnt)
|
|
var std float64
|
|
if cnt < 2 {
|
|
// no enough data for standard deviation, we assume it's half of the average rtt
|
|
// if we don't do this, standard deviation of 1 round tested nodes is 0, will always
|
|
// selected before 2 or more rounds tested nodes
|
|
std = float64(stats.Average / 2)
|
|
} else {
|
|
variance := float64(0)
|
|
for _, rtt := range validRTTs {
|
|
variance += math.Pow(float64(rtt-stats.Average), 2)
|
|
}
|
|
std = math.Sqrt(variance / float64(cnt))
|
|
}
|
|
stats.Deviation = time.Duration(std)
|
|
return stats
|
|
}
|
|
|
|
func (h *HealthPingRTTS) findOutdated(now time.Time) int {
|
|
for i := h.cap - 1; i < 2*h.cap; i++ {
|
|
// from oldest to latest
|
|
idx := h.calcIndex(i)
|
|
validity := h.rtts[idx].time.Add(h.validity)
|
|
if h.lastUpdateAt.After(validity) {
|
|
return idx
|
|
}
|
|
if validity.Before(now) {
|
|
return idx
|
|
}
|
|
}
|
|
return -1
|
|
}
|