Skip to content

Commit f3bcb8c

Browse files
committed
Add balancing optimal strategy
1 parent 698b04f commit f3bcb8c

8 files changed

Lines changed: 361 additions & 15 deletions

File tree

app/router/balancing.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import (
88
)
99

1010
type BalancingStrategy interface {
11-
PickOutbound([]string) string
11+
PickOutbound(outbound.Manager, []string) string
1212
}
1313

1414
type RandomStrategy struct {
1515
}
1616

17-
func (s *RandomStrategy) PickOutbound(tags []string) string {
17+
// PickOutbound implement BalancingStrategy interface
18+
func (s *RandomStrategy) PickOutbound(_ outbound.Manager, tags []string) string {
1819
n := len(tags)
1920
if n == 0 {
2021
panic("0 tags")
@@ -38,7 +39,7 @@ func (b *Balancer) PickOutbound() (string, error) {
3839
if len(tags) == 0 {
3940
return "", newError("no available outbounds selected")
4041
}
41-
tag := b.strategy.PickOutbound(tags)
42+
tag := b.strategy.PickOutbound(b.ohm, tags)
4243
if tag == "" {
4344
return "", newError("balancing strategy returns empty tag")
4445
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package router
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"math"
8+
"net/http"
9+
"net/url"
10+
"time"
11+
12+
"v2ray.com/core/common/net"
13+
"v2ray.com/core/common/session"
14+
"v2ray.com/core/common/task"
15+
"v2ray.com/core/features/outbound"
16+
"v2ray.com/core/transport"
17+
"v2ray.com/core/transport/pipe"
18+
)
19+
20+
// OptimalStrategy pick outbound by net speed
21+
type OptimalStrategy struct {
22+
timeout time.Duration
23+
interval time.Duration
24+
url *url.URL
25+
count uint32
26+
score float64
27+
obm outbound.Manager
28+
tag string
29+
tags []string
30+
periodic *task.Periodic
31+
}
32+
33+
// NewOptimalStrategy create new strategy
34+
func NewOptimalStrategy(config *OptimalStrategyConfig) *OptimalStrategy {
35+
s := &OptimalStrategy{}
36+
if config.Timeout == 0 {
37+
s.timeout = time.Second * 5
38+
} else {
39+
s.timeout = time.Second * time.Duration(config.Timeout)
40+
}
41+
if config.Interval == 0 {
42+
s.interval = time.Second * 60 * 10
43+
} else {
44+
s.interval = time.Second * time.Duration(config.Interval)
45+
}
46+
if config.URL == "" {
47+
s.url, _ = url.Parse("https://www.google.com")
48+
} else {
49+
var err error
50+
s.url, err = url.Parse(config.URL)
51+
if err != nil {
52+
panic(err)
53+
}
54+
if s.url.Scheme != "http" && s.url.Scheme != "https" {
55+
panic("Only http/https url support")
56+
}
57+
}
58+
if config.Count == 0 {
59+
s.count = 3
60+
} else {
61+
s.count = config.Count
62+
}
63+
s.score = 0
64+
65+
return s
66+
}
67+
68+
// PickOutbound implement BalancingStrategy interface
69+
func (s *OptimalStrategy) PickOutbound(obm outbound.Manager, tags []string) string {
70+
if len(tags) == 0 {
71+
panic("0 tags")
72+
} else if len(tags) == 1 {
73+
return s.tag
74+
}
75+
76+
s.obm = obm
77+
s.tags = tags
78+
79+
if s.periodic == nil {
80+
s.periodic = &task.Periodic{
81+
Interval: s.interval,
82+
Execute: s.run,
83+
}
84+
s.periodic.Start()
85+
s.tag = s.tags[0]
86+
return s.tag
87+
}
88+
89+
return s.tag
90+
}
91+
92+
// periodic execute function
93+
func (s *OptimalStrategy) run() error {
94+
s.score = 0
95+
96+
for _, tag := range s.tags {
97+
scores := make([]float64, 0, s.count)
98+
go s.testOutboud(tag, scores)
99+
}
100+
101+
return nil
102+
}
103+
104+
// Test outbound's network state with multi-round
105+
func (s *OptimalStrategy) testOutboud(tag string, scores []float64) {
106+
// calculate average score and end test round
107+
if len(scores) >= int(s.count) {
108+
var minScore float64 = float64(math.MaxInt64)
109+
var maxScore float64 = float64(math.MinInt64)
110+
var sumScore float64
111+
var score float64
112+
113+
for _, score := range scores {
114+
if score < minScore {
115+
minScore = score
116+
}
117+
if score > maxScore {
118+
maxScore = score
119+
}
120+
sumScore += score
121+
}
122+
if len(scores) < 3 {
123+
score = sumScore / float64(len(scores))
124+
} else {
125+
score = (sumScore - minScore - maxScore) / float64(s.count-2)
126+
}
127+
newError(fmt.Sprintf("Balance OptimalStrategy get %s's score: %.2f", tag, score)).AtDebug().WriteToLog()
128+
129+
if s.score < score {
130+
s.score = score
131+
s.tag = tag
132+
newError(fmt.Sprintf("Balance OptimalStrategy now pick detour [%s](score: %.2f) from %s", s.tag, s.score, s.tags)).AtInfo().WriteToLog()
133+
}
134+
return
135+
}
136+
// test outbound by fetch url
137+
oh := s.obm.GetHandler(tag)
138+
if oh == nil {
139+
newError("Wrong OptimalStrategy tag").AtError().WriteToLog()
140+
return
141+
}
142+
143+
client := &http.Client{
144+
Transport: &http.Transport{
145+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
146+
netDestination, err := net.ParseDestination(fmt.Sprintf("%s:%s", network, addr))
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
uplinkReader, uplinkWriter := pipe.New()
152+
downlinkReader, downlinkWriter := pipe.New()
153+
ctx = session.ContextWithOutbound(
154+
ctx,
155+
&session.Outbound{
156+
Target: netDestination,
157+
})
158+
go oh.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter})
159+
160+
return net.NewConnection(net.ConnectionInputMulti(uplinkWriter), net.ConnectionOutputMulti(downlinkReader)), nil
161+
},
162+
MaxConnsPerHost: 1,
163+
MaxIdleConns: 1,
164+
},
165+
Timeout: s.timeout,
166+
}
167+
startAt := time.Now()
168+
// send http request though this outbound
169+
req, _ := http.NewRequest("GET", s.url.String(), nil)
170+
resp, err := client.Do(req)
171+
// use http response speed or time(no http content) as score
172+
score := 0.0
173+
if err != nil {
174+
newError(err).AtError().WriteToLog()
175+
} else {
176+
contentSize := 0
177+
scanner := bufio.NewScanner(resp.Body)
178+
for scanner.Scan() {
179+
contentSize += len(scanner.Bytes())
180+
}
181+
if contentSize != 0 {
182+
score = float64(contentSize) / (float64(time.Now().UnixNano()-startAt.UnixNano()) / float64(time.Second))
183+
} else {
184+
// assert http header's Byte size is 100B
185+
score = 100 / (float64(time.Now().UnixNano()-startAt.UnixNano()) / float64(time.Second))
186+
}
187+
}
188+
// next test round
189+
resp.Body.Close()
190+
client.CloseIdleConnections()
191+
s.testOutboud(
192+
tag,
193+
append(scores, score),
194+
)
195+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package router_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"v2ray.com/core/app/proxyman/outbound"
9+
. "v2ray.com/core/app/router"
10+
"v2ray.com/core/common/buf"
11+
"v2ray.com/core/transport"
12+
)
13+
14+
// mock proxy/outbound/handler
15+
type mockHandler struct {
16+
tag string
17+
timeout time.Duration
18+
}
19+
20+
func (h *mockHandler) Tag() string {
21+
return h.tag
22+
}
23+
24+
func (h *mockHandler) Start() error {
25+
return nil
26+
}
27+
28+
func (h *mockHandler) Close() error {
29+
return nil
30+
}
31+
32+
func (h *mockHandler) Dispatch(ctx context.Context, link *transport.Link) {
33+
mockHttpResponse := `HTTP/1.1 200 OK
34+
Date: Mon, 27 Jul 2080 12:28:53 GMT
35+
Server: MockServer/0.0.1
36+
Content-Length: 53
37+
Content-Type: text/html
38+
Connection: Closed
39+
40+
<html>
41+
<body>
42+
<h1>Hello, World!</h1>
43+
</body>
44+
</html>
45+
`
46+
link.Reader.ReadMultiBuffer()
47+
if h.timeout != 0 {
48+
time.Sleep(h.timeout)
49+
}
50+
link.Writer.WriteMultiBuffer(buf.MergeBytes(buf.MultiBuffer{}, []byte(mockHttpResponse)))
51+
}
52+
53+
func TestRandomStrategy(t *testing.T) {
54+
strategy := RandomStrategy{}
55+
if strategy.PickOutbound(nil, []string{"test"}) != "test" {
56+
t.Error("Random strategy test fail")
57+
}
58+
}
59+
60+
func TestOptimalStrategy(t *testing.T) {
61+
ctx := context.Background()
62+
obm, _ := outbound.New(ctx, nil)
63+
obm.AddHandler(ctx, &mockHandler{tag: "test1", timeout: time.Millisecond * 100})
64+
obm.AddHandler(ctx, &mockHandler{tag: "test2"})
65+
strategy := NewOptimalStrategy(&OptimalStrategyConfig{URL: "http://test.com"})
66+
67+
tag := strategy.PickOutbound(obm, []string{"test1", "test2"})
68+
if tag != "test1" {
69+
t.Error("Should pick first tag on start")
70+
}
71+
// waiting outbound first round test
72+
time.Sleep(time.Second * 1)
73+
tag = strategy.PickOutbound(obm, []string{"test1", "test2"})
74+
if tag != "test2" {
75+
t.Error("Should pick fastest tag")
76+
}
77+
}

app/router/config.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,17 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
142142
}
143143

144144
func (br *BalancingRule) Build(ohm outbound.Manager) (*Balancer, error) {
145+
var strategy BalancingStrategy
146+
147+
if br.Strategy == "optimal" {
148+
strategy = NewOptimalStrategy(br.OptimalStrategyConfig)
149+
} else if br.Strategy == "" || br.Strategy == "random" {
150+
strategy = &RandomStrategy{}
151+
}
152+
145153
return &Balancer{
146154
selectors: br.OutboundSelector,
147-
strategy: &RandomStrategy{},
155+
strategy: strategy,
148156
ohm: ohm,
149157
}, nil
150158
}

app/router/config.pb.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package router
22

33
import (
44
fmt "fmt"
5-
proto "github.com/golang/protobuf/proto"
65
math "math"
6+
7+
proto "github.com/golang/protobuf/proto"
78
net "v2ray.com/core/common/net"
89
)
910

@@ -659,12 +660,25 @@ func (*RoutingRule) XXX_OneofWrappers() []interface{} {
659660
}
660661
}
661662

663+
type OptimalStrategyConfig struct {
664+
Timeout uint32 `protobuf:"varint,1,opt,name=timeout,proto3" json:"timeout,omitempty"`
665+
Interval uint32 `protobuf:"varint,2,opt,name=interval,proto3" json:"interval,omitempty"`
666+
URL string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
667+
Count uint32 `protobuf:"varint,5,opt,name=count,proto3" json:"count,omitempty"`
668+
}
669+
670+
func (m *OptimalStrategyConfig) Reset() { *m = OptimalStrategyConfig{} }
671+
func (m *OptimalStrategyConfig) String() string { return proto.CompactTextString(m) }
672+
func (*OptimalStrategyConfig) ProtoMessage() {}
673+
662674
type BalancingRule struct {
663-
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
664-
OutboundSelector []string `protobuf:"bytes,2,rep,name=outbound_selector,json=outboundSelector,proto3" json:"outbound_selector,omitempty"`
665-
XXX_NoUnkeyedLiteral struct{} `json:"-"`
666-
XXX_unrecognized []byte `json:"-"`
667-
XXX_sizecache int32 `json:"-"`
675+
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
676+
OutboundSelector []string `protobuf:"bytes,2,rep,name=outbound_selector,json=outboundSelector,proto3" json:"outbound_selector,omitempty"`
677+
Strategy string `protobuf:"bytes,3,opt,name=strategy,json=strategy,proto3" json:"strategy,omitempty"`
678+
OptimalStrategyConfig *OptimalStrategyConfig `protobuf:"bytes,4,opt,name=optimal_strategy_config,json=optimal_strategy_config,proto3" json:"optimal_strategy_config,omitempty"`
679+
XXX_NoUnkeyedLiteral struct{} `json:"-"`
680+
XXX_unrecognized []byte `json:"-"`
681+
XXX_sizecache int32 `json:"-"`
668682
}
669683

670684
func (m *BalancingRule) Reset() { *m = BalancingRule{} }
@@ -706,6 +720,13 @@ func (m *BalancingRule) GetOutboundSelector() []string {
706720
return nil
707721
}
708722

723+
func (m *BalancingRule) GetOptimalStrategyConfig() *OptimalStrategyConfig {
724+
if m != nil {
725+
return m.OptimalStrategyConfig
726+
}
727+
return nil
728+
}
729+
709730
type Config struct {
710731
DomainStrategy Config_DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=v2ray.core.app.router.Config_DomainStrategy" json:"domain_strategy,omitempty"`
711732
Rule []*RoutingRule `protobuf:"bytes,2,rep,name=rule,proto3" json:"rule,omitempty"`

app/router/config.proto

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,19 @@ message RoutingRule {
117117
string attributes = 15;
118118
}
119119

120+
message BalancingOptimalStrategyConfig {
121+
uint32 timeout = 1;
122+
uint32 interval = 2;
123+
string target = 3;
124+
string content = 4;
125+
uint32 count = 5;
126+
}
127+
120128
message BalancingRule {
121129
string tag = 1;
122130
repeated string outbound_selector = 2;
131+
string strategy = 3;
132+
BalancingOptimalStrategyConfig optimal_strategy_config = 4;
123133
}
124134

125135
message Config {

0 commit comments

Comments
 (0)