feat: add check_tolerance to make node selection stable

This commit is contained in:
mzz2017
2023-02-09 21:16:51 +08:00
parent e3a71d0ee2
commit 158dfb2a27
6 changed files with 33 additions and 19 deletions

View File

@ -26,6 +26,7 @@ type AliveDialerSet struct {
dialerGroupName string dialerGroupName string
l4proto consts.L4ProtoStr l4proto consts.L4ProtoStr
ipversion consts.IpVersionStr ipversion consts.IpVersionStr
tolerance time.Duration
aliveChangeCallback func(alive bool) aliveChangeCallback func(alive bool)
@ -43,6 +44,7 @@ func NewAliveDialerSet(
dialerGroupName string, dialerGroupName string,
l4proto consts.L4ProtoStr, l4proto consts.L4ProtoStr,
ipversion consts.IpVersionStr, ipversion consts.IpVersionStr,
tolerance time.Duration,
selectionPolicy consts.DialerSelectionPolicy, selectionPolicy consts.DialerSelectionPolicy,
dialers []*Dialer, dialers []*Dialer,
aliveChangeCallback func(alive bool), aliveChangeCallback func(alive bool),
@ -53,6 +55,7 @@ func NewAliveDialerSet(
dialerGroupName: dialerGroupName, dialerGroupName: dialerGroupName,
l4proto: l4proto, l4proto: l4proto,
ipversion: ipversion, ipversion: ipversion,
tolerance: tolerance,
aliveChangeCallback: aliveChangeCallback, aliveChangeCallback: aliveChangeCallback,
dialerToIndex: make(map[*Dialer]int), dialerToIndex: make(map[*Dialer]int),
dialerToLatency: make(map[*Dialer]time.Duration), dialerToLatency: make(map[*Dialer]time.Duration),
@ -146,14 +149,19 @@ func (a *AliveDialerSet) NotifyLatencyChange(dialer *Dialer, alive bool) {
bakOldBestDialer := a.minLatency.dialer bakOldBestDialer := a.minLatency.dialer
// Calc minLatency. // Calc minLatency.
a.dialerToLatency[dialer] = latency a.dialerToLatency[dialer] = latency
if alive && latency < a.minLatency.latency { if alive && latency <= a.minLatency.latency-a.tolerance {
a.minLatency.latency = latency a.minLatency.latency = latency
a.minLatency.dialer = dialer a.minLatency.dialer = dialer
} else if a.minLatency.dialer == dialer { } else if a.minLatency.dialer == dialer {
if latency > a.minLatency.latency {
// Latency increases.
a.minLatency.latency = time.Hour a.minLatency.latency = time.Hour
a.minLatency.dialer = nil a.minLatency.dialer = nil
a.calcMinLatency() a.calcMinLatency()
// Now `a.minLatency.dialer` will be nil if there is no alive dialer. // Now `a.minLatency.dialer` will be nil if there is no alive dialer.
} else {
a.minLatency.latency = latency
}
} }
currentAlive := a.minLatency.dialer != nil currentAlive := a.minLatency.dialer != nil
// If best dialer changed. // If best dialer changed.
@ -169,7 +177,8 @@ func (a *AliveDialerSet) NotifyLatencyChange(dialer *Dialer, alive bool) {
string(a.selectionPolicy): a.minLatency.latency, string(a.selectionPolicy): a.minLatency.latency,
"group": a.dialerGroupName, "group": a.dialerGroupName,
"network": string(a.l4proto) + string(a.ipversion), "network": string(a.l4proto) + string(a.ipversion),
"dialer": a.minLatency.dialer.Name(), "new dialer": a.minLatency.dialer.Name(),
"old dialer": bakOldBestDialer.Name(),
}).Infof("Group %vselects dialer", re) }).Infof("Group %vselects dialer", re)
} else { } else {
// Alive -> not alive // Alive -> not alive
@ -199,7 +208,7 @@ func (a *AliveDialerSet) calcMinLatency() {
if !ok { if !ok {
continue continue
} }
if latency < a.minLatency.latency { if latency <= a.minLatency.latency-a.tolerance {
a.minLatency.latency = latency a.minLatency.latency = latency
a.minLatency.dialer = d a.minLatency.dialer = d
} }

View File

@ -37,6 +37,7 @@ type GlobalOption struct {
TcpCheckOptionRaw TcpCheckOptionRaw // Lazy parse TcpCheckOptionRaw TcpCheckOptionRaw // Lazy parse
UdpCheckOptionRaw UdpCheckOptionRaw // Lazy parse UdpCheckOptionRaw UdpCheckOptionRaw // Lazy parse
CheckInterval time.Duration CheckInterval time.Duration
CheckTolerance time.Duration
} }
type InstanceOption struct { type InstanceOption struct {

View File

@ -39,10 +39,10 @@ type DialerGroup struct {
func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.Dialer, p DialerSelectionPolicy, aliveChangeCallback func(alive bool, l4proto uint8, ipversion uint8)) *DialerGroup { func NewDialerGroup(option *dialer.GlobalOption, name string, dialers []*dialer.Dialer, p DialerSelectionPolicy, aliveChangeCallback func(alive bool, l4proto uint8, ipversion uint8)) *DialerGroup {
log := option.Log log := option.Log
var registeredAliveDialerSet bool var registeredAliveDialerSet bool
aliveTcp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_4, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 4) }, true) aliveTcp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_4, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 4) }, true)
aliveTcp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_6, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 6) }, true) aliveTcp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_TCP, consts.IpVersionStr_6, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_TCP, 6) }, true)
aliveUdp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_4, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 4) }, true) aliveUdp4DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_4, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 4) }, true)
aliveUdp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_6, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 6) }, true) aliveUdp6DialerSet := dialer.NewAliveDialerSet(log, name, consts.L4ProtoStr_UDP, consts.IpVersionStr_6, option.CheckTolerance, p.Policy, dialers, func(alive bool) { aliveChangeCallback(alive, unix.IPPROTO_UDP, 6) }, true)
switch p.Policy { switch p.Policy {
case consts.DialerSelectionPolicy_Random, case consts.DialerSelectionPolicy_Random,

View File

@ -19,6 +19,7 @@ type Global struct {
TcpCheckUrl string `mapstructure:"tcp_check_url" default:"http://cp.cloudflare.com"` TcpCheckUrl string `mapstructure:"tcp_check_url" default:"http://cp.cloudflare.com"`
UdpCheckDns string `mapstructure:"udp_check_dns" default:"cloudflare-dns.com:53"` UdpCheckDns string `mapstructure:"udp_check_dns" default:"cloudflare-dns.com:53"`
CheckInterval time.Duration `mapstructure:"check_interval" default:"30s"` CheckInterval time.Duration `mapstructure:"check_interval" default:"30s"`
CheckTolerance time.Duration `mapstructure:"check_tolerance" default:"0"`
DnsUpstream common.UrlOrEmpty `mapstructure:"dns_upstream" require:""` DnsUpstream common.UrlOrEmpty `mapstructure:"dns_upstream" require:""`
LanInterface []string `mapstructure:"lan_interface"` LanInterface []string `mapstructure:"lan_interface"`
WanInterface []string `mapstructure:"wan_interface"` WanInterface []string `mapstructure:"wan_interface"`

View File

@ -196,6 +196,7 @@ func NewControlPlane(
TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{Raw: global.TcpCheckUrl}, TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{Raw: global.TcpCheckUrl},
UdpCheckOptionRaw: dialer.UdpCheckOptionRaw{Raw: global.UdpCheckDns}, UdpCheckOptionRaw: dialer.UdpCheckOptionRaw{Raw: global.UdpCheckDns},
CheckInterval: global.CheckInterval, CheckInterval: global.CheckInterval,
CheckTolerance: global.CheckTolerance,
} }
outbounds := []*outbound.DialerGroup{ outbounds := []*outbound.DialerGroup{
outbound.NewDialerGroup(option, consts.OutboundDirect.String(), outbound.NewDialerGroup(option, consts.OutboundDirect.String(),

View File

@ -10,6 +10,8 @@ global {
tcp_check_url: 'http://cp.cloudflare.com' tcp_check_url: 'http://cp.cloudflare.com'
udp_check_dns: 'dns.google:53' udp_check_dns: 'dns.google:53'
check_interval: 30s check_interval: 30s
# Group will switch node only when new_latency <= old_latency - tolerance
check_tolerance: 50ms
# Value can be scheme://host:port or empty string ''. # Value can be scheme://host:port or empty string ''.
# The scheme can be tcp/udp/tcp+udp. Empty string '' indicates as-is. # The scheme can be tcp/udp/tcp+udp. Empty string '' indicates as-is.