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  )