in cluster-autoscaler/core/scale_down.go [460:632]
func (sd *ScaleDown) UpdateUnneededNodes(
destinationNodes []*apiv1.Node,
scaleDownCandidates []*apiv1.Node,
timestamp time.Time,
pdbs []*policyv1.PodDisruptionBudget,
) errors.AutoscalerError {
// Only scheduled non expendable pods and pods waiting for lower priority pods preemption can prevent node delete.
// Extract cluster state from snapshot for initial analysis
allNodeInfos, err := sd.context.ClusterSnapshot.NodeInfos().List()
if err != nil {
// This should never happen, List() returns err only because scheduler interface requires it.
return errors.ToAutoscalerError(errors.InternalError, err)
}
sd.updateUnremovableNodes(timestamp)
skipped := 0
utilizationMap := make(map[string]simulator.UtilizationInfo)
currentlyUnneededNodeNames := make([]string, 0, len(scaleDownCandidates))
// Phase1 - look at the nodes utilization. Calculate the utilization
// only for the managed nodes.
for _, node := range scaleDownCandidates {
nodeInfo, err := sd.context.ClusterSnapshot.NodeInfos().Get(node.Name)
if err != nil {
klog.Errorf("Can't retrieve scale-down candidate %s from snapshot, err: %v", node.Name, err)
sd.addUnremovableNodeReason(node, simulator.UnexpectedError)
continue
}
reason, utilInfo := sd.checkNodeUtilization(timestamp, node, nodeInfo)
if utilInfo != nil {
utilizationMap[node.Name] = *utilInfo
}
if reason != simulator.NoReason {
// For logging purposes.
if reason == simulator.RecentlyUnremovable {
skipped++
}
sd.addUnremovableNodeReason(node, reason)
continue
}
currentlyUnneededNodeNames = append(currentlyUnneededNodeNames, node.Name)
}
if skipped > 0 {
klog.V(1).Infof("Scale-down calculation: ignoring %v nodes unremovable in the last %v", skipped, sd.context.AutoscalingOptions.UnremovableNodeRecheckTimeout)
}
emptyNodesToRemove := sd.getEmptyNodesToRemoveNoResourceLimits(currentlyUnneededNodeNames, timestamp)
emptyNodes := make(map[string]bool)
for _, empty := range emptyNodesToRemove {
emptyNodes[empty.Node.Name] = true
}
currentlyUnneededNonEmptyNodes := make([]string, 0, len(currentlyUnneededNodeNames))
for _, node := range currentlyUnneededNodeNames {
if !emptyNodes[node] {
currentlyUnneededNonEmptyNodes = append(currentlyUnneededNonEmptyNodes, node)
}
}
// Phase2 - check which nodes can be probably removed using fast drain.
currentCandidates, currentNonCandidates := sd.chooseCandidates(currentlyUnneededNonEmptyNodes)
destinations := make([]string, 0, len(destinationNodes))
for _, destinationNode := range destinationNodes {
destinations = append(destinations, destinationNode.Name)
}
// Look for nodes to remove in the current candidates
nodesToRemove, unremovable, newHints, simulatorErr := simulator.FindNodesToRemove(
currentCandidates,
destinations,
sd.context.ListerRegistry,
sd.context.ClusterSnapshot,
sd.context.PredicateChecker,
sd.podLocationHints,
sd.usageTracker,
timestamp,
pdbs)
if simulatorErr != nil {
return sd.markSimulationError(simulatorErr, timestamp)
}
additionalCandidatesCount := sd.context.ScaleDownNonEmptyCandidatesCount - len(nodesToRemove)
if additionalCandidatesCount > len(currentNonCandidates) {
additionalCandidatesCount = len(currentNonCandidates)
}
// Limit the additional candidates pool size for better performance.
additionalCandidatesPoolSize := int(math.Ceil(float64(len(allNodeInfos)) * sd.context.ScaleDownCandidatesPoolRatio))
if additionalCandidatesPoolSize < sd.context.ScaleDownCandidatesPoolMinCount {
additionalCandidatesPoolSize = sd.context.ScaleDownCandidatesPoolMinCount
}
if additionalCandidatesPoolSize > len(currentNonCandidates) {
additionalCandidatesPoolSize = len(currentNonCandidates)
}
if additionalCandidatesCount > 0 {
// Look for additional nodes to remove among the rest of nodes.
klog.V(3).Infof("Finding additional %v candidates for scale down.", additionalCandidatesCount)
additionalNodesToRemove, additionalUnremovable, additionalNewHints, simulatorErr :=
simulator.FindNodesToRemove(
currentNonCandidates[:additionalCandidatesPoolSize],
destinations,
sd.context.ListerRegistry,
sd.context.ClusterSnapshot,
sd.context.PredicateChecker,
sd.podLocationHints,
sd.usageTracker,
timestamp,
pdbs)
if simulatorErr != nil {
return sd.markSimulationError(simulatorErr, timestamp)
}
if len(additionalNodesToRemove) > additionalCandidatesCount {
additionalNodesToRemove = additionalNodesToRemove[:additionalCandidatesCount]
}
nodesToRemove = append(nodesToRemove, additionalNodesToRemove...)
unremovable = append(unremovable, additionalUnremovable...)
for key, value := range additionalNewHints {
newHints[key] = value
}
}
for _, empty := range emptyNodesToRemove {
nodesToRemove = append(nodesToRemove, simulator.NodeToBeRemoved{Node: empty.Node, PodsToReschedule: []*apiv1.Pod{}})
}
// Update the timestamp map.
result := make(map[string]time.Time)
unneededNodesList := make([]*apiv1.Node, 0, len(nodesToRemove))
for _, node := range nodesToRemove {
name := node.Node.Name
unneededNodesList = append(unneededNodesList, node.Node)
if val, found := sd.unneededNodes[name]; !found {
result[name] = timestamp
} else {
result[name] = val
}
}
// Add nodes to unremovable map
if len(unremovable) > 0 {
unremovableTimeout := timestamp.Add(sd.context.AutoscalingOptions.UnremovableNodeRecheckTimeout)
for _, unremovableNode := range unremovable {
sd.unremovableNodes[unremovableNode.Node.Name] = unremovableTimeout
sd.addUnremovableNode(unremovableNode)
}
klog.V(1).Infof("%v nodes found to be unremovable in simulation, will re-check them at %v", len(unremovable), unremovableTimeout)
}
// This method won't always check all nodes, so let's give a generic reason for all nodes that weren't checked.
for _, node := range scaleDownCandidates {
_, unremovableReasonProvided := sd.unremovableNodeReasons[node.Name]
_, unneeded := result[node.Name]
if !unneeded && !unremovableReasonProvided {
sd.addUnremovableNodeReason(node, simulator.NotUnneededOtherReason)
}
}
// Update state and metrics
sd.unneededNodesList = unneededNodesList
sd.unneededNodes = result
sd.podLocationHints = newHints
sd.nodeUtilizationMap = utilizationMap
sd.clusterStateRegistry.UpdateScaleDownCandidates(sd.unneededNodesList, timestamp)
metrics.UpdateUnneededNodesCount(len(sd.unneededNodesList))
return nil
}