github.com/kardianos/nomad@v0.1.3-0.20151022182107-b13df73ee850/scheduler/rank.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/nomad/nomad/structs"
     7  )
     8  
     9  // Rank is used to provide a score and various ranking metadata
    10  // along with a node when iterating. This state can be modified as
    11  // various rank methods are applied.
    12  type RankedNode struct {
    13  	Node          *structs.Node
    14  	Score         float64
    15  	TaskResources map[string]*structs.Resources
    16  
    17  	// Allocs is used to cache the proposed allocations on the
    18  	// node. This can be shared between iterators that require it.
    19  	Proposed []*structs.Allocation
    20  }
    21  
    22  func (r *RankedNode) GoString() string {
    23  	return fmt.Sprintf("<Node: %s Score: %0.3f>", r.Node.ID, r.Score)
    24  }
    25  
    26  func (r *RankedNode) ProposedAllocs(ctx Context) ([]*structs.Allocation, error) {
    27  	if r.Proposed != nil {
    28  		return r.Proposed, nil
    29  	}
    30  
    31  	p, err := ctx.ProposedAllocs(r.Node.ID)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	r.Proposed = p
    36  	return p, nil
    37  }
    38  
    39  func (r *RankedNode) SetTaskResources(task *structs.Task,
    40  	resource *structs.Resources) {
    41  	if r.TaskResources == nil {
    42  		r.TaskResources = make(map[string]*structs.Resources)
    43  	}
    44  	r.TaskResources[task.Name] = resource
    45  }
    46  
    47  // RankFeasibleIterator is used to iteratively yield nodes along
    48  // with ranking metadata. The iterators may manage some state for
    49  // performance optimizations.
    50  type RankIterator interface {
    51  	// Next yields a ranked option or nil if exhausted
    52  	Next() *RankedNode
    53  
    54  	// Reset is invoked when an allocation has been placed
    55  	// to reset any stale state.
    56  	Reset()
    57  }
    58  
    59  // FeasibleRankIterator is used to consume from a FeasibleIterator
    60  // and return an unranked node with base ranking.
    61  type FeasibleRankIterator struct {
    62  	ctx    Context
    63  	source FeasibleIterator
    64  }
    65  
    66  // NewFeasibleRankIterator is used to return a new FeasibleRankIterator
    67  // from a FeasibleIterator source.
    68  func NewFeasibleRankIterator(ctx Context, source FeasibleIterator) *FeasibleRankIterator {
    69  	iter := &FeasibleRankIterator{
    70  		ctx:    ctx,
    71  		source: source,
    72  	}
    73  	return iter
    74  }
    75  
    76  func (iter *FeasibleRankIterator) Next() *RankedNode {
    77  	option := iter.source.Next()
    78  	if option == nil {
    79  		return nil
    80  	}
    81  	ranked := &RankedNode{
    82  		Node: option,
    83  	}
    84  	return ranked
    85  }
    86  
    87  func (iter *FeasibleRankIterator) Reset() {
    88  	iter.source.Reset()
    89  }
    90  
    91  // StaticRankIterator is a RankIterator that returns a static set of results.
    92  // This is largely only useful for testing.
    93  type StaticRankIterator struct {
    94  	ctx    Context
    95  	nodes  []*RankedNode
    96  	offset int
    97  	seen   int
    98  }
    99  
   100  // NewStaticRankIterator returns a new static rank iterator over the given nodes
   101  func NewStaticRankIterator(ctx Context, nodes []*RankedNode) *StaticRankIterator {
   102  	iter := &StaticRankIterator{
   103  		ctx:   ctx,
   104  		nodes: nodes,
   105  	}
   106  	return iter
   107  }
   108  
   109  func (iter *StaticRankIterator) Next() *RankedNode {
   110  	// Check if exhausted
   111  	n := len(iter.nodes)
   112  	if iter.offset == n || iter.seen == n {
   113  		if iter.seen != n {
   114  			iter.offset = 0
   115  		} else {
   116  			return nil
   117  		}
   118  	}
   119  
   120  	// Return the next offset
   121  	offset := iter.offset
   122  	iter.offset += 1
   123  	iter.seen += 1
   124  	return iter.nodes[offset]
   125  }
   126  
   127  func (iter *StaticRankIterator) Reset() {
   128  	iter.seen = 0
   129  }
   130  
   131  // BinPackIterator is a RankIterator that scores potential options
   132  // based on a bin-packing algorithm.
   133  type BinPackIterator struct {
   134  	ctx      Context
   135  	source   RankIterator
   136  	evict    bool
   137  	priority int
   138  	tasks    []*structs.Task
   139  }
   140  
   141  // NewBinPackIterator returns a BinPackIterator which tries to fit tasks
   142  // potentially evicting other tasks based on a given priority.
   143  func NewBinPackIterator(ctx Context, source RankIterator, evict bool, priority int) *BinPackIterator {
   144  	iter := &BinPackIterator{
   145  		ctx:      ctx,
   146  		source:   source,
   147  		evict:    evict,
   148  		priority: priority,
   149  	}
   150  	return iter
   151  }
   152  
   153  func (iter *BinPackIterator) SetPriority(p int) {
   154  	iter.priority = p
   155  }
   156  
   157  func (iter *BinPackIterator) SetTasks(tasks []*structs.Task) {
   158  	iter.tasks = tasks
   159  }
   160  
   161  func (iter *BinPackIterator) Next() *RankedNode {
   162  OUTER:
   163  	for {
   164  		// Get the next potential option
   165  		option := iter.source.Next()
   166  		if option == nil {
   167  			return nil
   168  		}
   169  
   170  		// Get the proposed allocations
   171  		proposed, err := option.ProposedAllocs(iter.ctx)
   172  		if err != nil {
   173  			iter.ctx.Logger().Printf(
   174  				"[ERR] sched.binpack: failed to get proposed allocations: %v",
   175  				err)
   176  			continue
   177  		}
   178  
   179  		// Index the existing network usage
   180  		netIdx := structs.NewNetworkIndex()
   181  		netIdx.SetNode(option.Node)
   182  		netIdx.AddAllocs(proposed)
   183  
   184  		// Assign the resources for each task
   185  		total := new(structs.Resources)
   186  		for _, task := range iter.tasks {
   187  			taskResources := task.Resources.Copy()
   188  
   189  			// Check if we need a network resource
   190  			if len(taskResources.Networks) > 0 {
   191  				ask := taskResources.Networks[0]
   192  				offer, err := netIdx.AssignNetwork(ask)
   193  				if offer == nil {
   194  					iter.ctx.Metrics().ExhaustedNode(option.Node,
   195  						fmt.Sprintf("network: %s", err))
   196  					continue OUTER
   197  				}
   198  
   199  				// Reserve this to prevent another task from colliding
   200  				netIdx.AddReserved(offer)
   201  
   202  				// Update the network ask to the offer
   203  				taskResources.Networks = []*structs.NetworkResource{offer}
   204  			}
   205  
   206  			// Store the task resource
   207  			option.SetTaskResources(task, taskResources)
   208  
   209  			// Accumulate the total resource requirement
   210  			total.Add(taskResources)
   211  		}
   212  
   213  		// Add the resources we are trying to fit
   214  		proposed = append(proposed, &structs.Allocation{Resources: total})
   215  
   216  		// Check if these allocations fit, if they do not, simply skip this node
   217  		fit, dim, util, _ := structs.AllocsFit(option.Node, proposed, netIdx)
   218  		if !fit {
   219  			iter.ctx.Metrics().ExhaustedNode(option.Node, dim)
   220  			continue
   221  		}
   222  
   223  		// XXX: For now we completely ignore evictions. We should use that flag
   224  		// to determine if its possible to evict other lower priority allocations
   225  		// to make room. This explodes the search space, so it must be done
   226  		// carefully.
   227  
   228  		// Score the fit normally otherwise
   229  		fitness := structs.ScoreFit(option.Node, util)
   230  		option.Score += fitness
   231  		iter.ctx.Metrics().ScoreNode(option.Node, "binpack", fitness)
   232  		return option
   233  	}
   234  }
   235  
   236  func (iter *BinPackIterator) Reset() {
   237  	iter.source.Reset()
   238  }
   239  
   240  // JobAntiAffinityIterator is used to apply an anti-affinity to allocating
   241  // along side other allocations from this job. This is used to help distribute
   242  // load across the cluster.
   243  type JobAntiAffinityIterator struct {
   244  	ctx     Context
   245  	source  RankIterator
   246  	penalty float64
   247  	jobID   string
   248  }
   249  
   250  // NewJobAntiAffinityIterator is used to create a JobAntiAffinityIterator that
   251  // applies the given penalty for co-placement with allocs from this job.
   252  func NewJobAntiAffinityIterator(ctx Context, source RankIterator, penalty float64, jobID string) *JobAntiAffinityIterator {
   253  	iter := &JobAntiAffinityIterator{
   254  		ctx:     ctx,
   255  		source:  source,
   256  		penalty: penalty,
   257  		jobID:   jobID,
   258  	}
   259  	return iter
   260  }
   261  
   262  func (iter *JobAntiAffinityIterator) SetJob(jobID string) {
   263  	iter.jobID = jobID
   264  }
   265  
   266  func (iter *JobAntiAffinityIterator) Next() *RankedNode {
   267  	for {
   268  		option := iter.source.Next()
   269  		if option == nil {
   270  			return nil
   271  		}
   272  
   273  		// Get the proposed allocations
   274  		proposed, err := option.ProposedAllocs(iter.ctx)
   275  		if err != nil {
   276  			iter.ctx.Logger().Printf(
   277  				"[ERR] sched.job-anti-aff: failed to get proposed allocations: %v",
   278  				err)
   279  			continue
   280  		}
   281  
   282  		// Determine the number of collisions
   283  		collisions := 0
   284  		for _, alloc := range proposed {
   285  			if alloc.JobID == iter.jobID {
   286  				collisions += 1
   287  			}
   288  		}
   289  
   290  		// Apply a penalty if there are collisions
   291  		if collisions > 0 {
   292  			scorePenalty := -1 * float64(collisions) * iter.penalty
   293  			option.Score += scorePenalty
   294  			iter.ctx.Metrics().ScoreNode(option.Node, "job-anti-affinity", scorePenalty)
   295  		}
   296  		return option
   297  	}
   298  }
   299  
   300  func (iter *JobAntiAffinityIterator) Reset() {
   301  	iter.source.Reset()
   302  }