github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4proxy/loadbalancing.go (about) 1 // Copyright 2020 Matthew Holt 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package l4proxy 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "hash/fnv" 21 weakrand "math/rand" 22 "net" 23 "strconv" 24 "sync/atomic" 25 "time" 26 27 "github.com/caddyserver/caddy/v2" 28 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 29 30 "github.com/mholt/caddy-l4/layer4" 31 ) 32 33 // LoadBalancing has parameters related to load balancing. 34 type LoadBalancing struct { 35 // A selection policy is how to choose an available backend. 36 // The default policy is random selection. 37 SelectionPolicyRaw json.RawMessage `json:"selection,omitempty" caddy:"namespace=layer4.proxy.selection_policies inline_key=policy"` 38 39 // How long to try selecting available backends for each connection 40 // if the next available host is down. By default, this retry is 41 // disabled. Clients will wait for up to this long while the load 42 // balancer tries to find an available upstream host. 43 TryDuration caddy.Duration `json:"try_duration,omitempty"` 44 45 // How long to wait between selecting the next host from the pool. Default 46 // is 250ms. Only relevant when a connection to an upstream host fails. Be 47 // aware that setting this to 0 with a non-zero try_duration can cause the 48 // CPU to spin if all backends are down and latency is very low. 49 TryInterval caddy.Duration `json:"try_interval,omitempty"` 50 51 SelectionPolicy Selector `json:"-"` 52 } 53 54 // tryAgain takes the time that the handler was initially invoked 55 // and returns true if another attempt should be made at proxying the 56 // connection. If true is returned, it has already blocked long enough 57 // before the next retry (i.e. no more sleeping is needed). If false 58 // is returned, the handler should stop trying to proxy the connection. 59 func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time) bool { 60 // if we've tried long enough, break 61 if time.Since(start) >= time.Duration(lb.TryDuration) { 62 return false 63 } 64 65 // otherwise, wait and try the next available host 66 select { 67 case <-time.After(time.Duration(lb.TryInterval)): 68 return true 69 case <-ctx.Done(): 70 return false 71 } 72 } 73 74 // Selector selects an available upstream from the pool. 75 type Selector interface { 76 Select(UpstreamPool, *layer4.Connection) *Upstream 77 } 78 79 func init() { 80 caddy.RegisterModule(&RandomSelection{}) 81 caddy.RegisterModule(&RandomChoiceSelection{}) 82 caddy.RegisterModule(&LeastConnSelection{}) 83 caddy.RegisterModule(&RoundRobinSelection{}) 84 caddy.RegisterModule(&FirstSelection{}) 85 caddy.RegisterModule(&IPHashSelection{}) 86 } 87 88 // RandomSelection is a policy that selects 89 // an available host at random. 90 type RandomSelection struct{} 91 92 // CaddyModule returns the Caddy module information. 93 func (*RandomSelection) CaddyModule() caddy.ModuleInfo { 94 return caddy.ModuleInfo{ 95 ID: "layer4.proxy.selection_policies.random", 96 New: func() caddy.Module { return new(RandomSelection) }, 97 } 98 } 99 100 // Select returns an available host, if any. 101 func (r *RandomSelection) Select(pool UpstreamPool, _ *layer4.Connection) *Upstream { 102 // use reservoir sampling because the number of available 103 // hosts isn't known: https://en.wikipedia.org/wiki/Reservoir_sampling 104 var randomHost *Upstream 105 var count int 106 for _, upstream := range pool { 107 if !upstream.available() { 108 continue 109 } 110 // (n % 1 == 0) holds for all n, therefore an 111 // upstream will always be chosen if there is at 112 // least one available 113 count++ 114 if (weakrand.Int() % count) == 0 { 115 randomHost = upstream 116 } 117 } 118 return randomHost 119 } 120 121 // UnmarshalCaddyfile sets up the RandomSelection from Caddyfile tokens. Syntax: 122 // 123 // random 124 func (r *RandomSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 125 _, wrapper := d.Next(), d.Val() // consume wrapper name 126 127 // No same-line options are supported 128 if d.CountRemainingArgs() > 0 { 129 return d.ArgErr() 130 } 131 132 // No blocks are supported 133 if d.NextBlock(d.Nesting()) { 134 return d.Errf("malformed %s selection policy: blocks are not supported", wrapper) 135 } 136 137 return nil 138 } 139 140 // RandomChoiceSelection is a policy that selects 141 // two or more available hosts at random, then 142 // chooses the one with the least load. 143 type RandomChoiceSelection struct { 144 // The size of the sub-pool created from the larger upstream pool. The default value 145 // is 2 and the maximum at selection time is the size of the upstream pool. 146 Choose int `json:"choose,omitempty"` 147 } 148 149 // CaddyModule returns the Caddy module information. 150 func (*RandomChoiceSelection) CaddyModule() caddy.ModuleInfo { 151 return caddy.ModuleInfo{ 152 ID: "layer4.proxy.selection_policies.random_choose", 153 New: func() caddy.Module { return new(RandomChoiceSelection) }, 154 } 155 } 156 157 // Provision sets up r. 158 func (r *RandomChoiceSelection) Provision(_ caddy.Context) error { 159 if r.Choose == 0 { 160 r.Choose = 2 161 } 162 return nil 163 } 164 165 // Validate ensures that r's configuration is valid. 166 func (r *RandomChoiceSelection) Validate() error { 167 if r.Choose < 2 { 168 return fmt.Errorf("choose must be at least 2") 169 } 170 return nil 171 } 172 173 // Select returns an available host, if any. 174 func (r *RandomChoiceSelection) Select(pool UpstreamPool, _ *layer4.Connection) *Upstream { 175 k := r.Choose 176 if k > len(pool) { 177 k = len(pool) 178 } 179 choices := make([]*Upstream, k) 180 for i, upstream := range pool { 181 if !upstream.available() { 182 continue 183 } 184 j := weakrand.Intn(i + 1) 185 if j < k { 186 choices[j] = upstream 187 } 188 } 189 return leastConns(choices) 190 } 191 192 // UnmarshalCaddyfile sets up the RandomChoiceSelection from Caddyfile tokens. Syntax: 193 // 194 // random_choose <int> 195 // random_choose 196 func (r *RandomChoiceSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 197 _, wrapper := d.Next(), d.Val() // consume wrapper name 198 199 // Only one same-line option is supported 200 if d.CountRemainingArgs() > 1 { 201 return d.ArgErr() 202 } 203 204 if d.NextArg() { 205 val, err := strconv.ParseInt(d.Val(), 10, 32) 206 if err != nil { 207 return err 208 } 209 r.Choose = int(val) 210 } 211 212 // No blocks are supported 213 if d.NextBlock(d.Nesting()) { 214 return d.Errf("malformed %s selection policy: blocks are not supported", wrapper) 215 } 216 217 return nil 218 } 219 220 // LeastConnSelection is a policy that selects the upstream 221 // with the least active connections. If multiple upstreams 222 // have the same fewest number, one is chosen randomly. 223 type LeastConnSelection struct{} 224 225 // CaddyModule returns the Caddy module information. 226 func (*LeastConnSelection) CaddyModule() caddy.ModuleInfo { 227 return caddy.ModuleInfo{ 228 ID: "layer4.proxy.selection_policies.least_conn", 229 New: func() caddy.Module { return new(LeastConnSelection) }, 230 } 231 } 232 233 // Select selects the up host with the least number of connections in the 234 // pool. If more than one host has the same least number of connections, 235 // one of the hosts is chosen at random. 236 func (*LeastConnSelection) Select(pool UpstreamPool, _ *layer4.Connection) *Upstream { 237 var best *Upstream 238 var count int 239 leastConns := -1 240 241 for _, upstream := range pool { 242 if !upstream.available() { 243 continue 244 } 245 totalConns := upstream.totalConns() 246 if leastConns == -1 || totalConns < leastConns { 247 leastConns = totalConns 248 count = 0 249 } 250 251 // among hosts with same least connections, perform a reservoir 252 // sample: https://en.wikipedia.org/wiki/Reservoir_sampling 253 if totalConns == leastConns { 254 count++ 255 if (weakrand.Int() % count) == 0 { 256 best = upstream 257 } 258 } 259 } 260 261 return best 262 } 263 264 // UnmarshalCaddyfile sets up the LeastConnSelection from Caddyfile tokens. Syntax: 265 // 266 // least_conn 267 func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 268 _, wrapper := d.Next(), d.Val() // consume wrapper name 269 270 // No same-line options are supported 271 if d.CountRemainingArgs() > 0 { 272 return d.ArgErr() 273 } 274 275 // No blocks are supported 276 if d.NextBlock(d.Nesting()) { 277 return d.Errf("malformed %s selection policy: blocks are not supported", wrapper) 278 } 279 280 return nil 281 } 282 283 // RoundRobinSelection is a policy that selects 284 // a host based on round-robin ordering. 285 type RoundRobinSelection struct { 286 robin uint32 287 } 288 289 // CaddyModule returns the Caddy module information. 290 func (*RoundRobinSelection) CaddyModule() caddy.ModuleInfo { 291 return caddy.ModuleInfo{ 292 ID: "layer4.proxy.selection_policies.round_robin", 293 New: func() caddy.Module { return new(RoundRobinSelection) }, 294 } 295 } 296 297 // Select returns an available host, if any. 298 func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *layer4.Connection) *Upstream { 299 n := uint32(len(pool)) 300 if n == 0 { 301 return nil 302 } 303 for i := uint32(0); i < n; i++ { 304 atomic.AddUint32(&r.robin, 1) 305 host := pool[r.robin%n] 306 if host.available() { 307 return host 308 } 309 } 310 return nil 311 } 312 313 // UnmarshalCaddyfile sets up the RoundRobinSelection from Caddyfile tokens. Syntax: 314 // 315 // round_robin 316 func (r *RoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 317 _, wrapper := d.Next(), d.Val() // consume wrapper name 318 319 // No same-line options are supported 320 if d.CountRemainingArgs() > 0 { 321 return d.ArgErr() 322 } 323 324 // No blocks are supported 325 if d.NextBlock(d.Nesting()) { 326 return d.Errf("malformed %s selection policy: blocks are not supported", wrapper) 327 } 328 329 return nil 330 } 331 332 // FirstSelection is a policy that selects 333 // the first available host. 334 type FirstSelection struct{} 335 336 // CaddyModule returns the Caddy module information. 337 func (*FirstSelection) CaddyModule() caddy.ModuleInfo { 338 return caddy.ModuleInfo{ 339 ID: "layer4.proxy.selection_policies.first", 340 New: func() caddy.Module { return new(FirstSelection) }, 341 } 342 } 343 344 // Select returns an available host, if any. 345 func (*FirstSelection) Select(pool UpstreamPool, _ *layer4.Connection) *Upstream { 346 for _, host := range pool { 347 if host.available() { 348 return host 349 } 350 } 351 return nil 352 } 353 354 // UnmarshalCaddyfile sets up the FirstSelection from Caddyfile tokens. Syntax: 355 // 356 // first 357 func (r *FirstSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 358 _, wrapper := d.Next(), d.Val() // consume wrapper name 359 360 // No same-line options are supported 361 if d.CountRemainingArgs() > 0 { 362 return d.ArgErr() 363 } 364 365 // No blocks are supported 366 if d.NextBlock(d.Nesting()) { 367 return d.Errf("malformed %s selection policy: blocks are not supported", wrapper) 368 } 369 370 return nil 371 } 372 373 // IPHashSelection is a policy that selects a host 374 // based on hashing the remote IP of the connection. 375 type IPHashSelection struct{} 376 377 // CaddyModule returns the Caddy module information. 378 func (*IPHashSelection) CaddyModule() caddy.ModuleInfo { 379 return caddy.ModuleInfo{ 380 ID: "layer4.proxy.selection_policies.ip_hash", 381 New: func() caddy.Module { return new(IPHashSelection) }, 382 } 383 } 384 385 // Select returns an available host, if any. 386 func (*IPHashSelection) Select(pool UpstreamPool, conn *layer4.Connection) *Upstream { 387 remoteAddr := conn.Conn.RemoteAddr().String() 388 clientIP, _, err := net.SplitHostPort(remoteAddr) 389 if err != nil { 390 clientIP = remoteAddr 391 } 392 return hostByHashing(pool, clientIP) 393 } 394 395 // UnmarshalCaddyfile sets up the IPHashSelection from Caddyfile tokens. Syntax: 396 // 397 // ip_hash 398 func (r *IPHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 399 _, wrapper := d.Next(), d.Val() // consume wrapper name 400 401 // No same-line options are supported 402 if d.CountRemainingArgs() > 0 { 403 return d.ArgErr() 404 } 405 406 // No blocks are supported 407 if d.NextBlock(d.Nesting()) { 408 return d.Errf("malformed %s selection policy: blocks are not supported", wrapper) 409 } 410 411 return nil 412 } 413 414 // leastConns returns the upstream with the 415 // least number of active connections to it. 416 // If more than one upstream has the same 417 // least number of active connections, then 418 // one of those is chosen at random. 419 func leastConns(upstreams []*Upstream) *Upstream { 420 if len(upstreams) == 0 { 421 return nil 422 } 423 var best []*Upstream 424 var bestReqs int 425 for _, upstream := range upstreams { 426 reqs := upstream.totalConns() 427 if reqs == 0 { 428 return upstream 429 } 430 if reqs <= bestReqs { 431 bestReqs = reqs 432 best = append(best, upstream) 433 } 434 } 435 if len(best) == 0 { 436 return nil 437 } 438 return best[weakrand.Intn(len(best))] 439 } 440 441 // hostByHashing returns an available host 442 // from pool based on a hashable string s. 443 func hostByHashing(pool []*Upstream, s string) *Upstream { 444 // HRW hash (copy from caddy's code) 445 var highestHash uint32 446 var upstream *Upstream 447 for _, up := range pool { 448 if !up.available() { 449 continue 450 } 451 h := hash(up.String() + s) // important to hash key and server together 452 if h > highestHash { 453 highestHash = h 454 upstream = up 455 } 456 } 457 return upstream 458 } 459 460 // hash calculates a fast hash based on s. 461 func hash(s string) uint32 { 462 h := fnv.New32a() 463 _, _ = h.Write([]byte(s)) 464 return h.Sum32() 465 } 466 467 // Interface guards 468 var ( 469 _ Selector = (*RandomSelection)(nil) 470 _ Selector = (*RandomChoiceSelection)(nil) 471 _ Selector = (*LeastConnSelection)(nil) 472 _ Selector = (*RoundRobinSelection)(nil) 473 _ Selector = (*FirstSelection)(nil) 474 _ Selector = (*IPHashSelection)(nil) 475 476 _ caddy.Validator = (*RandomChoiceSelection)(nil) 477 _ caddy.Provisioner = (*RandomChoiceSelection)(nil) 478 479 _ caddyfile.Unmarshaler = (*RandomSelection)(nil) 480 _ caddyfile.Unmarshaler = (*RandomChoiceSelection)(nil) 481 _ caddyfile.Unmarshaler = (*LeastConnSelection)(nil) 482 _ caddyfile.Unmarshaler = (*RoundRobinSelection)(nil) 483 _ caddyfile.Unmarshaler = (*FirstSelection)(nil) 484 _ caddyfile.Unmarshaler = (*IPHashSelection)(nil) 485 )