...

Source file src/k8s.io/kubernetes/pkg/scheduler/framework/plugins/imagelocality/image_locality.go

Documentation: k8s.io/kubernetes/pkg/scheduler/framework/plugins/imagelocality

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package imagelocality
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	v1 "k8s.io/api/core/v1"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	"k8s.io/kubernetes/pkg/scheduler/framework"
    27  	"k8s.io/kubernetes/pkg/scheduler/framework/plugins/names"
    28  )
    29  
    30  // The two thresholds are used as bounds for the image score range. They correspond to a reasonable size range for
    31  // container images compressed and stored in registries; 90%ile of images on dockerhub drops into this range.
    32  const (
    33  	mb                    int64 = 1024 * 1024
    34  	minThreshold          int64 = 23 * mb
    35  	maxContainerThreshold int64 = 1000 * mb
    36  )
    37  
    38  // ImageLocality is a score plugin that favors nodes that already have requested pod container's images.
    39  type ImageLocality struct {
    40  	handle framework.Handle
    41  }
    42  
    43  var _ framework.ScorePlugin = &ImageLocality{}
    44  
    45  // Name is the name of the plugin used in the plugin registry and configurations.
    46  const Name = names.ImageLocality
    47  
    48  // Name returns name of the plugin. It is used in logs, etc.
    49  func (pl *ImageLocality) Name() string {
    50  	return Name
    51  }
    52  
    53  // Score invoked at the score extension point.
    54  func (pl *ImageLocality) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
    55  	nodeInfo, err := pl.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
    56  	if err != nil {
    57  		return 0, framework.AsStatus(fmt.Errorf("getting node %q from Snapshot: %w", nodeName, err))
    58  	}
    59  
    60  	nodeInfos, err := pl.handle.SnapshotSharedLister().NodeInfos().List()
    61  	if err != nil {
    62  		return 0, framework.AsStatus(err)
    63  	}
    64  	totalNumNodes := len(nodeInfos)
    65  
    66  	imageScores := sumImageScores(nodeInfo, pod, totalNumNodes)
    67  	score := calculatePriority(imageScores, len(pod.Spec.InitContainers)+len(pod.Spec.Containers))
    68  
    69  	return score, nil
    70  }
    71  
    72  // ScoreExtensions of the Score plugin.
    73  func (pl *ImageLocality) ScoreExtensions() framework.ScoreExtensions {
    74  	return nil
    75  }
    76  
    77  // New initializes a new plugin and returns it.
    78  func New(_ context.Context, _ runtime.Object, h framework.Handle) (framework.Plugin, error) {
    79  	return &ImageLocality{handle: h}, nil
    80  }
    81  
    82  // calculatePriority returns the priority of a node. Given the sumScores of requested images on the node, the node's
    83  // priority is obtained by scaling the maximum priority value with a ratio proportional to the sumScores.
    84  func calculatePriority(sumScores int64, numContainers int) int64 {
    85  	maxThreshold := maxContainerThreshold * int64(numContainers)
    86  	if sumScores < minThreshold {
    87  		sumScores = minThreshold
    88  	} else if sumScores > maxThreshold {
    89  		sumScores = maxThreshold
    90  	}
    91  
    92  	return framework.MaxNodeScore * (sumScores - minThreshold) / (maxThreshold - minThreshold)
    93  }
    94  
    95  // sumImageScores returns the sum of image scores of all the containers that are already on the node.
    96  // Each image receives a raw score of its size, scaled by scaledImageScore. The raw scores are later used to calculate
    97  // the final score.
    98  func sumImageScores(nodeInfo *framework.NodeInfo, pod *v1.Pod, totalNumNodes int) int64 {
    99  	var sum int64
   100  	for _, container := range pod.Spec.InitContainers {
   101  		if state, ok := nodeInfo.ImageStates[normalizedImageName(container.Image)]; ok {
   102  			sum += scaledImageScore(state, totalNumNodes)
   103  		}
   104  	}
   105  	for _, container := range pod.Spec.Containers {
   106  		if state, ok := nodeInfo.ImageStates[normalizedImageName(container.Image)]; ok {
   107  			sum += scaledImageScore(state, totalNumNodes)
   108  		}
   109  	}
   110  	return sum
   111  }
   112  
   113  // scaledImageScore returns an adaptively scaled score for the given state of an image.
   114  // The size of the image is used as the base score, scaled by a factor which considers how much nodes the image has "spread" to.
   115  // This heuristic aims to mitigate the undesirable "node heating problem", i.e., pods get assigned to the same or
   116  // a few nodes due to image locality.
   117  func scaledImageScore(imageState *framework.ImageStateSummary, totalNumNodes int) int64 {
   118  	spread := float64(imageState.NumNodes) / float64(totalNumNodes)
   119  	return int64(float64(imageState.Size) * spread)
   120  }
   121  
   122  // normalizedImageName returns the CRI compliant name for a given image.
   123  // TODO: cover the corner cases of missed matches, e.g,
   124  // 1. Using Docker as runtime and docker.io/library/test:tag in pod spec, but only test:tag will present in node status
   125  // 2. Using the implicit registry, i.e., test:tag or library/test:tag in pod spec but only docker.io/library/test:tag
   126  // in node status; note that if users consistently use one registry format, this should not happen.
   127  func normalizedImageName(name string) string {
   128  	if strings.LastIndex(name, ":") <= strings.LastIndex(name, "/") {
   129  		name = name + ":latest"
   130  	}
   131  	return name
   132  }
   133  

View as plain text