replica_calculator.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package podautoscaler
  14. import (
  15. "fmt"
  16. "math"
  17. "time"
  18. autoscaling "k8s.io/api/autoscaling/v2beta2"
  19. v1 "k8s.io/api/core/v1"
  20. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  21. "k8s.io/apimachinery/pkg/labels"
  22. "k8s.io/apimachinery/pkg/util/sets"
  23. corelisters "k8s.io/client-go/listers/core/v1"
  24. podutil "k8s.io/kubernetes/pkg/api/v1/pod"
  25. metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
  26. )
  27. const (
  28. // defaultTestingTolerance is default value for calculating when to
  29. // scale up/scale down.
  30. defaultTestingTolerance = 0.1
  31. defaultTestingCpuInitializationPeriod = 2 * time.Minute
  32. defaultTestingDelayOfInitialReadinessStatus = 10 * time.Second
  33. )
  34. type ReplicaCalculator struct {
  35. metricsClient metricsclient.MetricsClient
  36. podLister corelisters.PodLister
  37. tolerance float64
  38. cpuInitializationPeriod time.Duration
  39. delayOfInitialReadinessStatus time.Duration
  40. }
  41. func NewReplicaCalculator(metricsClient metricsclient.MetricsClient, podLister corelisters.PodLister, tolerance float64, cpuInitializationPeriod, delayOfInitialReadinessStatus time.Duration) *ReplicaCalculator {
  42. return &ReplicaCalculator{
  43. metricsClient: metricsClient,
  44. podLister: podLister,
  45. tolerance: tolerance,
  46. cpuInitializationPeriod: cpuInitializationPeriod,
  47. delayOfInitialReadinessStatus: delayOfInitialReadinessStatus,
  48. }
  49. }
  50. // GetResourceReplicas calculates the desired replica count based on a target resource utilization percentage
  51. // of the given resource for pods matching the given selector in the given namespace, and the current replica count
  52. func (c *ReplicaCalculator) GetResourceReplicas(currentReplicas int32, targetUtilization int32, resource v1.ResourceName, namespace string, selector labels.Selector) (replicaCount int32, utilization int32, rawUtilization int64, timestamp time.Time, err error) {
  53. metrics, timestamp, err := c.metricsClient.GetResourceMetric(resource, namespace, selector)
  54. if err != nil {
  55. return 0, 0, 0, time.Time{}, fmt.Errorf("unable to get metrics for resource %s: %v", resource, err)
  56. }
  57. podList, err := c.podLister.Pods(namespace).List(selector)
  58. if err != nil {
  59. return 0, 0, 0, time.Time{}, fmt.Errorf("unable to get pods while calculating replica count: %v", err)
  60. }
  61. itemsLen := len(podList)
  62. if itemsLen == 0 {
  63. return 0, 0, 0, time.Time{}, fmt.Errorf("no pods returned by selector while calculating replica count")
  64. }
  65. readyPodCount, ignoredPods, missingPods := groupPods(podList, metrics, resource, c.cpuInitializationPeriod, c.delayOfInitialReadinessStatus)
  66. removeMetricsForPods(metrics, ignoredPods)
  67. requests, err := calculatePodRequests(podList, resource)
  68. if err != nil {
  69. return 0, 0, 0, time.Time{}, err
  70. }
  71. if len(metrics) == 0 {
  72. return 0, 0, 0, time.Time{}, fmt.Errorf("did not receive metrics for any ready pods")
  73. }
  74. usageRatio, utilization, rawUtilization, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization)
  75. if err != nil {
  76. return 0, 0, 0, time.Time{}, err
  77. }
  78. rebalanceIgnored := len(ignoredPods) > 0 && usageRatio > 1.0
  79. if !rebalanceIgnored && len(missingPods) == 0 {
  80. if math.Abs(1.0-usageRatio) <= c.tolerance {
  81. // return the current replicas if the change would be too small
  82. return currentReplicas, utilization, rawUtilization, timestamp, nil
  83. }
  84. // if we don't have any unready or missing pods, we can calculate the new replica count now
  85. return int32(math.Ceil(usageRatio * float64(readyPodCount))), utilization, rawUtilization, timestamp, nil
  86. }
  87. if len(missingPods) > 0 {
  88. if usageRatio < 1.0 {
  89. // on a scale-down, treat missing pods as using 100% of the resource request
  90. for podName := range missingPods {
  91. metrics[podName] = metricsclient.PodMetric{Value: requests[podName]}
  92. }
  93. } else if usageRatio > 1.0 {
  94. // on a scale-up, treat missing pods as using 0% of the resource request
  95. for podName := range missingPods {
  96. metrics[podName] = metricsclient.PodMetric{Value: 0}
  97. }
  98. }
  99. }
  100. if rebalanceIgnored {
  101. // on a scale-up, treat unready pods as using 0% of the resource request
  102. for podName := range ignoredPods {
  103. metrics[podName] = metricsclient.PodMetric{Value: 0}
  104. }
  105. }
  106. // re-run the utilization calculation with our new numbers
  107. newUsageRatio, _, _, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization)
  108. if err != nil {
  109. return 0, utilization, rawUtilization, time.Time{}, err
  110. }
  111. if math.Abs(1.0-newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
  112. // return the current replicas if the change would be too small,
  113. // or if the new usage ratio would cause a change in scale direction
  114. return currentReplicas, utilization, rawUtilization, timestamp, nil
  115. }
  116. // return the result, where the number of replicas considered is
  117. // however many replicas factored into our calculation
  118. return int32(math.Ceil(newUsageRatio * float64(len(metrics)))), utilization, rawUtilization, timestamp, nil
  119. }
  120. // GetRawResourceReplicas calculates the desired replica count based on a target resource utilization (as a raw milli-value)
  121. // for pods matching the given selector in the given namespace, and the current replica count
  122. func (c *ReplicaCalculator) GetRawResourceReplicas(currentReplicas int32, targetUtilization int64, resource v1.ResourceName, namespace string, selector labels.Selector) (replicaCount int32, utilization int64, timestamp time.Time, err error) {
  123. metrics, timestamp, err := c.metricsClient.GetResourceMetric(resource, namespace, selector)
  124. if err != nil {
  125. return 0, 0, time.Time{}, fmt.Errorf("unable to get metrics for resource %s: %v", resource, err)
  126. }
  127. replicaCount, utilization, err = c.calcPlainMetricReplicas(metrics, currentReplicas, targetUtilization, namespace, selector, resource)
  128. return replicaCount, utilization, timestamp, err
  129. }
  130. // GetMetricReplicas calculates the desired replica count based on a target metric utilization
  131. // (as a milli-value) for pods matching the given selector in the given namespace, and the
  132. // current replica count
  133. func (c *ReplicaCalculator) GetMetricReplicas(currentReplicas int32, targetUtilization int64, metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (replicaCount int32, utilization int64, timestamp time.Time, err error) {
  134. metrics, timestamp, err := c.metricsClient.GetRawMetric(metricName, namespace, selector, metricSelector)
  135. if err != nil {
  136. return 0, 0, time.Time{}, fmt.Errorf("unable to get metric %s: %v", metricName, err)
  137. }
  138. replicaCount, utilization, err = c.calcPlainMetricReplicas(metrics, currentReplicas, targetUtilization, namespace, selector, v1.ResourceName(""))
  139. return replicaCount, utilization, timestamp, err
  140. }
  141. // calcPlainMetricReplicas calculates the desired replicas for plain (i.e. non-utilization percentage) metrics.
  142. func (c *ReplicaCalculator) calcPlainMetricReplicas(metrics metricsclient.PodMetricsInfo, currentReplicas int32, targetUtilization int64, namespace string, selector labels.Selector, resource v1.ResourceName) (replicaCount int32, utilization int64, err error) {
  143. podList, err := c.podLister.Pods(namespace).List(selector)
  144. if err != nil {
  145. return 0, 0, fmt.Errorf("unable to get pods while calculating replica count: %v", err)
  146. }
  147. if len(podList) == 0 {
  148. return 0, 0, fmt.Errorf("no pods returned by selector while calculating replica count")
  149. }
  150. readyPodCount, ignoredPods, missingPods := groupPods(podList, metrics, resource, c.cpuInitializationPeriod, c.delayOfInitialReadinessStatus)
  151. removeMetricsForPods(metrics, ignoredPods)
  152. if len(metrics) == 0 {
  153. return 0, 0, fmt.Errorf("did not receive metrics for any ready pods")
  154. }
  155. usageRatio, utilization := metricsclient.GetMetricUtilizationRatio(metrics, targetUtilization)
  156. rebalanceIgnored := len(ignoredPods) > 0 && usageRatio > 1.0
  157. if !rebalanceIgnored && len(missingPods) == 0 {
  158. if math.Abs(1.0-usageRatio) <= c.tolerance {
  159. // return the current replicas if the change would be too small
  160. return currentReplicas, utilization, nil
  161. }
  162. // if we don't have any unready or missing pods, we can calculate the new replica count now
  163. return int32(math.Ceil(usageRatio * float64(readyPodCount))), utilization, nil
  164. }
  165. if len(missingPods) > 0 {
  166. if usageRatio < 1.0 {
  167. // on a scale-down, treat missing pods as using 100% of the resource request
  168. for podName := range missingPods {
  169. metrics[podName] = metricsclient.PodMetric{Value: targetUtilization}
  170. }
  171. } else {
  172. // on a scale-up, treat missing pods as using 0% of the resource request
  173. for podName := range missingPods {
  174. metrics[podName] = metricsclient.PodMetric{Value: 0}
  175. }
  176. }
  177. }
  178. if rebalanceIgnored {
  179. // on a scale-up, treat unready pods as using 0% of the resource request
  180. for podName := range ignoredPods {
  181. metrics[podName] = metricsclient.PodMetric{Value: 0}
  182. }
  183. }
  184. // re-run the utilization calculation with our new numbers
  185. newUsageRatio, _ := metricsclient.GetMetricUtilizationRatio(metrics, targetUtilization)
  186. if math.Abs(1.0-newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
  187. // return the current replicas if the change would be too small,
  188. // or if the new usage ratio would cause a change in scale direction
  189. return currentReplicas, utilization, nil
  190. }
  191. // return the result, where the number of replicas considered is
  192. // however many replicas factored into our calculation
  193. return int32(math.Ceil(newUsageRatio * float64(len(metrics)))), utilization, nil
  194. }
  195. // GetObjectMetricReplicas calculates the desired replica count based on a target metric utilization (as a milli-value)
  196. // for the given object in the given namespace, and the current replica count.
  197. func (c *ReplicaCalculator) GetObjectMetricReplicas(currentReplicas int32, targetUtilization int64, metricName string, namespace string, objectRef *autoscaling.CrossVersionObjectReference, selector labels.Selector, metricSelector labels.Selector) (replicaCount int32, utilization int64, timestamp time.Time, err error) {
  198. utilization, timestamp, err = c.metricsClient.GetObjectMetric(metricName, namespace, objectRef, metricSelector)
  199. if err != nil {
  200. return 0, 0, time.Time{}, fmt.Errorf("unable to get metric %s: %v on %s %s/%s", metricName, objectRef.Kind, namespace, objectRef.Name, err)
  201. }
  202. usageRatio := float64(utilization) / float64(targetUtilization)
  203. replicaCount, timestamp, err = c.getUsageRatioReplicaCount(currentReplicas, usageRatio, namespace, selector)
  204. return replicaCount, utilization, timestamp, err
  205. }
  206. // getUsageRatioReplicaCount calculates the desired replica count based on usageRatio and ready pods count.
  207. // For currentReplicas=0 doesn't take into account ready pods count and tolerance to support scaling to zero pods.
  208. func (c *ReplicaCalculator) getUsageRatioReplicaCount(currentReplicas int32, usageRatio float64, namespace string, selector labels.Selector) (replicaCount int32, timestamp time.Time, err error) {
  209. if currentReplicas != 0 {
  210. if math.Abs(1.0-usageRatio) <= c.tolerance {
  211. // return the current replicas if the change would be too small
  212. return currentReplicas, timestamp, nil
  213. }
  214. readyPodCount := int64(0)
  215. readyPodCount, err = c.getReadyPodsCount(namespace, selector)
  216. if err != nil {
  217. return 0, time.Time{}, fmt.Errorf("unable to calculate ready pods: %s", err)
  218. }
  219. replicaCount = int32(math.Ceil(usageRatio * float64(readyPodCount)))
  220. } else {
  221. // Scale to zero or n pods depending on usageRatio
  222. replicaCount = int32(math.Ceil(usageRatio))
  223. }
  224. return replicaCount, timestamp, err
  225. }
  226. // GetObjectPerPodMetricReplicas calculates the desired replica count based on a target metric utilization (as a milli-value)
  227. // for the given object in the given namespace, and the current replica count.
  228. func (c *ReplicaCalculator) GetObjectPerPodMetricReplicas(statusReplicas int32, targetAverageUtilization int64, metricName string, namespace string, objectRef *autoscaling.CrossVersionObjectReference, metricSelector labels.Selector) (replicaCount int32, utilization int64, timestamp time.Time, err error) {
  229. utilization, timestamp, err = c.metricsClient.GetObjectMetric(metricName, namespace, objectRef, metricSelector)
  230. if err != nil {
  231. return 0, 0, time.Time{}, fmt.Errorf("unable to get metric %s: %v on %s %s/%s", metricName, objectRef.Kind, namespace, objectRef.Name, err)
  232. }
  233. replicaCount = statusReplicas
  234. usageRatio := float64(utilization) / (float64(targetAverageUtilization) * float64(replicaCount))
  235. if math.Abs(1.0-usageRatio) > c.tolerance {
  236. // update number of replicas if change is large enough
  237. replicaCount = int32(math.Ceil(float64(utilization) / float64(targetAverageUtilization)))
  238. }
  239. utilization = int64(math.Ceil(float64(utilization) / float64(statusReplicas)))
  240. return replicaCount, utilization, timestamp, nil
  241. }
  242. // @TODO(mattjmcnaughton) Many different functions in this module use variations
  243. // of this function. Make this function generic, so we don't repeat the same
  244. // logic in multiple places.
  245. func (c *ReplicaCalculator) getReadyPodsCount(namespace string, selector labels.Selector) (int64, error) {
  246. podList, err := c.podLister.Pods(namespace).List(selector)
  247. if err != nil {
  248. return 0, fmt.Errorf("unable to get pods while calculating replica count: %v", err)
  249. }
  250. if len(podList) == 0 {
  251. return 0, fmt.Errorf("no pods returned by selector while calculating replica count")
  252. }
  253. readyPodCount := 0
  254. for _, pod := range podList {
  255. if pod.Status.Phase == v1.PodRunning && podutil.IsPodReady(pod) {
  256. readyPodCount++
  257. }
  258. }
  259. return int64(readyPodCount), nil
  260. }
  261. // GetExternalMetricReplicas calculates the desired replica count based on a
  262. // target metric value (as a milli-value) for the external metric in the given
  263. // namespace, and the current replica count.
  264. func (c *ReplicaCalculator) GetExternalMetricReplicas(currentReplicas int32, targetUtilization int64, metricName, namespace string, metricSelector *metav1.LabelSelector, podSelector labels.Selector) (replicaCount int32, utilization int64, timestamp time.Time, err error) {
  265. metricLabelSelector, err := metav1.LabelSelectorAsSelector(metricSelector)
  266. if err != nil {
  267. return 0, 0, time.Time{}, err
  268. }
  269. metrics, timestamp, err := c.metricsClient.GetExternalMetric(metricName, namespace, metricLabelSelector)
  270. if err != nil {
  271. return 0, 0, time.Time{}, fmt.Errorf("unable to get external metric %s/%s/%+v: %s", namespace, metricName, metricSelector, err)
  272. }
  273. utilization = 0
  274. for _, val := range metrics {
  275. utilization = utilization + val
  276. }
  277. usageRatio := float64(utilization) / float64(targetUtilization)
  278. replicaCount, timestamp, err = c.getUsageRatioReplicaCount(currentReplicas, usageRatio, namespace, podSelector)
  279. return replicaCount, utilization, timestamp, err
  280. }
  281. // GetExternalPerPodMetricReplicas calculates the desired replica count based on a
  282. // target metric value per pod (as a milli-value) for the external metric in the
  283. // given namespace, and the current replica count.
  284. func (c *ReplicaCalculator) GetExternalPerPodMetricReplicas(statusReplicas int32, targetUtilizationPerPod int64, metricName, namespace string, metricSelector *metav1.LabelSelector) (replicaCount int32, utilization int64, timestamp time.Time, err error) {
  285. metricLabelSelector, err := metav1.LabelSelectorAsSelector(metricSelector)
  286. if err != nil {
  287. return 0, 0, time.Time{}, err
  288. }
  289. metrics, timestamp, err := c.metricsClient.GetExternalMetric(metricName, namespace, metricLabelSelector)
  290. if err != nil {
  291. return 0, 0, time.Time{}, fmt.Errorf("unable to get external metric %s/%s/%+v: %s", namespace, metricName, metricSelector, err)
  292. }
  293. utilization = 0
  294. for _, val := range metrics {
  295. utilization = utilization + val
  296. }
  297. replicaCount = statusReplicas
  298. usageRatio := float64(utilization) / (float64(targetUtilizationPerPod) * float64(replicaCount))
  299. if math.Abs(1.0-usageRatio) > c.tolerance {
  300. // update number of replicas if the change is large enough
  301. replicaCount = int32(math.Ceil(float64(utilization) / float64(targetUtilizationPerPod)))
  302. }
  303. utilization = int64(math.Ceil(float64(utilization) / float64(statusReplicas)))
  304. return replicaCount, utilization, timestamp, nil
  305. }
  306. func groupPods(pods []*v1.Pod, metrics metricsclient.PodMetricsInfo, resource v1.ResourceName, cpuInitializationPeriod, delayOfInitialReadinessStatus time.Duration) (readyPodCount int, ignoredPods sets.String, missingPods sets.String) {
  307. missingPods = sets.NewString()
  308. ignoredPods = sets.NewString()
  309. for _, pod := range pods {
  310. if pod.DeletionTimestamp != nil || pod.Status.Phase == v1.PodFailed {
  311. continue
  312. }
  313. // Pending pods are ignored.
  314. if pod.Status.Phase == v1.PodPending {
  315. ignoredPods.Insert(pod.Name)
  316. continue
  317. }
  318. // Pods missing metrics.
  319. metric, found := metrics[pod.Name]
  320. if !found {
  321. missingPods.Insert(pod.Name)
  322. continue
  323. }
  324. // Unready pods are ignored.
  325. if resource == v1.ResourceCPU {
  326. var ignorePod bool
  327. _, condition := podutil.GetPodCondition(&pod.Status, v1.PodReady)
  328. if condition == nil || pod.Status.StartTime == nil {
  329. ignorePod = true
  330. } else {
  331. // Pod still within possible initialisation period.
  332. if pod.Status.StartTime.Add(cpuInitializationPeriod).After(time.Now()) {
  333. // Ignore sample if pod is unready or one window of metric wasn't collected since last state transition.
  334. ignorePod = condition.Status == v1.ConditionFalse || metric.Timestamp.Before(condition.LastTransitionTime.Time.Add(metric.Window))
  335. } else {
  336. // Ignore metric if pod is unready and it has never been ready.
  337. ignorePod = condition.Status == v1.ConditionFalse && pod.Status.StartTime.Add(delayOfInitialReadinessStatus).After(condition.LastTransitionTime.Time)
  338. }
  339. }
  340. if ignorePod {
  341. ignoredPods.Insert(pod.Name)
  342. continue
  343. }
  344. }
  345. readyPodCount++
  346. }
  347. return
  348. }
  349. func calculatePodRequests(pods []*v1.Pod, resource v1.ResourceName) (map[string]int64, error) {
  350. requests := make(map[string]int64, len(pods))
  351. for _, pod := range pods {
  352. podSum := int64(0)
  353. for _, container := range pod.Spec.Containers {
  354. if containerRequest, ok := container.Resources.Requests[resource]; ok {
  355. podSum += containerRequest.MilliValue()
  356. } else {
  357. return nil, fmt.Errorf("missing request for %s", resource)
  358. }
  359. }
  360. requests[pod.Name] = podSum
  361. }
  362. return requests, nil
  363. }
  364. func removeMetricsForPods(metrics metricsclient.PodMetricsInfo, pods sets.String) {
  365. for _, pod := range pods.UnsortedList() {
  366. delete(metrics, pod)
  367. }
  368. }