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 }