Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 01:45 UTC
reverseproxy: weighted_round_robin load balancing policy (#5579)
* added weighted round robin algorithm to load balancer * added an adapt integration test for wrr and fixed a typo * changed args format to Caddyfile args convention * added provisioner and validator for wrr * simplified the code and improved doc
Saber Haj Rabiee committed
Jun 20, 2023 at 10:42 UTC
361946eb0c08791ad16ebc3e82a79512895e650f
3 files changed
+218
-1
caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance_wrr.txt
new
+71
@@ -0,0 +1,71 @@
1
+:8884
2
+
3
+reverse_proxy 127.0.0.1:65535 127.0.0.1:35535 {
4
+ lb_policy weighted_round_robin 10 1
5
+ lb_retries 5
6
+ lb_try_duration 10s
7
+ lb_try_interval 500ms
8
+ lb_retry_match {
9
+ path /foo*
10
+ method POST
11
+ }
12
+ lb_retry_match path /bar*
13
+}
14
+----------
15
+{
16
+ "apps": {
17
+ "http": {
18
+ "servers": {
19
+ "srv0": {
20
+ "listen": [
21
+ ":8884"
22
+ ],
23
+ "routes": [
24
+ {
25
+ "handle": [
26
+ {
27
+ "handler": "reverse_proxy",
28
+ "load_balancing": {
29
+ "retries": 5,
30
+ "retry_match": [
31
+ {
32
+ "method": [
33
+ "POST"
34
+ ],
35
+ "path": [
36
+ "/foo*"
37
+ ]
38
+ },
39
+ {
40
+ "path": [
41
+ "/bar*"
42
+ ]
43
+ }
44
+ ],
45
+ "selection_policy": {
46
+ "policy": "weighted_round_robin",
47
+ "weights": [
48
+ 10,
49
+ 1
50
+ ]
51
+ },
52
+ "try_duration": 10000000000,
53
+ "try_interval": 500000000
54
+ },
55
+ "upstreams": [
56
+ {
57
+ "dial": "127.0.0.1:65535"
58
+ },
59
+ {
60
+ "dial": "127.0.0.1:35535"
61
+ }
62
+ ]
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+ }
68
+ }
69
+ }
70
+ }
71
+}
modules/caddyhttp/reverseproxy/selectionpolicies.go
+90
-1
@@ -40,6 +40,7 @@ func init() {
40
caddy.RegisterModule(RandomChoiceSelection{})
41
caddy.RegisterModule(LeastConnSelection{})
42
caddy.RegisterModule(RoundRobinSelection{})
43
+ caddy.RegisterModule(WeightedRoundRobinSelection{})
44
caddy.RegisterModule(FirstSelection{})
45
caddy.RegisterModule(IPHashSelection{})
46
caddy.RegisterModule(ClientIPHashSelection{})
@@ -78,6 +79,90 @@ func (r *RandomSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
79
return nil
80
}
81
82
+// WeightedRoundRobinSelection is a policy that selects
83
+// a host based on weighted round-robin ordering.
84
+type WeightedRoundRobinSelection struct {
85
+ // The weight of each upstream in order,
86
+ // corresponding with the list of upstreams configured.
87
+ Weights []int `json:"weights,omitempty"`
88
+ index uint32
89
+ totalWeight int
90
+}
91
+
92
+// CaddyModule returns the Caddy module information.
93
+func (WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
94
+ return caddy.ModuleInfo{
95
+ ID: "http.reverse_proxy.selection_policies.weighted_round_robin",
96
+ New: func() caddy.Module {
97
+ return new(WeightedRoundRobinSelection)
98
+ },
99
+ }
100
+}
101
+
102
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
103
+func (r *WeightedRoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
104
+ for d.Next() {
105
+ args := d.RemainingArgs()
106
+ if len(args) == 0 {
107
+ return d.ArgErr()
108
+ }
109
+
110
+ for _, weight := range args {
111
+ weightInt, err := strconv.Atoi(weight)
112
+ if err != nil {
113
+ return d.Errf("invalid weight value '%s': %v", weight, err)
114
+ }
115
+ if weightInt < 1 {
116
+ return d.Errf("invalid weight value '%s': weight should be non-zero and positive", weight)
117
+ }
118
+ r.Weights = append(r.Weights, weightInt)
119
+ }
120
+ }
121
+ return nil
122
+}
123
+
124
+// Provision sets up r.
125
+func (r *WeightedRoundRobinSelection) Provision(ctx caddy.Context) error {
126
+ for _, weight := range r.Weights {
127
+ r.totalWeight += weight
128
+ }
129
+ return nil
130
+}
131
+
132
+// Select returns an available host, if any.
133
+func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream {
134
+ if len(pool) == 0 {
135
+ return nil
136
+ }
137
+ if len(r.Weights) < 2 {
138
+ return pool[0]
139
+ }
140
+ var index, totalWeight int
141
+ currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight
142
+ for i, weight := range r.Weights {
143
+ totalWeight += weight
144
+ if currentWeight < totalWeight {
145
+ index = i
146
+ break
147
+ }
148
+ }
149
+
150
+ upstreams := make([]*Upstream, 0, len(r.Weights))
151
+ for _, upstream := range pool {
152
+ if !upstream.Available() {
153
+ continue
154
+ }
155
+ upstreams = append(upstreams, upstream)
156
+ if len(upstreams) == cap(upstreams) {
157
+ break
158
+ }
159
+ }
160
+ if len(upstreams) == 0 {
161
+ return nil
162
+ }
163
+ return upstreams[index%len(upstreams)]
164
+}
165
+
166
// RandomChoiceSelection is a policy that selects
167
// two or more available hosts at random, then
168
// chooses the one with the least load.
@@ -762,6 +847,7 @@ var (
847
_ Selector = (*RandomChoiceSelection)(nil)
848
_ Selector = (*LeastConnSelection)(nil)
849
_ Selector = (*RoundRobinSelection)(nil)
850
+ _ Selector = (*WeightedRoundRobinSelection)(nil)
851
_ Selector = (*FirstSelection)(nil)
852
_ Selector = (*IPHashSelection)(nil)
853
_ Selector = (*ClientIPHashSelection)(nil)
@@ -770,8 +856,11 @@ var (
856
_ Selector = (*HeaderHashSelection)(nil)
857
_ Selector = (*CookieHashSelection)(nil)
858
773
- _ caddy.Validator = (*RandomChoiceSelection)(nil)
859
+ _ caddy.Validator = (*RandomChoiceSelection)(nil)
860
+
861
_ caddy.Provisioner = (*RandomChoiceSelection)(nil)
862
+ _ caddy.Provisioner = (*WeightedRoundRobinSelection)(nil)
863
864
_ caddyfile.Unmarshaler = (*RandomChoiceSelection)(nil)
865
+ _ caddyfile.Unmarshaler = (*WeightedRoundRobinSelection)(nil)
866
)
modules/caddyhttp/reverseproxy/selectionpolicies_test.go
+57
@@ -74,6 +74,63 @@ func TestRoundRobinPolicy(t *testing.T) {
74
}
75
}
76
77
+func TestWeightedRoundRobinPolicy(t *testing.T) {
78
+ pool := testPool()
79
+ wrrPolicy := WeightedRoundRobinSelection{
80
+ Weights: []int{3, 2, 1},
81
+ totalWeight: 6,
82
+ }
83
+ req, _ := http.NewRequest("GET", "/", nil)
84
+
85
+ h := wrrPolicy.Select(pool, req, nil)
86
+ if h != pool[0] {
87
+ t.Error("Expected first weighted round robin host to be first host in the pool.")
88
+ }
89
+ h = wrrPolicy.Select(pool, req, nil)
90
+ if h != pool[0] {
91
+ t.Error("Expected second weighted round robin host to be first host in the pool.")
92
+ }
93
+ // Third selected host is 1, because counter starts at 0
94
+ // and increments before host is selected
95
+ h = wrrPolicy.Select(pool, req, nil)
96
+ if h != pool[1] {
97
+ t.Error("Expected third weighted round robin host to be second host in the pool.")
98
+ }
99
+ h = wrrPolicy.Select(pool, req, nil)
100
+ if h != pool[1] {
101
+ t.Error("Expected fourth weighted round robin host to be second host in the pool.")
102
+ }
103
+ h = wrrPolicy.Select(pool, req, nil)
104
+ if h != pool[2] {
105
+ t.Error("Expected fifth weighted round robin host to be third host in the pool.")
106
+ }
107
+ h = wrrPolicy.Select(pool, req, nil)
108
+ if h != pool[0] {
109
+ t.Error("Expected sixth weighted round robin host to be first host in the pool.")
110
+ }
111
+
112
+ // mark host as down
113
+ pool[0].setHealthy(false)
114
+ h = wrrPolicy.Select(pool, req, nil)
115
+ if h != pool[1] {
116
+ t.Error("Expected to skip down host.")
117
+ }
118
+ // mark host as up
119
+ pool[0].setHealthy(true)
120
+
121
+ h = wrrPolicy.Select(pool, req, nil)
122
+ if h != pool[0] {
123
+ t.Error("Expected to select first host on availablity.")
124
+ }
125
+ // mark host as full
126
+ pool[1].countRequest(1)
127
+ pool[1].MaxRequests = 1
128
+ h = wrrPolicy.Select(pool, req, nil)
129
+ if h != pool[2] {
130
+ t.Error("Expected to skip full host.")
131
+ }
132
+}
133
+
134
func TestLeastConnPolicy(t *testing.T) {
135
pool := testPool()
136
lcPolicy := LeastConnSelection{}