k8s.io/apiserver@v0.31.1/pkg/util/flowcontrol/request/list_work_estimator.go (about)

     1  /*
     2  Copyright 2021 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 request
    18  
    19  import (
    20  	"math"
    21  	"net/http"
    22  	"net/url"
    23  
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/runtime/schema"
    26  	apirequest "k8s.io/apiserver/pkg/endpoints/request"
    27  	"k8s.io/apiserver/pkg/features"
    28  	"k8s.io/apiserver/pkg/storage"
    29  	etcdfeature "k8s.io/apiserver/pkg/storage/feature"
    30  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    31  	"k8s.io/klog/v2"
    32  )
    33  
    34  func newListWorkEstimator(countFn objectCountGetterFunc, config *WorkEstimatorConfig, maxSeatsFn maxSeatsFunc) WorkEstimatorFunc {
    35  	estimator := &listWorkEstimator{
    36  		config:        config,
    37  		countGetterFn: countFn,
    38  		maxSeatsFn:    maxSeatsFn,
    39  	}
    40  	return estimator.estimate
    41  }
    42  
    43  type listWorkEstimator struct {
    44  	config        *WorkEstimatorConfig
    45  	countGetterFn objectCountGetterFunc
    46  	maxSeatsFn    maxSeatsFunc
    47  }
    48  
    49  func (e *listWorkEstimator) estimate(r *http.Request, flowSchemaName, priorityLevelName string) WorkEstimate {
    50  	minSeats := e.config.MinimumSeats
    51  	maxSeats := e.maxSeatsFn(priorityLevelName)
    52  	if maxSeats == 0 || maxSeats > e.config.MaximumSeatsLimit {
    53  		maxSeats = e.config.MaximumSeatsLimit
    54  	}
    55  
    56  	requestInfo, ok := apirequest.RequestInfoFrom(r.Context())
    57  	if !ok {
    58  		// no RequestInfo should never happen, but to be on the safe side
    59  		// let's return maximumSeats
    60  		return WorkEstimate{InitialSeats: maxSeats}
    61  	}
    62  
    63  	if requestInfo.Name != "" {
    64  		// Requests with metadata.name specified are usually executed as get
    65  		// requests in storage layer so their width should be 1.
    66  		// Example of such list requests:
    67  		// /apis/certificates.k8s.io/v1/certificatesigningrequests?fieldSelector=metadata.name%3Dcsr-xxs4m
    68  		// /api/v1/namespaces/test/configmaps?fieldSelector=metadata.name%3Dbig-deployment-1&limit=500&resourceVersion=0
    69  		return WorkEstimate{InitialSeats: minSeats}
    70  	}
    71  
    72  	query := r.URL.Query()
    73  	listOptions := metav1.ListOptions{}
    74  	if err := metav1.Convert_url_Values_To_v1_ListOptions(&query, &listOptions, nil); err != nil {
    75  		klog.ErrorS(err, "Failed to convert options while estimating work for the list request")
    76  
    77  		// This request is destined to fail in the validation layer,
    78  		// return maximumSeats for this request to be consistent.
    79  		return WorkEstimate{InitialSeats: maxSeats}
    80  	}
    81  
    82  	// For watch requests, we want to adjust the cost only if they explicitly request
    83  	// sending initial events.
    84  	if requestInfo.Verb == "watch" {
    85  		if listOptions.SendInitialEvents == nil || !*listOptions.SendInitialEvents {
    86  			return WorkEstimate{InitialSeats: e.config.MinimumSeats}
    87  		}
    88  	}
    89  
    90  	isListFromCache := requestInfo.Verb == "watch" || !shouldListFromStorage(query, &listOptions)
    91  
    92  	numStored, err := e.countGetterFn(key(requestInfo))
    93  	switch {
    94  	case err == ObjectCountStaleErr:
    95  		// object count going stale is indicative of degradation, so we should
    96  		// be conservative here and allocate maximum seats to this list request.
    97  		// NOTE: if a CRD is removed, its count will go stale first and then the
    98  		// pruner will eventually remove the CRD from the cache.
    99  		return WorkEstimate{InitialSeats: maxSeats}
   100  	case err == ObjectCountNotFoundErr:
   101  		// there are multiple scenarios in which we can see this error:
   102  		//  a. the type is truly unknown, a typo on the caller's part.
   103  		//  b. the count has gone stale for too long and the pruner
   104  		//     has removed the type from the cache.
   105  		//  c. the type is an aggregated resource that is served by a
   106  		//     different apiserver (thus its object count is not updated)
   107  		// we don't have a way to distinguish between those situations.
   108  		// However, in case c, the request is delegated to a different apiserver,
   109  		// and thus its cost for our server is minimal. To avoid the situation
   110  		// when aggregated API calls are overestimated, we allocate the minimum
   111  		// possible seats (see #109106 as an example when being more conservative
   112  		// led to problems).
   113  		return WorkEstimate{InitialSeats: minSeats}
   114  	case err != nil:
   115  		// we should never be here since Get returns either ObjectCountStaleErr or
   116  		// ObjectCountNotFoundErr, return maximumSeats to be on the safe side.
   117  		klog.ErrorS(err, "Unexpected error from object count tracker")
   118  		return WorkEstimate{InitialSeats: maxSeats}
   119  	}
   120  
   121  	limit := numStored
   122  	if listOptions.Limit > 0 && listOptions.Limit < numStored {
   123  		limit = listOptions.Limit
   124  	}
   125  
   126  	var estimatedObjectsToBeProcessed int64
   127  
   128  	switch {
   129  	case isListFromCache:
   130  		// TODO: For resources that implement indexes at the watchcache level,
   131  		//  we need to adjust the cost accordingly
   132  		estimatedObjectsToBeProcessed = numStored
   133  	case listOptions.FieldSelector != "" || listOptions.LabelSelector != "":
   134  		estimatedObjectsToBeProcessed = numStored + limit
   135  	default:
   136  		estimatedObjectsToBeProcessed = 2 * limit
   137  	}
   138  
   139  	// for now, our rough estimate is to allocate one seat to each 100 obejcts that
   140  	// will be processed by the list request.
   141  	// we will come up with a different formula for the transformation function and/or
   142  	// fine tune this number in future iteratons.
   143  	seats := uint64(math.Ceil(float64(estimatedObjectsToBeProcessed) / e.config.ObjectsPerSeat))
   144  
   145  	// make sure we never return a seat of zero
   146  	if seats < minSeats {
   147  		seats = minSeats
   148  	}
   149  	if seats > maxSeats {
   150  		seats = maxSeats
   151  	}
   152  	return WorkEstimate{InitialSeats: seats}
   153  }
   154  
   155  func key(requestInfo *apirequest.RequestInfo) string {
   156  	groupResource := &schema.GroupResource{
   157  		Group:    requestInfo.APIGroup,
   158  		Resource: requestInfo.Resource,
   159  	}
   160  	return groupResource.String()
   161  }
   162  
   163  // NOTICE: Keep in sync with shouldDelegateList function in
   164  //
   165  //	staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go
   166  func shouldListFromStorage(query url.Values, opts *metav1.ListOptions) bool {
   167  	resourceVersion := opts.ResourceVersion
   168  	match := opts.ResourceVersionMatch
   169  	consistentListFromCacheEnabled := utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache)
   170  	requestWatchProgressSupported := etcdfeature.DefaultFeatureSupportChecker.Supports(storage.RequestWatchProgress)
   171  
   172  	// Serve consistent reads from storage if ConsistentListFromCache is disabled
   173  	consistentReadFromStorage := resourceVersion == "" && !(consistentListFromCacheEnabled && requestWatchProgressSupported)
   174  	// Watch cache doesn't support continuations, so serve them from etcd.
   175  	hasContinuation := len(opts.Continue) > 0
   176  	// Watch cache only supports ResourceVersionMatchNotOlderThan (default).
   177  	// see https://kubernetes.io/docs/reference/using-api/api-concepts/#semantics-for-get-and-list
   178  	isLegacyExactMatch := opts.Limit > 0 && match == "" && len(resourceVersion) > 0 && resourceVersion != "0"
   179  	unsupportedMatch := match != "" && match != metav1.ResourceVersionMatchNotOlderThan || isLegacyExactMatch
   180  
   181  	return consistentReadFromStorage || hasContinuation || unsupportedMatch
   182  }