github.com/bigcommerce/nomad@v0.9.3-bc/nomad/structs/network.go (about)

     1  package structs
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"net"
     7  	"sync"
     8  )
     9  
    10  const (
    11  	// MinDynamicPort is the smallest dynamic port generated
    12  	MinDynamicPort = 20000
    13  
    14  	// MaxDynamicPort is the largest dynamic port generated
    15  	MaxDynamicPort = 32000
    16  
    17  	// maxRandPortAttempts is the maximum number of attempt
    18  	// to assign a random port
    19  	maxRandPortAttempts = 20
    20  
    21  	// maxValidPort is the max valid port number
    22  	maxValidPort = 65536
    23  )
    24  
    25  var (
    26  	// bitmapPool is used to pool the bitmaps used for port collision
    27  	// checking. They are fairly large (8K) so we can re-use them to
    28  	// avoid GC pressure. Care should be taken to call Clear() on any
    29  	// bitmap coming from the pool.
    30  	bitmapPool = new(sync.Pool)
    31  )
    32  
    33  // NetworkIndex is used to index the available network resources
    34  // and the used network resources on a machine given allocations
    35  type NetworkIndex struct {
    36  	AvailNetworks  []*NetworkResource // List of available networks
    37  	AvailBandwidth map[string]int     // Bandwidth by device
    38  	UsedPorts      map[string]Bitmap  // Ports by IP
    39  	UsedBandwidth  map[string]int     // Bandwidth by device
    40  }
    41  
    42  // NewNetworkIndex is used to construct a new network index
    43  func NewNetworkIndex() *NetworkIndex {
    44  	return &NetworkIndex{
    45  		AvailBandwidth: make(map[string]int),
    46  		UsedPorts:      make(map[string]Bitmap),
    47  		UsedBandwidth:  make(map[string]int),
    48  	}
    49  }
    50  
    51  // Release is called when the network index is no longer needed
    52  // to attempt to re-use some of the memory it has allocated
    53  func (idx *NetworkIndex) Release() {
    54  	for _, b := range idx.UsedPorts {
    55  		bitmapPool.Put(b)
    56  	}
    57  }
    58  
    59  // Overcommitted checks if the network is overcommitted
    60  func (idx *NetworkIndex) Overcommitted() bool {
    61  	for device, used := range idx.UsedBandwidth {
    62  		avail := idx.AvailBandwidth[device]
    63  		if used > avail {
    64  			return true
    65  		}
    66  	}
    67  	return false
    68  }
    69  
    70  // SetNode is used to setup the available network resources. Returns
    71  // true if there is a collision
    72  func (idx *NetworkIndex) SetNode(node *Node) (collide bool) {
    73  
    74  	// COMPAT(0.11): Remove in 0.11
    75  	// Grab the network resources, handling both new and old
    76  	var networks []*NetworkResource
    77  	if node.NodeResources != nil && len(node.NodeResources.Networks) != 0 {
    78  		networks = node.NodeResources.Networks
    79  	} else if node.Resources != nil {
    80  		networks = node.Resources.Networks
    81  	}
    82  
    83  	// Add the available CIDR blocks
    84  	for _, n := range networks {
    85  		if n.Device != "" {
    86  			idx.AvailNetworks = append(idx.AvailNetworks, n)
    87  			idx.AvailBandwidth[n.Device] = n.MBits
    88  		}
    89  	}
    90  
    91  	// COMPAT(0.11): Remove in 0.11
    92  	// Handle reserving ports, handling both new and old
    93  	if node.ReservedResources != nil && node.ReservedResources.Networks.ReservedHostPorts != "" {
    94  		collide = idx.AddReservedPortRange(node.ReservedResources.Networks.ReservedHostPorts)
    95  	} else if node.Reserved != nil {
    96  		for _, n := range node.Reserved.Networks {
    97  			if idx.AddReserved(n) {
    98  				collide = true
    99  			}
   100  		}
   101  	}
   102  
   103  	return
   104  }
   105  
   106  // AddAllocs is used to add the used network resources. Returns
   107  // true if there is a collision
   108  func (idx *NetworkIndex) AddAllocs(allocs []*Allocation) (collide bool) {
   109  	for _, alloc := range allocs {
   110  		// Do not consider the resource impact of terminal allocations
   111  		if alloc.TerminalStatus() {
   112  			continue
   113  		}
   114  
   115  		if alloc.AllocatedResources != nil {
   116  			for _, task := range alloc.AllocatedResources.Tasks {
   117  				if len(task.Networks) == 0 {
   118  					continue
   119  				}
   120  				n := task.Networks[0]
   121  				if idx.AddReserved(n) {
   122  					collide = true
   123  				}
   124  			}
   125  		} else {
   126  			// COMPAT(0.11): Remove in 0.11
   127  			for _, task := range alloc.TaskResources {
   128  				if len(task.Networks) == 0 {
   129  					continue
   130  				}
   131  				n := task.Networks[0]
   132  				if idx.AddReserved(n) {
   133  					collide = true
   134  				}
   135  			}
   136  		}
   137  	}
   138  	return
   139  }
   140  
   141  // AddReserved is used to add a reserved network usage, returns true
   142  // if there is a port collision
   143  func (idx *NetworkIndex) AddReserved(n *NetworkResource) (collide bool) {
   144  	// Add the port usage
   145  	used := idx.UsedPorts[n.IP]
   146  	if used == nil {
   147  		// Try to get a bitmap from the pool, else create
   148  		raw := bitmapPool.Get()
   149  		if raw != nil {
   150  			used = raw.(Bitmap)
   151  			used.Clear()
   152  		} else {
   153  			used, _ = NewBitmap(maxValidPort)
   154  		}
   155  		idx.UsedPorts[n.IP] = used
   156  	}
   157  
   158  	for _, ports := range [][]Port{n.ReservedPorts, n.DynamicPorts} {
   159  		for _, port := range ports {
   160  			// Guard against invalid port
   161  			if port.Value < 0 || port.Value >= maxValidPort {
   162  				return true
   163  			}
   164  			if used.Check(uint(port.Value)) {
   165  				collide = true
   166  			} else {
   167  				used.Set(uint(port.Value))
   168  			}
   169  		}
   170  	}
   171  
   172  	// Add the bandwidth
   173  	idx.UsedBandwidth[n.Device] += n.MBits
   174  	return
   175  }
   176  
   177  // AddReservedPortRange marks the ports given as reserved on all network
   178  // interfaces. The port format is comma delimited, with spans given as n1-n2
   179  // (80,100-200,205)
   180  func (idx *NetworkIndex) AddReservedPortRange(ports string) (collide bool) {
   181  	// Convert the ports into a slice of ints
   182  	resPorts, err := ParsePortRanges(ports)
   183  	if err != nil {
   184  		return
   185  	}
   186  
   187  	// Ensure we create a bitmap for each available network
   188  	for _, n := range idx.AvailNetworks {
   189  		used := idx.UsedPorts[n.IP]
   190  		if used == nil {
   191  			// Try to get a bitmap from the pool, else create
   192  			raw := bitmapPool.Get()
   193  			if raw != nil {
   194  				used = raw.(Bitmap)
   195  				used.Clear()
   196  			} else {
   197  				used, _ = NewBitmap(maxValidPort)
   198  			}
   199  			idx.UsedPorts[n.IP] = used
   200  		}
   201  	}
   202  
   203  	for _, used := range idx.UsedPorts {
   204  		for _, port := range resPorts {
   205  			// Guard against invalid port
   206  			if port < 0 || port >= maxValidPort {
   207  				return true
   208  			}
   209  			if used.Check(uint(port)) {
   210  				collide = true
   211  			} else {
   212  				used.Set(uint(port))
   213  			}
   214  		}
   215  	}
   216  
   217  	return
   218  }
   219  
   220  // yieldIP is used to iteratively invoke the callback with
   221  // an available IP
   222  func (idx *NetworkIndex) yieldIP(cb func(net *NetworkResource, ip net.IP) bool) {
   223  	inc := func(ip net.IP) {
   224  		for j := len(ip) - 1; j >= 0; j-- {
   225  			ip[j]++
   226  			if ip[j] > 0 {
   227  				break
   228  			}
   229  		}
   230  	}
   231  
   232  	for _, n := range idx.AvailNetworks {
   233  		ip, ipnet, err := net.ParseCIDR(n.CIDR)
   234  		if err != nil {
   235  			continue
   236  		}
   237  		for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
   238  			if cb(n, ip) {
   239  				return
   240  			}
   241  		}
   242  	}
   243  }
   244  
   245  // AssignNetwork is used to assign network resources given an ask.
   246  // If the ask cannot be satisfied, returns nil
   247  func (idx *NetworkIndex) AssignNetwork(ask *NetworkResource) (out *NetworkResource, err error) {
   248  	err = fmt.Errorf("no networks available")
   249  	idx.yieldIP(func(n *NetworkResource, ip net.IP) (stop bool) {
   250  		// Convert the IP to a string
   251  		ipStr := ip.String()
   252  
   253  		// Check if we would exceed the bandwidth cap
   254  		availBandwidth := idx.AvailBandwidth[n.Device]
   255  		usedBandwidth := idx.UsedBandwidth[n.Device]
   256  		if usedBandwidth+ask.MBits > availBandwidth {
   257  			err = fmt.Errorf("bandwidth exceeded")
   258  			return
   259  		}
   260  
   261  		used := idx.UsedPorts[ipStr]
   262  
   263  		// Check if any of the reserved ports are in use
   264  		for _, port := range ask.ReservedPorts {
   265  			// Guard against invalid port
   266  			if port.Value < 0 || port.Value >= maxValidPort {
   267  				err = fmt.Errorf("invalid port %d (out of range)", port.Value)
   268  				return
   269  			}
   270  
   271  			// Check if in use
   272  			if used != nil && used.Check(uint(port.Value)) {
   273  				err = fmt.Errorf("reserved port collision")
   274  				return
   275  			}
   276  		}
   277  
   278  		// Create the offer
   279  		offer := &NetworkResource{
   280  			Device:        n.Device,
   281  			IP:            ipStr,
   282  			MBits:         ask.MBits,
   283  			ReservedPorts: ask.ReservedPorts,
   284  			DynamicPorts:  ask.DynamicPorts,
   285  		}
   286  
   287  		// Try to stochastically pick the dynamic ports as it is faster and
   288  		// lower memory usage.
   289  		var dynPorts []int
   290  		var dynErr error
   291  		dynPorts, dynErr = getDynamicPortsStochastic(used, ask)
   292  		if dynErr == nil {
   293  			goto BUILD_OFFER
   294  		}
   295  
   296  		// Fall back to the precise method if the random sampling failed.
   297  		dynPorts, dynErr = getDynamicPortsPrecise(used, ask)
   298  		if dynErr != nil {
   299  			err = dynErr
   300  			return
   301  		}
   302  
   303  	BUILD_OFFER:
   304  		for i, port := range dynPorts {
   305  			offer.DynamicPorts[i].Value = port
   306  		}
   307  
   308  		// Stop, we have an offer!
   309  		out = offer
   310  		err = nil
   311  		return true
   312  	})
   313  	return
   314  }
   315  
   316  // getDynamicPortsPrecise takes the nodes used port bitmap which may be nil if
   317  // no ports have been allocated yet, the network ask and returns a set of unused
   318  // ports to fullfil the ask's DynamicPorts or an error if it failed. An error
   319  // means the ask can not be satisfied as the method does a precise search.
   320  func getDynamicPortsPrecise(nodeUsed Bitmap, ask *NetworkResource) ([]int, error) {
   321  	// Create a copy of the used ports and apply the new reserves
   322  	var usedSet Bitmap
   323  	var err error
   324  	if nodeUsed != nil {
   325  		usedSet, err = nodeUsed.Copy()
   326  		if err != nil {
   327  			return nil, err
   328  		}
   329  	} else {
   330  		usedSet, err = NewBitmap(maxValidPort)
   331  		if err != nil {
   332  			return nil, err
   333  		}
   334  	}
   335  
   336  	for _, port := range ask.ReservedPorts {
   337  		usedSet.Set(uint(port.Value))
   338  	}
   339  
   340  	// Get the indexes of the unset
   341  	availablePorts := usedSet.IndexesInRange(false, MinDynamicPort, MaxDynamicPort)
   342  
   343  	// Randomize the amount we need
   344  	numDyn := len(ask.DynamicPorts)
   345  	if len(availablePorts) < numDyn {
   346  		return nil, fmt.Errorf("dynamic port selection failed")
   347  	}
   348  
   349  	numAvailable := len(availablePorts)
   350  	for i := 0; i < numDyn; i++ {
   351  		j := rand.Intn(numAvailable)
   352  		availablePorts[i], availablePorts[j] = availablePorts[j], availablePorts[i]
   353  	}
   354  
   355  	return availablePorts[:numDyn], nil
   356  }
   357  
   358  // getDynamicPortsStochastic takes the nodes used port bitmap which may be nil if
   359  // no ports have been allocated yet, the network ask and returns a set of unused
   360  // ports to fullfil the ask's DynamicPorts or an error if it failed. An error
   361  // does not mean the ask can not be satisfied as the method has a fixed amount
   362  // of random probes and if these fail, the search is aborted.
   363  func getDynamicPortsStochastic(nodeUsed Bitmap, ask *NetworkResource) ([]int, error) {
   364  	var reserved, dynamic []int
   365  	for _, port := range ask.ReservedPorts {
   366  		reserved = append(reserved, port.Value)
   367  	}
   368  
   369  	for i := 0; i < len(ask.DynamicPorts); i++ {
   370  		attempts := 0
   371  	PICK:
   372  		attempts++
   373  		if attempts > maxRandPortAttempts {
   374  			return nil, fmt.Errorf("stochastic dynamic port selection failed")
   375  		}
   376  
   377  		randPort := MinDynamicPort + rand.Intn(MaxDynamicPort-MinDynamicPort)
   378  		if nodeUsed != nil && nodeUsed.Check(uint(randPort)) {
   379  			goto PICK
   380  		}
   381  
   382  		for _, ports := range [][]int{reserved, dynamic} {
   383  			if isPortReserved(ports, randPort) {
   384  				goto PICK
   385  			}
   386  		}
   387  		dynamic = append(dynamic, randPort)
   388  	}
   389  
   390  	return dynamic, nil
   391  }
   392  
   393  // IntContains scans an integer slice for a value
   394  func isPortReserved(haystack []int, needle int) bool {
   395  	for _, item := range haystack {
   396  		if item == needle {
   397  			return true
   398  		}
   399  	}
   400  	return false
   401  }