@marcel91200 / caddy Mirror · read-only / commits / 361946eb
Fork 0

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{}