k8s.io/apiserver@v0.31.1/pkg/util/flowcontrol/request/mutating_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 "time" 23 24 apirequest "k8s.io/apiserver/pkg/endpoints/request" 25 "k8s.io/apiserver/pkg/util/flowcontrol/metrics" 26 ) 27 28 func newMutatingWorkEstimator(countFn watchCountGetterFunc, config *WorkEstimatorConfig, maxSeatsFn maxSeatsFunc) WorkEstimatorFunc { 29 estimator := &mutatingWorkEstimator{ 30 config: config, 31 countFn: countFn, 32 maxSeatsFn: maxSeatsFn, 33 } 34 return estimator.estimate 35 } 36 37 type mutatingWorkEstimator struct { 38 config *WorkEstimatorConfig 39 countFn watchCountGetterFunc 40 maxSeatsFn maxSeatsFunc 41 } 42 43 func (e *mutatingWorkEstimator) estimate(r *http.Request, flowSchemaName, priorityLevelName string) WorkEstimate { 44 minSeats := e.config.MinimumSeats 45 maxSeats := e.maxSeatsFn(priorityLevelName) 46 if maxSeats == 0 || maxSeats > e.config.MaximumSeatsLimit { 47 maxSeats = e.config.MaximumSeatsLimit 48 } 49 50 // TODO(wojtekt): Remove once we tune the algorithm to not fail 51 // scalability tests. 52 if !e.config.Enabled { 53 return WorkEstimate{ 54 InitialSeats: minSeats, 55 } 56 } 57 58 requestInfo, ok := apirequest.RequestInfoFrom(r.Context()) 59 if !ok { 60 // no RequestInfo should never happen, but to be on the safe side 61 // let's return a large value. 62 return WorkEstimate{ 63 InitialSeats: minSeats, 64 FinalSeats: maxSeats, 65 AdditionalLatency: e.config.eventAdditionalDuration(), 66 } 67 } 68 69 if isRequestExemptFromWatchEvents(requestInfo) { 70 return WorkEstimate{ 71 InitialSeats: minSeats, 72 FinalSeats: 0, 73 AdditionalLatency: time.Duration(0), 74 } 75 } 76 77 watchCount := e.countFn(requestInfo) 78 metrics.ObserveWatchCount(r.Context(), priorityLevelName, flowSchemaName, watchCount) 79 80 // The cost of the request associated with the watchers of that event 81 // consists of three parts: 82 // - cost of going through the event change logic 83 // - cost of serialization of the event 84 // - cost of processing an event object for each watcher (e.g. filtering, 85 // sending data over network) 86 // We're starting simple to get some operational experience with it and 87 // we will work on tuning the algorithm later. Given that the actual work 88 // associated with processing watch events is happening in multiple 89 // goroutines (proportional to the number of watchers) that are all 90 // resumed at once, as a starting point we assume that each such goroutine 91 // is taking 1/Nth of a seat for M milliseconds. 92 // We allow the accounting of that work in P&F to be reshaped into another 93 // rectangle of equal area for practical reasons. 94 var finalSeats uint64 95 var additionalLatency time.Duration 96 97 // TODO: Make this unconditional after we tune the algorithm better. 98 // Technically, there is an overhead connected to processing an event after 99 // the request finishes even if there is a small number of watches. 100 // However, until we tune the estimation we want to stay on the safe side 101 // an avoid introducing additional latency for almost every single request. 102 if watchCount >= int(e.config.WatchesPerSeat) { 103 // TODO: As described in the KEP, we should take into account that not all 104 // events are equal and try to estimate the cost of a single event based on 105 // some historical data about size of events. 106 finalSeats = uint64(math.Ceil(float64(watchCount) / e.config.WatchesPerSeat)) 107 finalWork := SeatsTimesDuration(float64(finalSeats), e.config.eventAdditionalDuration()) 108 109 // While processing individual events is highly parallel, 110 // the design/implementation of P&F has a couple limitations that 111 // make using this assumption in the P&F implementation very 112 // inefficient because: 113 // - we reserve max(initialSeats, finalSeats) for time of executing 114 // both phases of the request 115 // - even more importantly, when a given `wide` request is the one to 116 // be dispatched, we are not dispatching any other request until 117 // we accumulate enough seats to dispatch the nominated one, even 118 // if currently unoccupied seats would allow for dispatching some 119 // other requests in the meantime 120 // As a consequence of these, the wider the request, the more capacity 121 // will effectively be blocked and unused during dispatching and 122 // executing this request. 123 // 124 // To mitigate the impact of it, we're capping the maximum number of 125 // seats that can be assigned to a given request. Thanks to it: 126 // 1) we reduce the amount of seat-seconds that are "wasted" during 127 // dispatching and executing initial phase of the request 128 // 2) we are not changing the finalWork estimate - just potentially 129 // reshaping it to be narrower and longer. As long as the maximum 130 // seats setting will prevent dispatching too many requests at once 131 // to prevent overloading kube-apiserver (and/or etcd or the VM or 132 // a physical machine it is running on), we believe the relaxed 133 // version should be good enough to achieve the P&F goals. 134 // 135 // TODO: Confirm that the current cap of maximumSeats allow us to 136 // achieve the above. 137 if finalSeats > maxSeats { 138 finalSeats = maxSeats 139 } 140 additionalLatency = finalWork.DurationPerSeat(float64(finalSeats)) 141 } 142 143 return WorkEstimate{ 144 InitialSeats: 1, 145 FinalSeats: finalSeats, 146 AdditionalLatency: additionalLatency, 147 } 148 } 149 150 func isRequestExemptFromWatchEvents(requestInfo *apirequest.RequestInfo) bool { 151 // Creating token for service account does not produce any event, 152 // but still serviceaccounts can have multiple watchers. 153 if requestInfo.Resource == "serviceaccounts" && requestInfo.Subresource == "token" { 154 return true 155 } 156 return false 157 }