github.com/m3db/m3@v1.5.0/src/cluster/placement/selector/non_mirrored.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package selector
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"sort"
    27  
    28  	"github.com/m3db/m3/src/cluster/placement"
    29  	"github.com/m3db/m3/src/cluster/placement/algo"
    30  )
    31  
    32  var (
    33  	errInstanceAbsent  = errors.New("could not remove or replace a instance that does not exist")
    34  	errNoValidInstance = errors.New("no valid instance in the candidate list")
    35  )
    36  
    37  type nonMirroredSelector struct {
    38  	opts placement.Options
    39  }
    40  
    41  // NewNonMirroredSelector constructs an instance selector which doesn't mirror traffic
    42  // (no shardsets) and which takes into account existing
    43  // shard placement and instance weight in order to choose instances.
    44  func NewNonMirroredSelector(opts placement.Options) placement.InstanceSelector {
    45  	return &nonMirroredSelector{opts: opts}
    46  }
    47  
    48  func (f *nonMirroredSelector) SelectInitialInstances(
    49  	candidates []placement.Instance,
    50  	rf int,
    51  ) ([]placement.Instance, error) {
    52  	return getValidCandidates(placement.NewPlacement(), candidates, f.opts)
    53  }
    54  
    55  func (f *nonMirroredSelector) SelectAddingInstances(
    56  	candidates []placement.Instance,
    57  	p placement.Placement,
    58  ) ([]placement.Instance, error) {
    59  	candidates, err := getValidCandidates(p, candidates, f.opts)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	if f.opts.AddAllCandidates() {
    65  		return candidates, nil
    66  	}
    67  
    68  	instance, err := selectSingleCandidate(candidates, p)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	return []placement.Instance{instance}, nil
    73  }
    74  
    75  func (f *nonMirroredSelector) SelectReplaceInstances(
    76  	candidates []placement.Instance,
    77  	leavingInstanceIDs []string,
    78  	p placement.Placement,
    79  ) ([]placement.Instance, error) {
    80  	candidates, err := getValidCandidates(p, candidates, f.opts)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	leavingInstances, err := getLeavingInstances(p, leavingInstanceIDs)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	if f.opts.AddAllCandidates() {
    91  		if err := f.validateReplaceInstances(candidates, leavingInstances); err != nil {
    92  			return nil, err
    93  		}
    94  		return candidates, nil
    95  	}
    96  
    97  	return f.selectReplaceInstances(candidates, leavingInstances, p)
    98  }
    99  
   100  // selectReplaceInstances returns a sufficient number of instances to replace the weight
   101  // of the leaving instances.
   102  func (f *nonMirroredSelector) selectReplaceInstances(
   103  	candidates, leavingInstances []placement.Instance,
   104  	p placement.Placement,
   105  ) ([]placement.Instance, error) {
   106  	// Map isolation group to instances.
   107  	candidateGroups := buildIsolationGroupMap(candidates)
   108  
   109  	// Sort the candidate instances by the number of conflicts.
   110  	ph := algo.NewPlacementHelper(p, f.opts)
   111  	instances := make([]sortableValue, 0, len(candidateGroups))
   112  	for group, instancesInGroup := range candidateGroups {
   113  		conflicts := 0
   114  		for _, leaving := range leavingInstances {
   115  			for _, s := range leaving.Shards().All() {
   116  				if !ph.CanMoveShard(s.ID(), leaving, group) {
   117  					conflicts++
   118  				}
   119  			}
   120  		}
   121  		for _, instance := range instancesInGroup {
   122  			instances = append(instances, sortableValue{value: instance, weight: conflicts})
   123  		}
   124  	}
   125  
   126  	groups := groupInstancesByConflict(instances, f.opts)
   127  	if len(groups) == 0 {
   128  		return nil, errNoValidInstance
   129  	}
   130  
   131  	var totalWeight uint32
   132  	for _, instance := range leavingInstances {
   133  		totalWeight += instance.Weight()
   134  	}
   135  	result, leftWeight := fillWeight(groups, int(totalWeight))
   136  
   137  	if leftWeight > 0 && !f.opts.AllowPartialReplace() {
   138  		return nil, fmt.Errorf("could not find enough instances to replace %v, %d weight could not be replaced",
   139  			leavingInstances, leftWeight)
   140  	}
   141  	return result, nil
   142  }
   143  
   144  func (f *nonMirroredSelector) validateReplaceInstances(
   145  	candidates, leavingInstances []placement.Instance,
   146  ) error {
   147  	var leavingWeight int
   148  	for _, instance := range leavingInstances {
   149  		leavingWeight += int(instance.Weight())
   150  	}
   151  	var candidateWeight int
   152  	for _, instance := range candidates {
   153  		candidateWeight += int(instance.Weight())
   154  	}
   155  
   156  	if leavingWeight > candidateWeight && !f.opts.AllowPartialReplace() {
   157  		return fmt.Errorf("could not find enough instances to replace %v, %d weight could not be replaced",
   158  			leavingInstances, leavingWeight)
   159  	}
   160  	return nil
   161  }
   162  
   163  func groupInstancesByConflict(instancesSortedByConflicts []sortableValue, opts placement.Options) [][]placement.Instance {
   164  	allowConflict := opts.AllowPartialReplace()
   165  	sort.Sort(sortableValues(instancesSortedByConflicts))
   166  	var groups [][]placement.Instance
   167  	lastSeenConflict := -1
   168  	for _, instance := range instancesSortedByConflicts {
   169  		if !allowConflict && instance.weight > 0 {
   170  			break
   171  		}
   172  		if instance.weight > lastSeenConflict {
   173  			lastSeenConflict = instance.weight
   174  			groups = append(groups, []placement.Instance{})
   175  		}
   176  		if lastSeenConflict == instance.weight {
   177  			groups[len(groups)-1] = append(groups[len(groups)-1], instance.value.(placement.Instance))
   178  		}
   179  	}
   180  	return groups
   181  }
   182  
   183  func fillWeight(groups [][]placement.Instance, targetWeight int) ([]placement.Instance, int) {
   184  	var (
   185  		result           []placement.Instance
   186  		instancesInGroup []placement.Instance
   187  	)
   188  	for _, group := range groups {
   189  		sort.Sort(placement.ByIDAscending(group))
   190  		instancesInGroup, targetWeight = knapsack(group, targetWeight)
   191  		result = append(result, instancesInGroup...)
   192  		if targetWeight <= 0 {
   193  			break
   194  		}
   195  	}
   196  	return result, targetWeight
   197  }
   198  
   199  func knapsack(instances []placement.Instance, targetWeight int) ([]placement.Instance, int) {
   200  	totalWeight := 0
   201  	for _, instance := range instances {
   202  		totalWeight += int(instance.Weight())
   203  	}
   204  	if totalWeight <= targetWeight {
   205  		return instances[:], targetWeight - totalWeight
   206  	}
   207  	// totalWeight > targetWeight, there is a combination of instances to meet targetWeight for sure
   208  	// we do dp until totalWeight rather than targetWeight here because we need to
   209  	// at least cover the targetWeight, which is a little bit different than the knapsack problem
   210  	weights := make([]int, totalWeight+1)
   211  	combination := make([][]placement.Instance, totalWeight+1)
   212  
   213  	// dp: weights[i][j] = max(weights[i-1][j], weights[i-1][j-instance.Weight] + instance.Weight)
   214  	// when there are multiple combination to reach a target weight, we prefer the one with less instances
   215  	for i := range instances {
   216  		weight := int(instances[i].Weight())
   217  		// this loop needs to go from len to 1 because weights is being updated in place
   218  		for j := totalWeight; j >= 1; j-- {
   219  			if j-weight < 0 {
   220  				continue
   221  			}
   222  			newWeight := weights[j-weight] + weight
   223  			if newWeight > weights[j] {
   224  				weights[j] = weights[j-weight] + weight
   225  				combination[j] = append(combination[j-weight], instances[i])
   226  			} else if newWeight == weights[j] {
   227  				// if can reach same weight, find a combination with less instances
   228  				if len(combination[j-weight])+1 < len(combination[j]) {
   229  					combination[j] = append(combination[j-weight], instances[i])
   230  				}
   231  			}
   232  		}
   233  	}
   234  
   235  	for i := targetWeight; i <= totalWeight; i++ {
   236  		if weights[i] >= targetWeight {
   237  			return combination[i], targetWeight - weights[i]
   238  		}
   239  	}
   240  
   241  	panic("should never reach here")
   242  }
   243  
   244  func buildIsolationGroupMap(candidates []placement.Instance) map[string][]placement.Instance {
   245  	result := make(map[string][]placement.Instance, len(candidates))
   246  	for _, instance := range candidates {
   247  		if _, exist := result[instance.IsolationGroup()]; !exist {
   248  			result[instance.IsolationGroup()] = make([]placement.Instance, 0)
   249  		}
   250  		result[instance.IsolationGroup()] = append(result[instance.IsolationGroup()], instance)
   251  	}
   252  	return result
   253  }
   254  
   255  func selectSingleCandidate(
   256  	candidates []placement.Instance,
   257  	p placement.Placement,
   258  ) (placement.Instance, error) {
   259  	candidateGroups := buildIsolationGroupMap(candidates)
   260  	existingGroups := buildIsolationGroupMap(p.Instances())
   261  
   262  	// If there is an isolation group not in the current placement, prefer the isolation group.
   263  	for r, instances := range candidateGroups {
   264  		if _, exist := existingGroups[r]; !exist {
   265  			// All the isolation groups have at least 1 instance.
   266  			return instances[0], nil
   267  		}
   268  	}
   269  
   270  	// Otherwise sort the isolation groups in the current placement
   271  	// by capacity and find a instance from least sized isolation group.
   272  	groups := make(sortableValues, 0, len(existingGroups))
   273  	for group, instances := range existingGroups {
   274  		weight := 0
   275  		for _, i := range instances {
   276  			weight += int(i.Weight())
   277  		}
   278  		groups = append(groups, sortableValue{value: group, weight: weight})
   279  	}
   280  	sort.Sort(groups)
   281  
   282  	for _, group := range groups {
   283  		if i, exist := candidateGroups[group.value.(string)]; exist {
   284  			for _, instance := range i {
   285  				return instance, nil
   286  			}
   287  		}
   288  	}
   289  
   290  	// no instance in the candidate instances can be added to the placement
   291  	return nil, errNoValidInstance
   292  }
   293  
   294  type sortableValue struct {
   295  	value  interface{}
   296  	weight int
   297  }
   298  
   299  type sortableValues []sortableValue
   300  
   301  func (values sortableValues) Len() int {
   302  	return len(values)
   303  }
   304  
   305  func (values sortableValues) Less(i, j int) bool {
   306  	return values[i].weight < values[j].weight
   307  }
   308  
   309  func (values sortableValues) Swap(i, j int) {
   310  	values[i], values[j] = values[j], values[i]
   311  }