From 65bf96497d88e7b31bcc9d9259d54b6709b1cf64 Mon Sep 17 00:00:00 2001 From: qmhu Date: Mon, 9 May 2022 19:44:52 +0800 Subject: [PATCH 1/2] Use recommendationMission to control recommendation lifecycle --- cmd/craned/app/manager.go | 26 +- docs/images/analytics-arch.png | Bin 0 -> 123070 bytes docs/index.md | 6 +- docs/index.zh.md | 6 +- .../tutorials/analytics-and-recommendation.md | 294 ++---------------- .../analytics-and-recommendation.zh.md | 34 ++ docs/tutorials/hpa-recommendation.md | 217 +++++++++++++ docs/tutorials/hpa-recommendation.zh.md | 217 +++++++++++++ docs/tutorials/resource-recommendation.md | 142 +++++++++ docs/tutorials/resource-recommendation.zh.md | 134 ++++++++ ...ctive-hpa-to-scaling-with-effectiveness.md | 23 ++ ...ve-hpa-to-scaling-with-effectiveness.zh.md | 23 ++ examples/analytics/analytics-hpa.yaml | 26 +- examples/analytics/analytics-resource.yaml | 24 +- examples/analytics/nginx-deployment.yaml | 28 ++ go.mod | 2 +- go.sum | 2 + mkdocs.yml | 2 + .../analytics/analytics_controller.go | 281 ++++++++++++----- .../ehpa/effective_hpa_controller.go | 22 ++ .../recommendation_controller.go | 79 +---- pkg/controller/recommendation/updater.go | 107 ++++--- pkg/known/annotation.go | 4 + pkg/known/label.go | 6 + pkg/recommend/advisor/ehpa.go | 3 +- pkg/recommend/config.go | 10 +- pkg/recommend/recommender.go | 8 +- 27 files changed, 1185 insertions(+), 541 deletions(-) create mode 100644 docs/images/analytics-arch.png create mode 100644 docs/tutorials/analytics-and-recommendation.zh.md create mode 100644 docs/tutorials/hpa-recommendation.md create mode 100644 docs/tutorials/hpa-recommendation.zh.md create mode 100644 docs/tutorials/resource-recommendation.md create mode 100644 docs/tutorials/resource-recommendation.zh.md create mode 100644 examples/analytics/nginx-deployment.yaml diff --git a/cmd/craned/app/manager.go b/cmd/craned/app/manager.go index 4350c0c58..ee37d1fb8 100644 --- a/cmd/craned/app/manager.go +++ b/cmd/craned/app/manager.go @@ -23,6 +23,7 @@ import ( autoscalingapi "github.com/gocrane/api/autoscaling/v1alpha1" ensuranceapi "github.com/gocrane/api/ensurance/v1alpha1" predictionapi "github.com/gocrane/api/prediction/v1alpha1" + "github.com/gocrane/crane/cmd/craned/app/options" "github.com/gocrane/crane/pkg/controller/analytics" "github.com/gocrane/crane/pkg/controller/cnp" @@ -282,29 +283,30 @@ func initializationControllers(ctx context.Context, mgr ctrl.Manager, opts *opti } if utilfeature.DefaultMutableFeatureGate.Enabled(features.CraneAnalysis) { - if err := (&analytics.Controller{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - RestMapper: mgr.GetRESTMapper(), - Recorder: mgr.GetEventRecorderFor("analytics-controller"), - }).SetupWithManager(mgr); err != nil { - klog.Exit(err, "unable to create controller", "controller", "AnalyticsController") - } - configSet, err := recommend.LoadConfigSetFromFile(opts.RecommendationConfigFile) if err != nil { klog.Errorf("Failed to load recommendation config file: %v", err) os.Exit(1) } - if err := (&recommendation.Controller{ + + if err := (&analytics.Controller{ Client: mgr.GetClient(), - ConfigSet: configSet, Scheme: mgr.GetScheme(), RestMapper: mgr.GetRESTMapper(), - Recorder: mgr.GetEventRecorderFor("recommendation-controller"), + Recorder: mgr.GetEventRecorderFor("analytics-controller"), + ConfigSet: configSet, ScaleClient: scaleClient, PredictorMgr: predictorMgr, Provider: historyDataSource, + }).SetupWithManager(mgr); err != nil { + klog.Exit(err, "unable to create controller", "controller", "AnalyticsController") + } + + if err := (&recommendation.Controller{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + RestMapper: mgr.GetRESTMapper(), + Recorder: mgr.GetEventRecorderFor("recommendation-controller"), }).SetupWithManager(mgr); err != nil { klog.Exit(err, "unable to create controller", "controller", "RecommendationController") } diff --git a/docs/images/analytics-arch.png b/docs/images/analytics-arch.png new file mode 100644 index 0000000000000000000000000000000000000000..262f17cb45db91037df88794c6558fd0077903e3 GIT binary patch literal 123070 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%0A=~}+X6#Qz)csR+w%t>xMS!J!S0V&qF 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 aj-)QXnioDjYi&*id<=B2>y&6Y#QX>A+gf=5 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index c582a40cb..a984433ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,8 +35,10 @@ Analytics model analyzes the workload and provide recommendations about resource Two Recommendations are currently supported: -- **ResourceRecommend**: Recommend container requests & limit resources based on historic metrics. -- **Effective HPARecommend**: Recommend which workloads are suitable for autoscaling and provide optimized configurations such as minReplicas, maxReplicas. +- [**ResourceRecommend**](tutorials/resource-recommendation.md): Recommend container requests & limit resources based on historic metrics. +- [**Effective HPARecommend**](tutorials/hpa-recommendation.md): Recommend which workloads are suitable for autoscaling and provide optimized configurations such as minReplicas, maxReplicas. + +Please see [this document](tutorials/analytics-and-recommendation.md) to learn more. ### QoS Ensurance Kubernetes is capable of starting multiple pods on same node, and as a result, some of the user applications may be impacted when there are resources(e.g. cpu) consumption competition. To mitigate this, Crane allows users defining PrioirtyClass for the pods and QoSEnsurancePolicy, and then detects disruption and ensure the high priority pods not being impacted by resource competition. diff --git a/docs/index.zh.md b/docs/index.zh.md index 811b8fec3..272760ed2 100644 --- a/docs/index.zh.md +++ b/docs/index.zh.md @@ -34,8 +34,10 @@ Analytics model analyzes the workload and provide recommendations about resource Two Recommendations are currently supported: -- **ResourceRecommend**: Recommend container requests & limit resources based on historic metrics. -- **Effective HPARecommend**: Recommend which workloads are suitable for autoscaling and provide optimized configurations such as minReplicas, maxReplicas. +- [**ResourceRecommend**](tutorials/resource-recommendation.md): Recommend container requests & limit resources based on historic metrics. +- [**Effective HPARecommend**](tutorials/hpa-recommendation.md): Recommend which workloads are suitable for autoscaling and provide optimized configurations such as minReplicas, maxReplicas. + +Please see [this document](tutorials/analytics-and-recommendation.md) to learn more. ### QoS Ensurance Kubernetes is capable of starting multiple pods on same node, and as a result, some of the user applications may be impacted when there are resources(e.g. cpu) consumption competition. To mitigate this, Crane allows users defining PrioirtyClass for the pods and QoSEnsurancePolicy, and then detects disruption and ensure the high priority pods not being impacted by resource competition. diff --git a/docs/tutorials/analytics-and-recommendation.md b/docs/tutorials/analytics-and-recommendation.md index 1a8dc1145..31bbe6284 100644 --- a/docs/tutorials/analytics-and-recommendation.md +++ b/docs/tutorials/analytics-and-recommendation.md @@ -4,289 +4,33 @@ Analytics and Recommendation provide capacity that analyzes the workload in k8s Two Recommendations are currently supported: -- **ResourceRecommend**: Recommend container requests & limit resources based on historic metrics. -- **Effective HPARecommend**: Recommend which workloads are suitable for autoscaling and provide optimized configurations such as minReplicas, maxReplicas. +- [**ResourceRecommend**](resource-recommendation.md): Recommend container requests & limit resources based on historic metrics. +- [**HPARecommend**](hpa-recommendation.md): Recommend which workloads are suitable for autoscaling and provide optimized configurations such as minReplicas, maxReplicas. -## Analytics and Recommend Pod Resources +## Architecture -Create an **Resource** `Analytics` to give recommendation for deployment: `craned` and `metric-adapter` as a sample. +![analytics-arch](../images/analytics-arch.png) -```bash -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-resource.yaml -kubectl get analytics -n crane-system -``` +## An analytical process -```yaml title="analytics-resource.yaml" hl_lines="7 24 11-14 28-31" -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: craned-resource - namespace: crane-system -spec: - type: Resource # This can only be "Resource" or "HPA". - completionStrategy: - completionStrategyType: Periodical # This can only be "Once" or "Periodical". - periodSeconds: 86400 # analytics selected resources every 1 day - resourceSelectors: # defines all the resources to be select with - - kind: Deployment - apiVersion: apps/v1 - name: craned +1. Users create `Analytics` object and config ResourceSelector to select resources to be analyzed. Multiple types of resource selection (based on Group,Kind, and Version) are supported. +2. Analyze each selected resource in parallel and try to execute analysis and give recommendation. Each analysis process is divided into two stages: inspecting and advising: + 1. Inspecting: Filter resources that don't match the recommended conditions. For example, for hpa recommendation, the workload that have many not running pod is excluded + 2. Advising: Analysis and calculate based on algorithm model then provide the recommendation result. +3. If you paas the above two stages, it will create `Recommendation` object and display the result in `recommendation.Status` +4. You can find the failure reasons from `analytics.status.recommendations` +5. Wait for the next analytics based on the interval ---- +## Core concept -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: metric-adapter-resource - namespace: crane-system -spec: - type: Resource # This can only be "Resource" or "HPA". - completionStrategy: - completionStrategyType: Periodical # This can only be "Once" or "Periodical". - periodSeconds: 3600 # analytics selected resources every 1 hour - resourceSelectors: # defines all the resources to be select with - - kind: Deployment - apiVersion: apps/v1 - name: metric-adapter -``` +### Analytics -The output is: +Analysis defines a scanning analysis task. Two task types are supported: resource recommendation and hpa recommendation. Crane regularly runs analysis tasks and produces recommended results. -```bash -NAME AGE -craned-resource 15m -metric-adapter-resource 15m -``` +### Recommendation -You can get created recommendation from analytics status: +The recommendation shows the results of an `Analytics`. The recommended result is a YAML configuration that allows users to take appropriate optimization actions, such as adjusting the resource configuration of the application. -```bash -kubectl get analytics craned-resource -n crane-system -o yaml -``` +### Configuration -The output is similar to: - -```yaml hl_lines="18-21" -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: craned-resource - namespace: crane-system -spec: - completionStrategy: - completionStrategyType: Periodical - periodSeconds: 86400 - resourceSelectors: - - apiVersion: apps/v1 - kind: Deployment - labelSelector: {} - name: craned - type: Resource -status: - lastSuccessfulTime: "2022-01-12T08:40:59Z" - recommendations: - - name: craned-resource-resource-j7shb - namespace: crane-system - uid: 8ce2eedc-7969-4b80-8aee-fd4a98d6a8b6 -``` - -The recommendation name presents on `status.recommendations[0].name`. Then you can get recommendation detail by running: - -```bash -kubectl get recommend -n crane-system craned-resource-resource-j7shb -o yaml -``` - -The output is similar to: - -```yaml hl_lines="32-37" -apiVersion: analysis.crane.io/v1alpha1 -kind: Recommendation -metadata: - name: craned-resource-resource-j7shb - namespace: crane-system - ownerReferences: - - apiVersion: analysis.crane.io/v1alpha1 - blockOwnerDeletion: false - controller: false - kind: Analytics - name: craned-resource - uid: a9e6dc0d-ab26-4f2a-84bd-4fe9e0f3e105 -spec: - completionStrategy: - completionStrategyType: Periodical - periodSeconds: 86400 - targetRef: - apiVersion: apps/v1 - kind: Deployment - name: craned - namespace: crane-system - type: Resource -status: - conditions: - - lastTransitionTime: "2022-01-12T08:40:59Z" - message: Recommendation is ready - reason: RecommendationReady - status: "True" - type: Ready - lastSuccessfulTime: "2022-01-12T08:40:59Z" - lastUpdateTime: "2022-01-12T08:40:59Z" - resourceRequest: - containers: - - containerName: craned - target: - cpu: 114m - memory: 120586239m -``` - -The `status.resourceRequest` is recommended by crane's recommendation engine. - -Something you should know about Resource recommendation: - -* Resource Recommendation use historic prometheus metrics to calculate and propose. -* We use **Percentile** algorithm to process metrics that also used by VPA. -* If the workload is running for a long term like several weeks, the result will be more accurate. - -## Analytics and Recommend HPA - -Create an **HPA** `Analytics` to give recommendations for deployment: `craned` and `metric-adapter` as a sample. - -```bash -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-hpa.yaml -kubectl get analytics -n crane-system -``` - -```yaml title="analytics-hpa.yaml" hl_lines="7 24 11-14 28-31" -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: craned-hpa - namespace: crane-system -spec: - type: HPA # This can only be "Resource" or "HPA". - completionStrategy: - completionStrategyType: Periodical # This can only be "Once" or "Periodical". - periodSeconds: 600 # analytics selected resources every 10 minutes - resourceSelectors: # defines all the resources to be select with - - kind: Deployment - apiVersion: apps/v1 - name: craned - ---- - -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: metric-adapter-hpa - namespace: crane-system -spec: - type: HPA # This can only be "Resource" or "HPA". - completionStrategy: - completionStrategyType: Periodical # This can only be "Once" or "Periodical". - periodSeconds: 3600 # analytics selected resources every 1 hour - resourceSelectors: # defines all the resources to be select with - - kind: Deployment - apiVersion: apps/v1 - name: metric-adapter -``` - - -The output is: - -```bash -NAME AGE -craned-hpa 5m52s -craned-resource 18h -metric-adapter-hpa 5m52s -metric-adapter-resource 18h - -``` - -You can get created recommendation from analytics status: - -```bash -kubectl get analytics craned-hpa -n crane-system -o yaml -``` - -The output is similar to: - -```yaml hl_lines="21" -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: craned-hpa - namespace: crane-system -spec: - completionStrategy: - completionStrategyType: Periodical - periodSeconds: 86400 - resourceSelectors: - - apiVersion: apps/v1 - kind: Deployment - labelSelector: {} - name: craned - type: HPA -status: - lastSuccessfulTime: "2022-01-13T07:26:18Z" - recommendations: - - apiVersion: analysis.crane.io/v1alpha1 - kind: Recommendation - name: craned-hpa-hpa-2f22w - namespace: crane-system - uid: 397733ee-986a-4630-af75-736d2b58bfac -``` - -The recommendation name presents on `status.recommendations[0].name`. Then you can get recommendation detail by running: - -```bash -kubectl get recommend -n crane-system craned-resource-resource-2f22w -o yaml -``` - -The output is similar to: - -```yaml hl_lines="26-29" -apiVersion: analysis.crane.io/v1alpha1 -kind: Recommendation -metadata: - name: craned-hpa-hpa-2f22w - namespace: crane-system - ownerReferences: - - apiVersion: analysis.crane.io/v1alpha1 - blockOwnerDeletion: false - controller: false - kind: Analytics - name: craned-hpa - uid: b216d9c3-c52e-4c9c-b9e9-9d5b45165b1d -spec: - completionStrategy: - completionStrategyType: Periodical - periodSeconds: 86400 - targetRef: - apiVersion: apps/v1 - kind: Deployment - name: craned - namespace: crane-system - type: HPA -status: - conditions: - - lastTransitionTime: "2022-01-13T07:51:18Z" - message: 'Failed to offer recommend, Recommendation crane-system/craned-hpa-hpa-2f22w - error EHPAAdvisor prediction metrics data is unexpected, List length is 0 ' - reason: FailedOfferRecommend - status: "False" - type: Ready - lastUpdateTime: "2022-01-13T07:51:18Z" -``` - -The `status.resourceRequest` is recommended by crane's recommendation engine. The fail reason is demo workload don't have enough run time. - -Something you should know about HPA recommendation: - -* HPA Recommendation use historic prometheus metrics to calculate, forecast and propose. -* We use **DSP** algorithm to process metrics. -* We recommend using Effective HorizontalPodAutoscaler to execute autoscaling, you can see [this document](using-time-series-prediction.md) to learn more. -* The Workload need match following conditions: - * Existing at least one ready pod - * Ready pod ratio should larger that 50% - * Must provide cpu request for pod spec - * The workload should be running for at least **a week** to get enough metrics to forecast - * The workload's cpu load should be predictable, **too low** or **too unstable** workload often is unpredictable +Different analytics uses different computing models. Crane provides a default computing model and a corresponding configuration that users can modify to customize the recommended effect. You can modify the default configuration globally or modify the configuration of a single analytics task. diff --git a/docs/tutorials/analytics-and-recommendation.zh.md b/docs/tutorials/analytics-and-recommendation.zh.md new file mode 100644 index 000000000..655326e24 --- /dev/null +++ b/docs/tutorials/analytics-and-recommendation.zh.md @@ -0,0 +1,34 @@ +# 分析和推荐 + +分析和推荐提供了一套自动化的成本优化能力,它帮助用户自动的发现问题并提供优化的方案。就像电脑/手机助手一样,它会定期的扫描、分析你的集群并给出推荐建议。目前,我们提供了两种优化能力: + +- [**资源推荐**](resource-recommendation.zh.md): 基于应用的历史资源使用推荐 Container 合适的 requests 和 limits +- [**弹性推荐**](hpa-recommendation.zh.md): 筛选所有的工作负载,推荐出适合做弹性的工作负载并给出弹性建议 + +## 架构 + +![analytics-arch](../images/analytics-arch.png) + +## 一次分析的过程 + +1. 用户创建 Analytics 对象,通过 ResourceSelector 选择需要分析的资源,支持选择多类型(基于Group,Kind,Version)的批量选择 +2. 并行分析每个选择的资源,尝试进行分析推荐,每次分析过程分成筛选和推荐两个阶段: + 1. 筛选:排除不满足推荐条件的资源。比如对于弹性推荐,排除没有 running pod 的workload + 2. 推荐:通过算法计算分析,给出推荐结果 +3. 如果通过筛选,创建 Recommendation 对象,将推荐结果展示在 Recommendation.Status +4. 未通过筛选的原因和状态展示在 Analytics.Status +5. 根据运行间隔等待下次分析 + +## 名词解释 + +### 分析 + +分析定义了一个扫描分析任务。支持两种任务类型:资源推荐和弹性推荐。Crane 定期运行分析任务,并产生推荐结果。 + +### 推荐 + +推荐展示了一个优化推荐的结果。推荐的结果是一段 YAML 配置,根据结果用户可以进行相应的优化动作,比如调整应用的资源配置。 + +### 参数配置 + +不同的分析采用不同的计算模型,Crane 提供了一套默认的计算模型以及一套配套的配置,用户可以通过修改配置来定制推荐的效果。支持修改全局的默认配置和修改单个分析任务的配置。 diff --git a/docs/tutorials/hpa-recommendation.md b/docs/tutorials/hpa-recommendation.md new file mode 100644 index 000000000..d578a276d --- /dev/null +++ b/docs/tutorials/hpa-recommendation.md @@ -0,0 +1,217 @@ +# HPA Recommendation + +Using hpa recommendations, you can find resources in the cluster that are suitable for autoscaling, and use Crane's recommended result to create autoscaling object: [Effective HorizontalPodAutoscaler](tutorials/using-time-series-prediction.md) + +## Create HPA Analytics + +Create an **Resource** `Analytics` to give recommendation for deployment: `nginx-deployment` as a sample. + +```bash +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-hpa.yaml +kubectl get analytics +``` + +```yaml title="analytics-hpa.yaml" +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + name: nginx-hpa +spec: + type: HPA # This can only be "Resource" or "HPA". + completionStrategy: + completionStrategyType: Periodical # This can only be "Once" or "Periodical". + periodSeconds: 600 # analytics selected resources every 10 minutes + resourceSelectors: # defines all the resources to be select with + - kind: Deployment + apiVersion: apps/v1 + name: nginx-deployment + config: # defines all the configuration for this analytics + ehpa.deployment-min-replicas: "1" + ehpa.fluctuation-threshold: "0" + ehpa.min-cpu-usage-threshold: "0" +``` + +The output is: + +```bash +NAME AGE +nginx-hpa 16m +``` + +You can get created recommendation from analytics status: + +```bash +kubectl get analytics nginx-hpa -o yaml +``` + +The output is similar to: + +```yaml +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + creationTimestamp: "2022-05-15T13:34:19Z" + name: nginx-hpa + namespace: default +spec: + completionStrategy: + completionStrategyType: Periodical + periodSeconds: 600 + config: + ehpa.deployment-min-replicas: "1" + ehpa.fluctuation-threshold: "0" + ehpa.min-cpu-usage-threshold: "0" + resourceSelectors: + - apiVersion: apps/v1 + kind: Deployment + labelSelector: {} + name: nginx-deployment + type: HPA +status: + conditions: + - lastTransitionTime: "2022-05-15T13:34:19Z" + message: Analytics is ready + reason: AnalyticsReady + status: "True" + type: Ready + lastUpdateTime: "2022-05-15T13:34:19Z" + recommendations: + - lastStartTime: "2022-05-15T13:34:19Z" + message: Success + name: nginx-hpa-hpa-cd86s + namespace: default + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + uid: b3cea8cb-259d-4cb2-bbbe-cd0e6544daaf +``` + +## Recommendation: Analytics result + +The recommendation name presents on `status.recommendations[0].name`. Then you can get recommendation detail by running: + +```bash +kubectl get recommend nginx-hpa-hpa-cd86s -o yaml +``` + +The output is similar to: + +```yaml +apiVersion: analysis.crane.io/v1alpha1 +kind: Recommendation +metadata: + creationTimestamp: "2022-05-15T13:34:19Z" + generateName: nginx-hpa-hpa- + generation: 2 + labels: + analysis.crane.io/analytics-name: nginx-hpa + analysis.crane.io/analytics-type: HPA + analysis.crane.io/analytics-uid: 5564edd0-d7cd-4da6-865b-27fa4fddf7c4 + app: nginx + name: nginx-hpa-hpa-cd86s + namespace: default + ownerReferences: + - apiVersion: analysis.crane.io/v1alpha1 + blockOwnerDeletion: false + controller: false + kind: Analytics + name: nginx-hpa + uid: 5564edd0-d7cd-4da6-865b-27fa4fddf7c4 +spec: + adoptionType: StatusAndAnnotation + completionStrategy: + completionStrategyType: Once + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + type: HPA +status: + conditions: + - lastTransitionTime: "2022-05-15T13:34:19Z" + message: Recommendation is ready + reason: RecommendationReady + status: "True" + type: Ready + lastUpdateTime: "2022-05-15T13:34:19Z" + recommendedValue: | + maxReplicas: 2 + metrics: + - resource: + name: cpu + target: + averageUtilization: 75 + type: Utilization + type: Resource + minReplicas: 2 +``` + +## HPA Recommendation Algorithm model + +### Inspecting + +1. Workload with low replicas: If the replicas is too low, may not be suitable for hpa recommendation. Associated configuration: ehpa.deployment-min-replicas | ehpa.statefulset-min-replicas | ehpa.workload-min-replicas +2. Workload with a certain percentage of not running pods: if the workload of Pod mostly can't run normally, may not be suitable for flexibility. Associated configuration: ehpa.pod-min-ready-seconds | ehpa.pod-available-ratio +3. Workload with low cpu usage: The low CPU usage workload means that there is no load pressure. In this case, we can't estimate it. Associated configuration: ehpa.min-cpu-usage-threshold +4. Workload with low fluctuation of cpu usage: dividing of the maximum and minimum usage is defined as the fluctuation rate. If the fluctuation rate is too low, the workload will not benefit much from hpa. Associated configuration: ehpa.fluctuation-threshold + +### Advising + +In the advising phase, one EffectiveHPA Spec is recommended using the following Algorithm model. The recommended logic for each field is as follows: + +**Recommend TargetUtilization** + +Principle: Use Pod P99 resource utilization to recommend hpa. Because if the application can accept this utilization over P99 time, it can be inferred as a target for elasticity. + +1. Get the Pod P99 usage of the past seven days by Percentile algorithm: pod_cpu_usage_p99 +2. Corresponding utilization: target_pod_CPU_utilization = pod_cpu_usage_p99 / pod_cpu_request +3. To prevent over-utilization or under-utilization, target_pod_cpu_utilization needs to be less than ehpa.min-cpu-target-utilization and greater than ehpa. max-cpu-target-utilization + +**Recommend minReplicas** + +Principle: MinReplicas are recommended for the lowest hourly workload utilization for the past seven days. + +1. Calculate the lowest median workload cpu usage of the past seven days: workload_cpu_usage_medium_min +2. Corresponding replicas: minReplicas = workload_cpu_usage_medium_min / pod_cpu_request / ehpa.max-cpu-target-utilization +3. To prevent the minReplicas being too small, the minReplicas must be greater than or equal to ehpa.default-min-replicas + +**Recommend maxReplicas** + +Principle: Use workload's past and future seven days load to recommend maximum replicas. + +1. Calculate P95 workload CPU usage for the past seven days and the next seven days: workload_cpu_usage_p95 +2. Corresponding replicas: max_replicas_origin = workload_cpu_usage_p95 / pod_cpu_request / target_cpu_utilization +3. To handle with the peak traffic, Magnify by a certain factor: max_replicas = max_replicas_origin * ehpa.max-replicas-factor + +**Recommend MetricSpec(except CpuUtilization)** + +1. If HPA is configured for workload, MetricSpecs other than CpuUtilization are inherited + +**Recommend Behavior** + +1. If HPA is configured for workload, the corresponding Behavior configuration is inherited + +**Recommend Prediction** + +1. Try to predict the CPU usage of the workload in the next seven days using DSP +2. If the prediction is successful, add the prediction configuration +3. If the workload is not predictable, do not add the prediction configuration. + +## Configurations for HPA Recommendation + +- ehpa.deployment-min-replicas: the default value is 1,hpa recommendations are not made for workloads smaller than this value +- ehpa.statefulset-min-replicas: the default value is 1,hpa recommendations are not made for workloads smaller than this value +- ehpa.workload-min-replicas: the default value is 1,Workload replicas smaller than this value are not recommended for hpa. +- ehpa.pod-min-ready-seconds: the default value is 30,specifies the number of seconds in decide whether a POD is ready. +- ehpa.pod-available-ratio: the default value is 0.5,Workloads whose Ready pod ratio is smaller than this value are not recommended for hpa. +- ehpa.default-min-replicas: the default value is 2,the default minimum minReplicas. +- ehpa.max-replicas-factor: the default value is 3,the factor for calculate maxReplicas. +- ehpa.min-cpu-usage-threshold: the default value is 10, hpa recommendations are not made for workloads smaller than this value. +- ehpa.fluctuation-threshold: the default value is 1.5, hpa recommendations are not made for workloads smaller than this value. +- ehpa.min-cpu-target-utilization: the default value is 30 +- ehpa.max-cpu-target-utilization: the default value is 75 +- ehpa.reference-hpa: the default value is true, which means inherits the existing HPA configuration diff --git a/docs/tutorials/hpa-recommendation.zh.md b/docs/tutorials/hpa-recommendation.zh.md new file mode 100644 index 000000000..2a9f6fd8c --- /dev/null +++ b/docs/tutorials/hpa-recommendation.zh.md @@ -0,0 +1,217 @@ +# 弹性推荐 + +通过弹性推荐,你可以发现集群中适合弹性的资源,并使用 Crane 推荐的弹性配置创建自动弹性器: EffectiveHPA + +## 创建弹性分析 + +创建一个**弹性分析** `Analytics`,这里我们通过实例 deployment: `nginx` 作为一个例子 + +```bash +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-hpa.yaml +kubectl get analytics +``` + +```yaml title="analytics-hpa.yaml" +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + name: nginx-hpa +spec: + type: HPA # This can only be "Resource" or "HPA". + completionStrategy: + completionStrategyType: Periodical # This can only be "Once" or "Periodical". + periodSeconds: 600 # analytics selected resources every 10 minutes + resourceSelectors: # defines all the resources to be select with + - kind: Deployment + apiVersion: apps/v1 + name: nginx-deployment + config: # defines all the configuration for this analytics + ehpa.deployment-min-replicas: "1" + ehpa.fluctuation-threshold: "0" + ehpa.min-cpu-usage-threshold: "0" +``` + +结果如下: + +```bash +NAME AGE +nginx-hpa 16m +``` + +查看 Analytics 的 Status,通过 status.recommendations[0].name 得到 Recommendation 的 name: + +```bash +kubectl get analytics nginx-hpa -o yaml +``` + +结果如下: + +```yaml +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + creationTimestamp: "2022-05-15T13:34:19Z" + name: nginx-hpa + namespace: default +spec: + completionStrategy: + completionStrategyType: Periodical + periodSeconds: 600 + config: + ehpa.deployment-min-replicas: "1" + ehpa.fluctuation-threshold: "0" + ehpa.min-cpu-usage-threshold: "0" + resourceSelectors: + - apiVersion: apps/v1 + kind: Deployment + labelSelector: {} + name: nginx-deployment + type: HPA +status: + conditions: + - lastTransitionTime: "2022-05-15T13:34:19Z" + message: Analytics is ready + reason: AnalyticsReady + status: "True" + type: Ready + lastUpdateTime: "2022-05-15T13:34:19Z" + recommendations: + - lastStartTime: "2022-05-15T13:34:19Z" + message: Success + name: nginx-hpa-hpa-cd86s + namespace: default + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + uid: b3cea8cb-259d-4cb2-bbbe-cd0e6544daaf +``` + +## 查看分析结果 + +查看 **Recommendation** 结果: + +```bash +kubectl get recommend nginx-hpa-hpa-cd86s -o yaml +``` + +分析结果如下: + +```yaml +apiVersion: analysis.crane.io/v1alpha1 +kind: Recommendation +metadata: + creationTimestamp: "2022-05-15T13:34:19Z" + generateName: nginx-hpa-hpa- + generation: 2 + labels: + analysis.crane.io/analytics-name: nginx-hpa + analysis.crane.io/analytics-type: HPA + analysis.crane.io/analytics-uid: 5564edd0-d7cd-4da6-865b-27fa4fddf7c4 + app: nginx + name: nginx-hpa-hpa-cd86s + namespace: default + ownerReferences: + - apiVersion: analysis.crane.io/v1alpha1 + blockOwnerDeletion: false + controller: false + kind: Analytics + name: nginx-hpa + uid: 5564edd0-d7cd-4da6-865b-27fa4fddf7c4 +spec: + adoptionType: StatusAndAnnotation + completionStrategy: + completionStrategyType: Once + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + type: HPA +status: + conditions: + - lastTransitionTime: "2022-05-15T13:34:19Z" + message: Recommendation is ready + reason: RecommendationReady + status: "True" + type: Ready + lastUpdateTime: "2022-05-15T13:34:19Z" + recommendedValue: | + maxReplicas: 2 + metrics: + - resource: + name: cpu + target: + averageUtilization: 75 + type: Utilization + type: Resource + minReplicas: 2 +``` + +## 弹性推荐计算模型 + +### 筛选阶段 + +1. 低副本数的工作负载: 过低的副本数可能弹性需求不高,关联配置: ehpa.deployment-min-replicas | ehpa.statefulset-min-replicas | ehpa.workload-min-replicas +2. 存在一定比例非 Running Pod 的工作负载: 如果工作负载的 Pod 大多不能正常运行,可能不适合弹性,关联配置: ehpa.pod-min-ready-seconds | ehpa.pod-available-ratio +3. 低 CPU 使用量的工作负载: 过低使用量的工作负载意味着没有业务压力,此时通过使用率推荐弹性不准,关联配置: ehpa.min-cpu-usage-threshold +4. CPU 使用量的波动率过低: 使用量的最大值和最小值的倍数定义为波动率,波动率过低的工作负载通过弹性降本的收益不大,关联配置: ehpa.fluctuation-threshold + +### 推荐 + +推荐阶段通过以下模型推荐一个 EffectiveHPA 的 Spec。每个字段的推荐逻辑如下: + +**推荐 TargetUtilization** + +原理: 使用 Pod P99 资源利用率推荐弹性的目标。因为如果应用可以在 P99 时间内接受这个利用率,可以推断出可作为弹性的目标。 + + 1. 通过 Percentile 算法得到 Pod 过去七天 的 P99 使用量: pod_cpu_usage_p99 + 2. 对应的利用率:target_pod_cpu_utilization = pod_cpu_usage_p99 / pod_cpu_request + 3. 为了防止利用率过大或过小,target_pod_cpu_utilization 需要小于 ehpa.min-cpu-target-utilization 和大于 ehpa.max-cpu-target-utilization + +**推荐 minReplicas** + +原理: 使用 workload 过去七天内每小时负载最低的利用率推荐 minReplicas。 + +1. 计算过去7天 workload 每小时使用量中位数的最低值: workload_cpu_usage_medium_min +2. 对应的最低利用率对应的副本数: minReplicas = workload_cpu_usage_medium_min / pod_cpu_request / ehpa.max-cpu-target-utilization +3. 为了防止 minReplicas 过小,minReplicas 需要大于等于 ehpa.default-min-replicas + +**推荐 maxReplicas** + +原理: 使用 workload 过去和未来七天的负载推荐最大副本数。 + +1. 计算过去七天和未来七天 workload cpu 使用量的 P95: workload_cpu_usage_p95 +2. 对应的副本数: max_replicas_origin = workload_cpu_usage_p95 / pod_cpu_request / target_cpu_utilization +3. 为了应对流量洪峰,放大一定倍数: max_replicas = max_replicas_origin * ehpa.max-replicas-factor + +**推荐CPU以外 MetricSpec** + +1. 如果 workload 配置了 HPA,继承相应除 CpuUtilization 以外的其他 MetricSpec + +**推荐 Behavior** + +1. 如果 workload 配置了 HPA,继承相应的 Behavior 配置 + +**预测** + +1. 尝试预测工作负载未来七天的 CPU 使用量,算法是 DSP +2. 如果预测成功则添加预测配置 +3. 如果不可预测则不添加预测配置,退化成不具有预测功能的 EffectiveHPA + +## 弹性分析计算配置 + +- ehpa.deployment-min-replicas: 默认值 1,小于该值的工作负载不做弹性推荐 +- ehpa.statefulset-min-replicas: 默认值 1,小于该值的工作负载不做弹性推荐 +- ehpa.workload-min-replicas: 默认值 1,小于该值的工作负载不做弹性推荐 +- ehpa.pod-min-ready-seconds: 默认值 30,定义了 Pod 是否 Ready 的秒数 +- ehpa.pod-available-ratio: 默认值 0.5,Ready Pod 比例小于该值的工作负载不做弹性推荐 +- ehpa.default-min-replicas: 默认值 2,最小 minReplicas +- ehpa.max-replicas-factor: 默认值 3,计算 maxReplicas 的倍数 +- ehpa.min-cpu-usage-threshold: 默认值 10, 小于该值的工作负载不做弹性推荐 +- ehpa.fluctuation-threshold: 默认值 1.5, 小于该值的工作负载不做弹性推荐 +- ehpa.min-cpu-target-utilization: 默认值 30 +- ehpa.max-cpu-target-utilization: 默认值 75 +- ehpa.reference-hpa: 默认值 true,继承现有的 HPA 配置 \ No newline at end of file diff --git a/docs/tutorials/resource-recommendation.md b/docs/tutorials/resource-recommendation.md new file mode 100644 index 000000000..6eb46324b --- /dev/null +++ b/docs/tutorials/resource-recommendation.md @@ -0,0 +1,142 @@ +# Resource Recommendation + +Resource recommendation allows you to obtain recommended values for resources in a cluster and use them to improve the resource utilization of the cluster. + +## Create Resource Analytics + +Create an **Resource** `Analytics` to give recommendation for deployment: `nginx-deployment` as a sample. + +```bash +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-resource.yaml +kubectl get analytics -n crane-system +``` + +```yaml title="analytics-resource.yaml" hl_lines="7 24 11-14 28-31" +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + name: nginx-resource +spec: + type: Resource # This can only be "Resource" or "HPA". + completionStrategy: + completionStrategyType: Periodical # This can only be "Once" or "Periodical". + periodSeconds: 86400 # analytics selected resources every 1 day + resourceSelectors: # defines all the resources to be select with + - kind: Deployment + apiVersion: apps/v1 + name: nginx-deployment +``` + +The output is: + +```bash +NAME AGE +nginx-resource 16m +``` + +You can get created recommendation from analytics status: + +```bash +kubectl get analytics nginx-resource -o yaml +``` + +The output is similar to: + +```yaml +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + name: nginx-resource + namespace: default +spec: + completionStrategy: + completionStrategyType: Periodical + periodSeconds: 86400 + resourceSelectors: + - apiVersion: apps/v1 + kind: Deployment + labelSelector: {} + name: nginx-deployment + type: Resource +status: + conditions: + - lastTransitionTime: "2022-05-15T14:38:35Z" + message: Analytics is ready + reason: AnalyticsReady + status: "True" + type: Ready + lastUpdateTime: "2022-05-15T14:38:35Z" + recommendations: + - lastStartTime: "2022-05-15T14:38:35Z" + message: Success + name: nginx-resource-resource-w45nq + namespace: default + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + uid: 750cb3bd-0b87-4f87-acbe-57e621af0a1e +``` + +The recommendation name presents on `status.recommendations[0].name`. Then you can get recommendation detail by running: + +## Recommendation: Analytics result + +```bash +kubectl get recommend -n crane-system craned-resource-resource-j7shb -o yaml +``` + +The output is similar to: + +```yaml hl_lines="32-37" +apiVersion: analysis.crane.io/v1alpha1 +kind: Recommendation +metadata: + creationTimestamp: "2022-05-15T14:38:35Z" + generateName: nginx-resource-resource- + generation: 1 + labels: + analysis.crane.io/analytics-name: nginx-resource + analysis.crane.io/analytics-type: Resource + analysis.crane.io/analytics-uid: 89e6d867-d639-4255-89cf-a3436dad6251 + app: nginx + name: nginx-resource-resource-w45nq + namespace: default + ownerReferences: + - apiVersion: analysis.crane.io/v1alpha1 + blockOwnerDeletion: false + controller: false + kind: Analytics + name: nginx-resource + uid: 89e6d867-d639-4255-89cf-a3436dad6251 + resourceVersion: "541878166" + selfLink: /apis/analysis.crane.io/v1alpha1/namespaces/default/recommendations/nginx-resource-resource-w45nq + uid: 750cb3bd-0b87-4f87-acbe-57e621af0a1e +spec: + adoptionType: StatusAndAnnotation + completionStrategy: + completionStrategyType: Once + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + type: Resource +status: + recommendedValue: | + containers: + - containerName: nginx + target: + cpu: 114m + memory: "120586239" +``` + +The `status.resourceRequest` is recommended by crane's recommendation engine. + +Something you should know about Resource recommendation: + +* Resource Recommendation use historic prometheus metrics to calculate and propose. +* We use **Percentile** algorithm to process metrics that also used by VPA. +* If the workload is running for a long term like several weeks, the result will be more accurate. diff --git a/docs/tutorials/resource-recommendation.zh.md b/docs/tutorials/resource-recommendation.zh.md new file mode 100644 index 000000000..2a1d65497 --- /dev/null +++ b/docs/tutorials/resource-recommendation.zh.md @@ -0,0 +1,134 @@ +# 资源推荐 + +通过资源推荐,你可以得到集群中资源的推荐值并使用它提升集群的资源利用率。 + +## 创建资源分析 + +创建一个**资源分析** `Analytics`,这里我们通过实例 deployment: `nginx` 作为一个例子 + +```bash +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml +kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-resource.yaml +kubectl get analytics +``` + +```yaml title="analytics-resource.yaml" +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + name: nginx-resource +spec: + type: Resource # This can only be "Resource" or "HPA". + completionStrategy: + completionStrategyType: Periodical # This can only be "Once" or "Periodical". + periodSeconds: 86400 # analytics selected resources every 1 day + resourceSelectors: # defines all the resources to be select with + - kind: Deployment + apiVersion: apps/v1 + name: nginx-deployment +``` + +结果如下: + +```bash +NAME AGE +nginx-resource 16m +``` + +查看 Analytics 的 Status,通过 status.recommendations[0].name 得到 Recommendation 的 name: + +```bash +kubectl get analytics nginx-resource -o yaml +``` + +结果如下: + +```yaml +apiVersion: analysis.crane.io/v1alpha1 +kind: Analytics +metadata: + name: nginx-resource + namespace: default +spec: + completionStrategy: + completionStrategyType: Periodical + periodSeconds: 86400 + resourceSelectors: + - apiVersion: apps/v1 + kind: Deployment + labelSelector: {} + name: nginx-deployment + type: Resource +status: + conditions: + - lastTransitionTime: "2022-05-15T14:38:35Z" + message: Analytics is ready + reason: AnalyticsReady + status: "True" + type: Ready + lastUpdateTime: "2022-05-15T14:38:35Z" + recommendations: + - lastStartTime: "2022-05-15T14:38:35Z" + message: Success + name: nginx-resource-resource-w45nq + namespace: default + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + uid: 750cb3bd-0b87-4f87-acbe-57e621af0a1e +``` + +## 查看分析结果 + +查看 **Recommendation** 结果: + +```bash +kubectl get recommend nginx-resource-resource-w45nq -o yaml +``` + +分析结果如下: + +```yaml +apiVersion: analysis.crane.io/v1alpha1 +kind: Recommendation +metadata: + creationTimestamp: "2022-05-15T14:38:35Z" + generateName: nginx-resource-resource- + generation: 1 + labels: + analysis.crane.io/analytics-name: nginx-resource + analysis.crane.io/analytics-type: Resource + analysis.crane.io/analytics-uid: 89e6d867-d639-4255-89cf-a3436dad6251 + app: nginx + name: nginx-resource-resource-w45nq + namespace: default + ownerReferences: + - apiVersion: analysis.crane.io/v1alpha1 + blockOwnerDeletion: false + controller: false + kind: Analytics + name: nginx-resource + uid: 89e6d867-d639-4255-89cf-a3436dad6251 + resourceVersion: "541878166" + selfLink: /apis/analysis.crane.io/v1alpha1/namespaces/default/recommendations/nginx-resource-resource-w45nq + uid: 750cb3bd-0b87-4f87-acbe-57e621af0a1e +spec: + adoptionType: StatusAndAnnotation + completionStrategy: + completionStrategyType: Once + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: nginx-deployment + namespace: default + type: Resource +status: + recommendedValue: | + containers: + - containerName: nginx + target: + cpu: 114m + memory: "120586239" +``` \ No newline at end of file diff --git a/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.md b/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.md index a429b4373..34ebde49d 100644 --- a/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.md +++ b/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.md @@ -69,6 +69,7 @@ spec: ``` #### Metric conversion + When user defines `spec.metrics` in EffectiveHorizontalPodAutoscaler and prediction configuration is enabled, EffectiveHPAController will convert it to a new metric and configure the background HorizontalPodAutoscaler. This is a source EffectiveHorizontalPodAutoscaler yaml for metric definition. @@ -193,3 +194,25 @@ status: expectReplicas: 0 ``` + +## FAQ + +### error: unable to get metric crane_pod_cpu_usage + +When checking the status for EffectiveHorizontalPodAutoscaler, you may see this error: + +```yaml +- lastTransitionTime: "2022-05-15T14:05:43Z" + message: 'the HPA was unable to compute the replica count: unable to get metric + crane_pod_cpu_usage: unable to fetch metrics from custom metrics API: TimeSeriesPrediction + is not ready. ' + reason: FailedGetPodsMetric + status: "False" + type: ScalingActive +``` + +reason: Not all workload's cpu metric are predictable, if predict your workload failed, it will show above errors. + +solution: +- Just waiting. the Prediction algorithm need more time, you can see `DSP` section to know more about this algorithm. +- EffectiveHorizontalPodAutoscaler have a protection mechanism when prediction failed, it will use the actual cpu utilization to do autoscaling. \ No newline at end of file diff --git a/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md b/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md index 0995937fd..9037ac4a3 100644 --- a/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md +++ b/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md @@ -195,3 +195,26 @@ status: expectReplicas: 0 ``` + +## 常见问题 + +### 错误: unable to get metric crane_pod_cpu_usage + +当你查看 EffectiveHorizontalPodAutoscaler 的 Status 时,可以会看到这样的错误: + +```yaml +- lastTransitionTime: "2022-05-15T14:05:43Z" + message: 'the HPA was unable to compute the replica count: unable to get metric + crane_pod_cpu_usage: unable to fetch metrics from custom metrics API: TimeSeriesPrediction + is not ready. ' + reason: FailedGetPodsMetric + status: "False" + type: ScalingActive +``` + +原因:不是所有的工作负载的 CPU 使用率都是可预测的,当无法预测时就会显示以上错误。 +reason: Not all workload's cpu metric are predictable, if predict your workload failed, it will show above errors. + +解决方案: +- 等一段时间再看。预测算法 `DSP` 需要一定时间的数据才能进行预测。希望了解算法细节的可以查看算法的文档。 +- EffectiveHorizontalPodAutoscaler 提供一种保护机制,当预测失效时依然能通过实际的 CPU 使用率工作。 \ No newline at end of file diff --git a/examples/analytics/analytics-hpa.yaml b/examples/analytics/analytics-hpa.yaml index a72319d96..fb60ccee9 100644 --- a/examples/analytics/analytics-hpa.yaml +++ b/examples/analytics/analytics-hpa.yaml @@ -1,8 +1,7 @@ apiVersion: analysis.crane.io/v1alpha1 kind: Analytics metadata: - name: craned-hpa - namespace: crane-system + name: nginx-hpa spec: type: HPA # This can only be "Resource" or "HPA". completionStrategy: @@ -11,21 +10,8 @@ spec: resourceSelectors: # defines all the resources to be select with - kind: Deployment apiVersion: apps/v1 - name: craned - ---- - -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: metric-adapter-hpa - namespace: crane-system -spec: - type: HPA # This can only be "Resource" or "HPA". - completionStrategy: - completionStrategyType: Periodical # This can only be "Once" or "Periodical". - periodSeconds: 3600 # analytics selected resources every 1 hour - resourceSelectors: # defines all the resources to be select with - - kind: Deployment - apiVersion: apps/v1 - name: metric-adapter + name: nginx-deployment + config: # defines all the configuration for this analytics + ehpa.deployment-min-replicas: "1" + ehpa.fluctuation-threshold: "0" + ehpa.min-cpu-usage-threshold: "0" diff --git a/examples/analytics/analytics-resource.yaml b/examples/analytics/analytics-resource.yaml index 98da731ec..67ca43ca8 100644 --- a/examples/analytics/analytics-resource.yaml +++ b/examples/analytics/analytics-resource.yaml @@ -1,8 +1,7 @@ apiVersion: analysis.crane.io/v1alpha1 kind: Analytics metadata: - name: craned-resource - namespace: crane-system + name: nginx-resource spec: type: Resource # This can only be "Resource" or "HPA". completionStrategy: @@ -11,23 +10,4 @@ spec: resourceSelectors: # defines all the resources to be select with - kind: Deployment apiVersion: apps/v1 - name: craned - ---- - -apiVersion: analysis.crane.io/v1alpha1 -kind: Analytics -metadata: - name: metric-adapter-resource - namespace: crane-system -spec: - type: Resource # This can only be "Resource" or "HPA". - completionStrategy: - completionStrategyType: Periodical # This can only be "Once" or "Periodical". - periodSeconds: 3600 # analytics selected resources every 1 hour - resourceSelectors: # defines all the resources to be select with - - kind: Deployment - apiVersion: apps/v1 - name: metric-adapter - - + name: nginx-deployment diff --git a/examples/analytics/nginx-deployment.yaml b/examples/analytics/nginx-deployment.yaml new file mode 100644 index 000000000..b89f8560f --- /dev/null +++ b/examples/analytics/nginx-deployment.yaml @@ -0,0 +1,28 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 + resources: + limits: + cpu: "100m" + memory: 500Mi + requests: + cpu: "100m" + memory: 500Mi \ No newline at end of file diff --git a/go.mod b/go.mod index 337ef666e..9a5b1a7aa 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( github.com/go-echarts/go-echarts/v2 v2.2.4 - github.com/gocrane/api v0.4.0 + github.com/gocrane/api v0.4.1-0.20220507041258-d376db2b4ad4 github.com/google/cadvisor v0.39.2 github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 github.com/prometheus/client_golang v1.11.0 diff --git a/go.sum b/go.sum index 732f29695..e4ee71bad 100644 --- a/go.sum +++ b/go.sum @@ -310,6 +310,8 @@ github.com/gocrane/api v0.3.0 h1:ziH+zYQy/shiqQ6yskMs67e+bQ9WmPp8eCVhLW85NFQ= github.com/gocrane/api v0.3.0/go.mod h1:GxI+t9AW8+NsHkz2JkPBIJN//9eLUjTZl1ScYAbXMbk= github.com/gocrane/api v0.4.0 h1:1IWP3gbkp3T4kX68w4+PfqUr4Cb/gaJrihLYg6aKOLY= github.com/gocrane/api v0.4.0/go.mod h1:GxI+t9AW8+NsHkz2JkPBIJN//9eLUjTZl1ScYAbXMbk= +github.com/gocrane/api v0.4.1-0.20220507041258-d376db2b4ad4 h1:vGDg3G6y661KAlhjf/8/r8JCjaIi6aV8szCP+MZRU3Y= +github.com/gocrane/api v0.4.1-0.20220507041258-d376db2b4ad4/go.mod h1:GxI+t9AW8+NsHkz2JkPBIJN//9eLUjTZl1ScYAbXMbk= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/mkdocs.yml b/mkdocs.yml index 749b9a2a1..48611a2bd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,6 +68,8 @@ nav: - Tutorials: - Effective HPA: tutorials/using-effective-hpa-to-scaling-with-effectiveness.md - Analytics and Recommendation: tutorials/analytics-and-recommendation.md + - Resource Recommendation: tutorials/resource-recommendation.md + - HPA Recommendation: tutorials/hpa-recommendation.md - Qos Ensurance: tutorials/using-qos-ensurance.md - Time Series Prediction: tutorials/using-time-series-prediction.md - Load-aware Scheduling: tutorials/scheduling-pods-based-on-actual-node-load.md diff --git a/pkg/controller/analytics/analytics_controller.go b/pkg/controller/analytics/analytics_controller.go index eaf4bd7a9..a1e6dbc7c 100644 --- a/pkg/controller/analytics/analytics_controller.go +++ b/pkg/controller/analytics/analytics_controller.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "time" corev1 "k8s.io/api/core/v1" @@ -19,11 +20,13 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/scale" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" analysisv1alph1 "github.com/gocrane/api/analysis/v1alpha1" craneclient "github.com/gocrane/api/pkg/generated/clientset/versioned" @@ -31,6 +34,13 @@ import ( analysislister "github.com/gocrane/api/pkg/generated/listers/analysis/v1alpha1" "github.com/gocrane/crane/pkg/known" + predictormgr "github.com/gocrane/crane/pkg/predictor" + "github.com/gocrane/crane/pkg/providers" + "github.com/gocrane/crane/pkg/recommend" +) + +const ( + RecommendationMissionMessageSuccess = "Success" ) type Controller struct { @@ -43,6 +53,10 @@ type Controller struct { discoveryClient discovery.DiscoveryInterface recommLister analysislister.RecommendationLister K8SVersion *version.Version + ScaleClient scale.ScalesGetter + PredictorMgr predictormgr.Manager + Provider providers.History + ConfigSet *analysisv1alph1.ConfigSet } func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -69,9 +83,9 @@ func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, nil } - c.DoAnalytics(ctx, analytics) + finished := c.DoAnalytics(ctx, analytics) - if analytics.Spec.CompletionStrategy.CompletionStrategyType == analysisv1alph1.CompletionStrategyPeriodical { + if finished && analytics.Spec.CompletionStrategy.CompletionStrategyType == analysisv1alph1.CompletionStrategyPeriodical { if analytics.Spec.CompletionStrategy.PeriodSeconds != nil { d := time.Second * time.Duration(*analytics.Spec.CompletionStrategy.PeriodSeconds) klog.V(4).InfoS("Will re-sync", "after", d) @@ -81,7 +95,7 @@ func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } } - return ctrl.Result{}, nil + return ctrl.Result{RequeueAfter: time.Second * 1}, nil } // ShouldAnalytics decide if we need do analytics according to status @@ -105,7 +119,7 @@ func (c *Controller) ShouldAnalytics(analytics *analysisv1alph1.Analytics) bool return true } -func (c *Controller) DoAnalytics(ctx context.Context, analytics *analysisv1alph1.Analytics) { +func (c *Controller) DoAnalytics(ctx context.Context, analytics *analysisv1alph1.Analytics) bool { newStatus := analytics.Status.DeepCopy() identities, err := c.GetIdentities(ctx, analytics) @@ -115,14 +129,41 @@ func (c *Controller) DoAnalytics(ctx context.Context, analytics *analysisv1alph1 klog.Errorf(msg) setReadyCondition(newStatus, metav1.ConditionFalse, "FailedSelectResource", msg) c.UpdateStatus(ctx, analytics, newStatus) - return + return false } - var recommendations []*analysisv1alph1.Recommendation + timeNow := metav1.Now() + + // if the first mission start time is last round, reset currMissions here + currMissions := newStatus.Recommendations + if currMissions != nil && len(currMissions) > 0 { + firstMissionStartTime := currMissions[0].LastStartTime + if firstMissionStartTime.IsZero() { + currMissions = nil + } else { + planingTime := firstMissionStartTime.Add(time.Duration(*analytics.Spec.CompletionStrategy.PeriodSeconds) * time.Second) + if time.Now().After(planingTime) { + currMissions = nil // reset missions to trigger creation for missions + } + } + } + + if currMissions == nil { + // create recommendation Missions for this round + for _, id := range identities { + currMissions = append(currMissions, analysisv1alph1.RecommendationMission{ + TargetRef: corev1.ObjectReference{Kind: id.Kind, APIVersion: id.APIVersion, Namespace: id.Namespace, Name: id.Name}, + }) + } + } + + var currRecommendations []*analysisv1alph1.Recommendation + labelSet := labels.Set{} + labelSet[known.AnalyticsUidLabel] = string(analytics.UID) if analytics.Namespace == known.CraneSystemNamespace { - recommendations, err = c.recommLister.List(labels.Everything()) + currRecommendations, err = c.recommLister.List(labels.SelectorFromSet(labelSet)) } else { - recommendations, err = c.recommLister.Recommendations(analytics.Namespace).List(labels.Everything()) + currRecommendations, err = c.recommLister.Recommendations(analytics.Namespace).List(labels.SelectorFromSet(labelSet)) } if err != nil { c.Recorder.Event(analytics, corev1.EventTypeNormal, "FailedSelectResource", err.Error()) @@ -130,83 +171,88 @@ func (c *Controller) DoAnalytics(ctx context.Context, analytics *analysisv1alph1 klog.Errorf(msg) setReadyCondition(newStatus, metav1.ConditionFalse, "FailedSelectResource", msg) c.UpdateStatus(ctx, analytics, newStatus) - return - } - - recommendationMap := map[string]*analysisv1alph1.Recommendation{} - for _, r := range recommendations { - k := objRefKey(r.Spec.TargetRef.Kind, r.Spec.TargetRef.APIVersion, r.Spec.TargetRef.Namespace, r.Spec.TargetRef.Name, string(r.Spec.Type)) - recommendationMap[k] = r.DeepCopy() + return false } if klog.V(6).Enabled() { - // Print recommendations - for k, r := range recommendationMap { - klog.V(6).InfoS("recommendations", "analytics", klog.KObj(analytics), "key", k, "namespace", r.Namespace, "name", r.Name) - } // Print identities for k, id := range identities { klog.V(6).InfoS("identities", "analytics", klog.KObj(analytics), "key", k, "apiVersion", id.APIVersion, "kind", id.Kind, "namespace", id.Namespace, "name", id.Name) } } - var refs []analysisv1alph1.RecommendationReference - - for k, id := range identities { - if r, exists := recommendationMap[k]; exists { - refs = append(refs, analysisv1alph1.RecommendationReference{ - ObjectReference: corev1.ObjectReference{ - Kind: recommendationMap[k].Kind, - Name: recommendationMap[k].Name, - Namespace: recommendationMap[k].Namespace, - APIVersion: recommendationMap[k].APIVersion, - UID: recommendationMap[k].UID, - }, - TargetRef: recommendationMap[k].Spec.TargetRef, - }) - found := false - for _, or := range r.OwnerReferences { - if or.Name == analytics.Name && or.Kind == analytics.Kind && or.APIVersion == analytics.APIVersion { - found = true - break - } + maxConcurrency := 10 + executeIndex := -1 + var concurrency int + for index, mission := range currMissions { + if mission.LastStartTime != nil { + continue + } + if executeIndex == -1 { + executeIndex = index + } + if concurrency < maxConcurrency { + concurrency++ + } + } + + wg := sync.WaitGroup{} + wg.Add(concurrency) + for index := range currMissions { + if index < executeIndex || index >= concurrency+executeIndex { + continue + } + + var existRecommendation *analysisv1alph1.Recommendation + for _, r := range currRecommendations { + if currMissions[index].UID == r.UID { + existRecommendation = r } - if !found { - rCopy := r.DeepCopy() - rCopy.OwnerReferences = append(rCopy.OwnerReferences, *newOwnerRef(analytics)) - if err = c.Update(ctx, rCopy); err != nil { - c.Recorder.Event(analytics, corev1.EventTypeNormal, "FailedUpdateRecommendation", err.Error()) - msg := fmt.Sprintf("Failed to update ownerReferences for recommendation %s, Analytics %s error %v", klog.KObj(rCopy), klog.KObj(analytics), err) - klog.Errorf(msg) - setReadyCondition(newStatus, metav1.ConditionFalse, "FailedUpdateRecommendation", msg) - c.UpdateStatus(ctx, analytics, newStatus) - return + } + + go c.ExecuteMission(ctx, &wg, analytics, identities, &currMissions[index], existRecommendation, timeNow) + } + + wg.Wait() + + finished := false + if executeIndex+concurrency == len(currMissions) { + finished = true + } + + if finished { + newStatus.LastUpdateTime = &timeNow + + // clean orphan recommendation + for _, recommendation := range currRecommendations { + exist := false + for _, mission := range currMissions { + if recommendation.UID == mission.UID { + exist = true } - klog.InfoS("Successful to update ownerReferences", "Recommendation", rCopy, "Analytics", analytics) } - } else { - if err = c.CreateRecommendation(ctx, analytics, id, &refs); err != nil { - c.Recorder.Event(analytics, corev1.EventTypeNormal, "FailedCreateRecommendation", err.Error()) - msg := fmt.Sprintf("Failed to create recommendation, Analytics %s error %v", klog.KObj(analytics), err) - klog.Errorf(msg) - setReadyCondition(newStatus, metav1.ConditionFalse, "FailedCreateRecommendation", msg) - c.UpdateStatus(ctx, analytics, newStatus) - return + + if !exist { + klog.Infof("Deleting recommendation %s.", klog.KObj(recommendation)) + err = c.Client.Delete(ctx, recommendation) + if err != nil { + klog.Errorf("Delete recommendation %s failed: %v", klog.KObj(recommendation), err) + } } } + } - newStatus.Recommendations = refs - timeNow := metav1.Now() - newStatus.LastUpdateTime = &timeNow + + newStatus.Recommendations = currMissions setReadyCondition(newStatus, metav1.ConditionTrue, "AnalyticsReady", "Analytics is ready") c.UpdateStatus(ctx, analytics, newStatus) + return finished } -func (c *Controller) CreateRecommendation(ctx context.Context, analytics *analysisv1alph1.Analytics, - id ObjectIdentity, refs *[]analysisv1alph1.RecommendationReference) error { +func (c *Controller) CreateRecommendationObject(ctx context.Context, analytics *analysisv1alph1.Analytics, + target corev1.ObjectReference, id ObjectIdentity) *analysisv1alph1.Recommendation { - targetRef := corev1.ObjectReference{Kind: id.Kind, APIVersion: id.APIVersion, Namespace: id.Namespace, Name: id.Name} recommendation := &analysisv1alph1.Recommendation{ ObjectMeta: metav1.ObjectMeta{ GenerateName: fmt.Sprintf("%s-%s-", analytics.Name, strings.ToLower(string(analytics.Spec.Type))), @@ -217,31 +263,19 @@ func (c *Controller) CreateRecommendation(ctx context.Context, analytics *analys Labels: id.Labels, }, Spec: analysisv1alph1.RecommendationSpec{ - TargetRef: targetRef, - Type: analytics.Spec.Type, - CompletionStrategy: analytics.Spec.CompletionStrategy, + TargetRef: target, + Type: analytics.Spec.Type, }, } - if err := c.Create(ctx, recommendation); err != nil { - klog.Error(err, "Failed to create Recommendation") - return err + if recommendation.Labels == nil { + recommendation.Labels = map[string]string{} } + recommendation.Labels[known.AnalyticsNameLabel] = analytics.Name + recommendation.Labels[known.AnalyticsUidLabel] = string(analytics.UID) + recommendation.Labels[known.AnalyticsTypeLabel] = string(analytics.Spec.Type) - klog.InfoS("Successful to create", "Recommendation", klog.KObj(recommendation), "Analytics", klog.KObj(analytics)) - - *refs = append(*refs, analysisv1alph1.RecommendationReference{ - ObjectReference: corev1.ObjectReference{ - Kind: recommendation.Kind, - Name: recommendation.Name, - Namespace: recommendation.Namespace, - APIVersion: recommendation.APIVersion, - UID: recommendation.UID, - }, - TargetRef: targetRef, - }) - - return nil + return recommendation } func (c *Controller) SetupWithManager(mgr ctrl.Manager) error { @@ -352,9 +386,88 @@ func (c *Controller) GetIdentities(ctx context.Context, analytics *analysisv1alp } } + if len(identities) == 0 { + return nil, fmt.Errorf("no resource matched resource selector") + } + return identities, nil } +func (c *Controller) ExecuteMission(ctx context.Context, wg *sync.WaitGroup, analytics *analysisv1alph1.Analytics, identities map[string]ObjectIdentity, mission *analysisv1alph1.RecommendationMission, existRecommendation *analysisv1alph1.Recommendation, timeNow metav1.Time) { + defer func() { + mission.LastStartTime = &timeNow + klog.Infof("Mission message: %s", mission.Message) + + wg.Done() + }() + + k := objRefKey(mission.TargetRef.Kind, mission.TargetRef.APIVersion, mission.TargetRef.Namespace, mission.TargetRef.Name, string(analytics.Spec.Type)) + if id, exist := identities[k]; !exist { + mission.Message = fmt.Sprintf("Failed to get identity, key %s. ", k) + return + } else { + recommendation := existRecommendation + if recommendation == nil { + recommendation = c.CreateRecommendationObject(ctx, analytics, mission.TargetRef, id) + } + // do recommendation + recommender, err := recommend.NewRecommender(c.Client, c.RestMapper, c.ScaleClient, recommendation, c.PredictorMgr, c.Provider, c.ConfigSet, analytics.Spec.Config) + if err != nil { + mission.Message = fmt.Sprintf("Failed to create recommender, Recommendation %s error %v", klog.KObj(recommendation), err) + return + } + + proposed, err := recommender.Offer() + if err != nil { + mission.Message = fmt.Sprintf("Failed to offer recommend, Recommendation %s: %v", klog.KObj(recommendation), err) + return + } + + var value string + if proposed.ResourceRequest != nil { + valueBytes, err := yaml.Marshal(proposed.ResourceRequest) + if err != nil { + mission.Message = err.Error() + return + } + value = string(valueBytes) + } else if proposed.EffectiveHPA != nil { + valueBytes, err := yaml.Marshal(proposed.EffectiveHPA) + if err != nil { + mission.Message = err.Error() + return + } + value = string(valueBytes) + } + + recommendation.Status.RecommendedValue = value + if existRecommendation != nil { + klog.Infof("Update recommendation %s", klog.KObj(recommendation)) + if err := c.Update(ctx, recommendation); err != nil { + mission.Message = fmt.Sprintf("Failed to create recommendation %s: %v", klog.KObj(recommendation), err) + return + } + + klog.Infof("Successful to update Recommendation %s", klog.KObj(recommendation)) + } else { + klog.Infof("Create recommendation %s", klog.KObj(recommendation)) + if err := c.Create(ctx, recommendation); err != nil { + mission.Message = fmt.Sprintf("Failed to create recommendation %s: %v", klog.KObj(recommendation), err) + return + } + + klog.Infof("Successful to create Recommendation %s", klog.KObj(recommendation)) + } + + mission.Message = "Success" + mission.UID = recommendation.UID + mission.Name = recommendation.Name + mission.Namespace = recommendation.Namespace + mission.Kind = recommendation.Kind + mission.APIVersion = recommendation.APIVersion + } +} + func (c *Controller) UpdateStatus(ctx context.Context, analytics *analysisv1alph1.Analytics, newStatus *analysisv1alph1.AnalyticsStatus) { if !equality.Semantic.DeepEqual(&analytics.Status, newStatus) { analytics.Status = *newStatus diff --git a/pkg/controller/ehpa/effective_hpa_controller.go b/pkg/controller/ehpa/effective_hpa_controller.go index a487e392c..efc3a5d3c 100644 --- a/pkg/controller/ehpa/effective_hpa_controller.go +++ b/pkg/controller/ehpa/effective_hpa_controller.go @@ -17,10 +17,12 @@ import ( "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" autoscalingapi "github.com/gocrane/api/autoscaling/v1alpha1" predictionapi "github.com/gocrane/api/prediction/v1alpha1" + "github.com/gocrane/crane/pkg/known" "github.com/gocrane/crane/pkg/metrics" "github.com/gocrane/crane/pkg/utils" ) @@ -102,6 +104,26 @@ func (c *EffectiveHPAController) Reconcile(ctx context.Context, req ctrl.Request setHPACondition(newStatus, hpa.Status.Conditions) + // sync custom metric to annotations + if hpa.Status.CurrentMetrics != nil { + var currentMetrics string + if ehpa.Annotations == nil { + ehpa.Annotations = map[string]string{} + } + currentMetrics = ehpa.Annotations[known.EffectiveHorizontalPodAutoscalerCurrentMetricsAnnotation] + + valueBytes, err := yaml.Marshal(hpa.Status.CurrentMetrics) + if err == nil && currentMetrics != string(valueBytes) { + ehpa.Annotations[known.EffectiveHorizontalPodAutoscalerCurrentMetricsAnnotation] = string(valueBytes) + klog.V(4).Infof("Updating ehpa %s current metrics: %s.", klog.KObj(ehpa), string(valueBytes)) + err := c.Client.Update(ctx, ehpa) + if err != nil { + klog.Errorf("Failed to update current metrics for ehpa %s: %v", klog.KObj(ehpa), err) + } + klog.Infof("Updated ehpa %s current metrics: %s.", klog.KObj(ehpa), string(valueBytes)) + } + } + // scale target to its specific replicas for Preview strategy if ehpa.Spec.ScaleStrategy == autoscalingapi.ScaleStrategyPreview && ehpa.Spec.SpecificReplicas != nil && *ehpa.Spec.SpecificReplicas != scale.Status.Replicas { scale.Spec.Replicas = *ehpa.Spec.SpecificReplicas diff --git a/pkg/controller/recommendation/recommendation_controller.go b/pkg/controller/recommendation/recommendation_controller.go index e4c3a1077..479aa52a2 100644 --- a/pkg/controller/recommendation/recommendation_controller.go +++ b/pkg/controller/recommendation/recommendation_controller.go @@ -3,7 +3,6 @@ package recommendation import ( "context" "fmt" - "time" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -17,9 +16,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" analysisv1alph1 "github.com/gocrane/api/analysis/v1alpha1" + predictormgr "github.com/gocrane/crane/pkg/predictor" "github.com/gocrane/crane/pkg/providers" - "github.com/gocrane/crane/pkg/recommend" ) // Controller is responsible for reconcile Recommendation @@ -47,89 +46,31 @@ func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, nil } - shouldRecommend := c.ShouldRecommend(recommendation) - if !shouldRecommend { - klog.V(4).Infof("Nothing happens for Recommendation %s", req.NamespacedName) - return ctrl.Result{}, nil - } - // defaulting for TargetRef.Namespace if recommendation.Spec.TargetRef.Namespace == "" { recommendation.Spec.TargetRef.Namespace = recommendation.Namespace } - c.DoRecommend(ctx, recommendation) - - if recommendation.Spec.CompletionStrategy.CompletionStrategyType == analysisv1alph1.CompletionStrategyPeriodical { - if recommendation.Spec.CompletionStrategy.PeriodSeconds != nil { - d := time.Second * time.Duration(*recommendation.Spec.CompletionStrategy.PeriodSeconds) - klog.V(4).InfoS("Will re-sync", "after", d) - return ctrl.Result{ - RequeueAfter: d, - }, nil - } - } - return ctrl.Result{}, nil -} - -// ShouldRecommend decide if we need do recommendation according to status -func (c *Controller) ShouldRecommend(recommendation *analysisv1alph1.Recommendation) bool { - lastUpdateTime := recommendation.Status.LastUpdateTime - - if recommendation.Spec.CompletionStrategy.CompletionStrategyType == analysisv1alph1.CompletionStrategyOnce { - if lastUpdateTime != nil { - // already finish recommendation - return false - } - } else { - if lastUpdateTime != nil { - planingTime := lastUpdateTime.Add(time.Duration(*recommendation.Spec.CompletionStrategy.PeriodSeconds) * time.Second) - if time.Now().Before(planingTime) { - return false - } - } - } - - return true -} - -func (c *Controller) DoRecommend(ctx context.Context, recommendation *analysisv1alph1.Recommendation) { - klog.V(4).Infof("Starting to process Recommendation %s", klog.KObj(recommendation)) - newStatus := recommendation.Status.DeepCopy() - recommender, err := recommend.NewRecommender(c.Client, c.RestMapper, c.ScaleClient, recommendation, c.PredictorMgr, c.Provider, c.ConfigSet) + updated, err := c.UpdateRecommendation(ctx, recommendation) if err != nil { - c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedCreateRecommender", err.Error()) - msg := fmt.Sprintf("Failed to create recommender, Recommendation %s error %v", klog.KObj(recommendation), err) + c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedUpdateRecommendationValue", err.Error()) + msg := fmt.Sprintf("Failed to update recommendation value, Recommendation %s: %v", klog.KObj(recommendation), err) klog.Errorf(msg) - setReadyCondition(newStatus, metav1.ConditionFalse, "FailedCreateRecommender", msg) + setReadyCondition(newStatus, metav1.ConditionFalse, "FailedUpdateRecommendationValue", msg) c.UpdateStatus(ctx, recommendation, newStatus) - return + return ctrl.Result{}, err } - proposed, err := recommender.Offer() - if err != nil { - c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedOfferRecommendation", err.Error()) - msg := fmt.Sprintf("Failed to offer recommend, Recommendation %s: %v", klog.KObj(recommendation), err) - klog.Errorf(msg) - setReadyCondition(newStatus, metav1.ConditionFalse, "FailedOfferRecommend", msg) - c.UpdateStatus(ctx, recommendation, newStatus) - return - } + if updated { + c.Recorder.Event(recommendation, v1.EventTypeNormal, "UpdatedRecommendationValue", "") - err = c.UpdateRecommendation(ctx, recommendation, proposed, newStatus) - if err != nil { - c.Recorder.Event(recommendation, v1.EventTypeWarning, "FailedUpdateRecommendationValue", err.Error()) - msg := fmt.Sprintf("Failed to update recommendation value, Recommendation %s: %v", klog.KObj(recommendation), err) - klog.Errorf(msg) - setReadyCondition(newStatus, metav1.ConditionFalse, "FailedUpdateRecommendationValue", msg) + setReadyCondition(newStatus, metav1.ConditionTrue, "RecommendationReady", "Recommendation is ready") c.UpdateStatus(ctx, recommendation, newStatus) - return } - setReadyCondition(newStatus, metav1.ConditionTrue, "RecommendationReady", "Recommendation is ready") - c.UpdateStatus(ctx, recommendation, newStatus) + return ctrl.Result{}, nil } func (c *Controller) UpdateStatus(ctx context.Context, recommendation *analysisv1alph1.Recommendation, newStatus *analysisv1alph1.RecommendationStatus) { diff --git a/pkg/controller/recommendation/updater.go b/pkg/controller/recommendation/updater.go index 5dd79b8f0..93b74fce2 100644 --- a/pkg/controller/recommendation/updater.go +++ b/pkg/controller/recommendation/updater.go @@ -18,29 +18,31 @@ import ( autoscalingapi "github.com/gocrane/api/autoscaling/v1alpha1" "github.com/gocrane/crane/pkg/known" - "github.com/gocrane/crane/pkg/recommend/types" + recommendtypes "github.com/gocrane/crane/pkg/recommend/types" "github.com/gocrane/crane/pkg/utils" ) -func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *analysisapi.Recommendation, proposed *types.ProposedRecommendation, status *analysisapi.RecommendationStatus) error { - var value string - if proposed.ResourceRequest != nil { - valueBytes, err := yaml.Marshal(proposed.ResourceRequest) +func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *analysisapi.Recommendation) (bool, error) { + var proposedEHPA recommendtypes.EffectiveHorizontalPodAutoscalerRecommendation + var proposedResource recommendtypes.ProposedRecommendation + needUpdate := false + + if recommendation.Spec.Type == analysisapi.AnalysisTypeResource { + err := yaml.Unmarshal([]byte(recommendation.Status.RecommendedValue), &proposedResource) if err != nil { - return err + return false, err } - value = string(valueBytes) - } else if proposed.EffectiveHPA != nil { - valueBytes, err := yaml.Marshal(proposed.EffectiveHPA) + } + + if recommendation.Spec.Type == analysisapi.AnalysisTypeHPA { + err := yaml.Unmarshal([]byte(recommendation.Status.RecommendedValue), &proposedEHPA) if err != nil { - return err + return false, err } - value = string(valueBytes) } - status.RecommendedValue = value if recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeStatus { - return nil + return false, nil } unstructed := &unstructured.Unstructured{} @@ -48,7 +50,7 @@ func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *a unstructed.SetKind(recommendation.Spec.TargetRef.Kind) err := c.Client.Get(ctx, client.ObjectKey{Name: recommendation.Spec.TargetRef.Name, Namespace: recommendation.Spec.TargetRef.Namespace}, unstructed) if err != nil { - return fmt.Errorf("get target object failed: %v. ", err) + return false, fmt.Errorf("get target object failed: %v. ", err) } if recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeStatusAndAnnotation || recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeAuto { @@ -59,24 +61,32 @@ func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *a switch recommendation.Spec.Type { case analysisapi.AnalysisTypeResource: - annotation[known.ResourceRecommendationValueAnnotation] = value + if annotation[known.ResourceRecommendationValueAnnotation] != recommendation.Status.RecommendedValue { + annotation[known.ResourceRecommendationValueAnnotation] = recommendation.Status.RecommendedValue + needUpdate = true + } case analysisapi.AnalysisTypeHPA: - annotation[known.HPARecommendationValueAnnotation] = value + if annotation[known.HPARecommendationValueAnnotation] != recommendation.Status.RecommendedValue { + annotation[known.HPARecommendationValueAnnotation] = recommendation.Status.RecommendedValue + needUpdate = true + } } - unstructed.SetAnnotations(annotation) - err = c.Client.Update(ctx, unstructed) - if err != nil { - return fmt.Errorf("update target annotation failed: %v. ", err) + if needUpdate { + unstructed.SetAnnotations(annotation) + err = c.Client.Update(ctx, unstructed) + if err != nil { + return false, fmt.Errorf("update target annotation failed: %v. ", err) + } } } // Only support Auto Type for EHPA recommendation if recommendation.Spec.AdoptionType == analysisapi.AdoptionTypeAuto { - if proposed.EffectiveHPA != nil { + if recommendation.Spec.Type == analysisapi.AnalysisTypeHPA { ehpa, err := utils.GetEHPAFromScaleTarget(ctx, c.Client, recommendation.Spec.TargetRef.Namespace, recommendation.Spec.TargetRef) if err != nil { - return fmt.Errorf("get EHPA from target failed: %v. ", err) + return false, fmt.Errorf("get EHPA from target failed: %v. ", err) } if ehpa == nil { ehpa = &autoscalingapi.EffectiveHorizontalPodAutoscaler{ @@ -85,11 +95,11 @@ func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *a Name: recommendation.Spec.TargetRef.Name, }, Spec: autoscalingapi.EffectiveHorizontalPodAutoscalerSpec{ - MinReplicas: proposed.EffectiveHPA.MinReplicas, - MaxReplicas: *proposed.EffectiveHPA.MaxReplicas, - Metrics: proposed.EffectiveHPA.Metrics, + MinReplicas: proposedEHPA.MinReplicas, + MaxReplicas: *proposedEHPA.MaxReplicas, + Metrics: proposedEHPA.Metrics, ScaleStrategy: autoscalingapi.ScaleStrategyPreview, - Prediction: proposed.EffectiveHPA.Prediction, + Prediction: proposedEHPA.Prediction, ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Kind: recommendation.Spec.TargetRef.Kind, APIVersion: recommendation.Spec.TargetRef.APIVersion, @@ -98,43 +108,44 @@ func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *a }, } - err = c.Client.Create(ctx, ehpa) - if err == nil { - c.Recorder.Event(ehpa, v1.EventTypeNormal, "UpdateValue", "Created EffectiveHorizontalPodAutoscaler.") - klog.Infof("Create EffectiveHorizontalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation)) + if err = c.Client.Create(ctx, ehpa); err == nil { + return false, err } - return err + c.Recorder.Event(ehpa, v1.EventTypeNormal, "UpdateValue", "Created EffectiveHorizontalPodAutoscaler.") + klog.Infof("Create EffectiveHorizontalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation)) + needUpdate = true } else { // we don't override ScaleStrategy, because we always use preview to be the default version, // if user change it, we don't want to override it. // The reason for Prediction is the same. ehpaUpdate := ehpa.DeepCopy() - ehpaUpdate.Spec.MinReplicas = proposed.EffectiveHPA.MinReplicas - ehpaUpdate.Spec.MaxReplicas = *proposed.EffectiveHPA.MaxReplicas - ehpaUpdate.Spec.Metrics = proposed.EffectiveHPA.Metrics + ehpaUpdate.Spec.MinReplicas = proposedEHPA.MinReplicas + ehpaUpdate.Spec.MaxReplicas = *proposedEHPA.MaxReplicas + ehpaUpdate.Spec.Metrics = proposedEHPA.Metrics if !equality.Semantic.DeepEqual(&ehpaUpdate.Spec, &ehpa.Spec) { - err = c.Client.Update(ctx, ehpaUpdate) - if err == nil { - c.Recorder.Event(ehpa, v1.EventTypeNormal, "UpdateValue", "Updated EffectiveHorizontalPodAutoscaler.") - klog.Infof("Update EffectiveHorizontalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation)) + if err = c.Client.Update(ctx, ehpaUpdate); err != nil { + return false, err + } - return err + c.Recorder.Event(ehpa, v1.EventTypeNormal, "UpdateValue", "Updated EffectiveHorizontalPodAutoscaler.") + klog.Infof("Update EffectiveHorizontalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation)) + needUpdate = true } } } - if proposed.ResourceRequest != nil { + if recommendation.Spec.Type == analysisapi.AnalysisTypeResource { evpa, err := utils.GetEVPAFromScaleTarget(ctx, c.Client, recommendation.Spec.TargetRef.Namespace, recommendation.Spec.TargetRef) if err != nil { - return fmt.Errorf("get EVPA from target failed: %v. ", err) + return false, fmt.Errorf("get EVPA from target failed: %v. ", err) } if evpa == nil { off := vpatypes.UpdateModeOff var policies []autoscalingapi.ContainerResourcePolicy podTemplate, err := utils.GetPodTemplate(ctx, evpa.Namespace, evpa.Spec.TargetRef.Name, evpa.Spec.TargetRef.Kind, evpa.Spec.TargetRef.APIVersion, c.Client) if err != nil { - return err + return false, err } for _, container := range podTemplate.Spec.Containers { @@ -164,16 +175,16 @@ func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *a }, } - err = c.Client.Create(ctx, evpa) - if err == nil { - c.Recorder.Event(evpa, v1.EventTypeNormal, "UpdateValue", "Created EffectiveVerticalPodAutoscaler.") - klog.Infof("Create EffectiveVerticalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation)) + if err = c.Client.Create(ctx, evpa); err != nil { + return false, err } - return err + c.Recorder.Event(evpa, v1.EventTypeNormal, "UpdateValue", "Created EffectiveVerticalPodAutoscaler.") + klog.Infof("Create EffectiveVerticalPodAutoscaler successfully, recommendation %s", klog.KObj(recommendation)) + needUpdate = true } // no need to update evpa now } } - return nil + return needUpdate, nil } diff --git a/pkg/known/annotation.go b/pkg/known/annotation.go index cc0ee510f..46d64da5c 100644 --- a/pkg/known/annotation.go +++ b/pkg/known/annotation.go @@ -4,3 +4,7 @@ const ( HPARecommendationValueAnnotation = "analysis.crane.io/hpa-recommendation" ResourceRecommendationValueAnnotation = "analysis.crane.io/resource-recommendation" ) + +const ( + EffectiveHorizontalPodAutoscalerCurrentMetricsAnnotation = "autoscaling.crane.io/effective-hpa-current-metrics" +) diff --git a/pkg/known/label.go b/pkg/known/label.go index c33988061..7334f8d97 100644 --- a/pkg/known/label.go +++ b/pkg/known/label.go @@ -9,3 +9,9 @@ const ( EnsuranceAnalyzedPressureTaintKey = "ensurance.crane.io/analyzed-pressure" EnsuranceAnalyzedPressureConditionKey = "analyzed-pressure" ) + +const ( + AnalyticsNameLabel = "analysis.crane.io/analytics-name" + AnalyticsUidLabel = "analysis.crane.io/analytics-uid" + AnalyticsTypeLabel = "analysis.crane.io/analytics-type" +) diff --git a/pkg/recommend/advisor/ehpa.go b/pkg/recommend/advisor/ehpa.go index 8af654d60..5f209b374 100644 --- a/pkg/recommend/advisor/ehpa.go +++ b/pkg/recommend/advisor/ehpa.go @@ -240,6 +240,7 @@ func (a *EHPAAdvisor) minMaxMedians(predictionTs []*common.TimeSeries) (float64, medianMin = value } } + klog.V(4).Infof("EHPAAdvisor minMaxMedians medianMax %f, medianMin %f, medianUsages %v", medianMax, medianMin, medianUsages) return medianMin, medianMax, nil @@ -253,7 +254,7 @@ func (a *EHPAAdvisor) checkFluctuation(medianMin, medianMax float64) error { } if medianMin == 0 { - return fmt.Errorf("mean cpu usage is zero. ") + medianMin = 0.1 // use a small value to continue calculate } fluctuation := medianMax / medianMin diff --git a/pkg/recommend/config.go b/pkg/recommend/config.go index f3e084136..406371709 100644 --- a/pkg/recommend/config.go +++ b/pkg/recommend/config.go @@ -43,7 +43,7 @@ func loadConfigSetFromBytes(configSetBytes []byte) (*analysisv1alpha1.ConfigSet, return configSet, nil } -func GetProperties(configSet *analysisv1alpha1.ConfigSet, dst analysisv1alpha1.Target) map[string]string { +func GetProperties(configSet *analysisv1alpha1.ConfigSet, dst analysisv1alpha1.Target, analyticsConfig map[string]string) map[string]string { var selectedProps map[string]string maxMatchLevel := -1 for i, config := range configSet.Configs { @@ -60,6 +60,14 @@ func GetProperties(configSet *analysisv1alpha1.ConfigSet, dst analysisv1alpha1.T } } } + + // override by analytics config + if analyticsConfig != nil { + for k, v := range analyticsConfig { + selectedProps[k] = v + } + } + bytes, _ := json.Marshal(dst) klog.V(4).Infof("Got properties %v for target %s", selectedProps, string(bytes)) return selectedProps diff --git a/pkg/recommend/recommender.go b/pkg/recommend/recommender.go index 609e60c29..03bc06bbb 100644 --- a/pkg/recommend/recommender.go +++ b/pkg/recommend/recommender.go @@ -29,8 +29,8 @@ import ( func NewRecommender(kubeClient client.Client, restMapper meta.RESTMapper, scaleClient scale.ScalesGetter, recommendation *analysisapi.Recommendation, predictorMgr predictormgr.Manager, dataSource providers.History, - configSet *analysisapi.ConfigSet) (*Recommender, error) { - c, err := GetContext(kubeClient, restMapper, scaleClient, recommendation, predictorMgr, dataSource, configSet) + configSet *analysisapi.ConfigSet, analyticsConfig map[string]string) (*Recommender, error) { + c, err := GetContext(kubeClient, restMapper, scaleClient, recommendation, predictorMgr, dataSource, configSet, analyticsConfig) if err != nil { return nil, err } @@ -93,7 +93,7 @@ func (r *Recommender) Offer() (proposed *types.ProposedRecommendation, err error func GetContext(kubeClient client.Client, restMapper meta.RESTMapper, scaleClient scale.ScalesGetter, recommendation *analysisapi.Recommendation, predictorMgr predictormgr.Manager, dataSource providers.History, - configSet *analysisapi.ConfigSet) (*types.Context, error) { + configSet *analysisapi.ConfigSet, analyticsConfig map[string]string) (*types.Context, error) { c := &types.Context{} targetRef := autoscalingv2.CrossVersionObjectReference{ @@ -107,7 +107,7 @@ func GetContext(kubeClient client.Client, restMapper meta.RESTMapper, Namespace: recommendation.Spec.TargetRef.Namespace, Name: recommendation.Spec.TargetRef.Name, } - c.ConfigProperties = GetProperties(configSet, target) + c.ConfigProperties = GetProperties(configSet, target, analyticsConfig) var scale *autoscalingv1.Scale var mapping *meta.RESTMapping From 34490557c5edf986d36960341b95121e338a8228 Mon Sep 17 00:00:00 2001 From: qmhu Date: Tue, 17 May 2022 17:25:15 +0800 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: zsnmwy <35299017+zsnmwy@users.noreply.github.com> --- .../tutorials/analytics-and-recommendation.md | 4 +-- docs/tutorials/hpa-recommendation.md | 35 ++++++++++++------- docs/tutorials/hpa-recommendation.zh.md | 31 ++++++++++------ docs/tutorials/resource-recommendation.md | 26 ++++++++++---- docs/tutorials/resource-recommendation.zh.md | 24 +++++++++---- ...ve-hpa-to-scaling-with-effectiveness.zh.md | 2 +- 6 files changed, 84 insertions(+), 38 deletions(-) diff --git a/docs/tutorials/analytics-and-recommendation.md b/docs/tutorials/analytics-and-recommendation.md index 31bbe6284..6d30ca5bf 100644 --- a/docs/tutorials/analytics-and-recommendation.md +++ b/docs/tutorials/analytics-and-recommendation.md @@ -15,8 +15,8 @@ Two Recommendations are currently supported: 1. Users create `Analytics` object and config ResourceSelector to select resources to be analyzed. Multiple types of resource selection (based on Group,Kind, and Version) are supported. 2. Analyze each selected resource in parallel and try to execute analysis and give recommendation. Each analysis process is divided into two stages: inspecting and advising: - 1. Inspecting: Filter resources that don't match the recommended conditions. For example, for hpa recommendation, the workload that have many not running pod is excluded - 2. Advising: Analysis and calculate based on algorithm model then provide the recommendation result. + 1. Inspecting: Filter resources that don't match the recommended conditions. For example, for hpa recommendation, the workload that has many not running pod is excluded + 2. Advising: Analysis and calculation based on algorithm model then provide the recommendation result. 3. If you paas the above two stages, it will create `Recommendation` object and display the result in `recommendation.Status` 4. You can find the failure reasons from `analytics.status.recommendations` 5. Wait for the next analytics based on the interval diff --git a/docs/tutorials/hpa-recommendation.md b/docs/tutorials/hpa-recommendation.md index d578a276d..80b45e0f7 100644 --- a/docs/tutorials/hpa-recommendation.md +++ b/docs/tutorials/hpa-recommendation.md @@ -1,16 +1,27 @@ # HPA Recommendation -Using hpa recommendations, you can find resources in the cluster that are suitable for autoscaling, and use Crane's recommended result to create autoscaling object: [Effective HorizontalPodAutoscaler](tutorials/using-time-series-prediction.md) +Using hpa recommendations, you can find resources in the cluster that are suitable for autoscaling, and use Crane's recommended result to create an autoscaling object: [Effective HorizontalPodAutoscaler](using-time-series-prediction.md). ## Create HPA Analytics Create an **Resource** `Analytics` to give recommendation for deployment: `nginx-deployment` as a sample. -```bash -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-hpa.yaml -kubectl get analytics -``` +=== "Main" + + ```bash + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-hpa.yaml + kubectl get analytics + ``` + +=== "Mirror" + + ```bash + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/nginx-deployment.yaml?download=false + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/analytics-hpa.yaml?download=false + kubectl get analytics + ``` + ```yaml title="analytics-hpa.yaml" apiVersion: analysis.crane.io/v1alpha1 @@ -39,7 +50,7 @@ NAME AGE nginx-hpa 16m ``` -You can get created recommendation from analytics status: +You can get created recommendations from analytics status: ```bash kubectl get analytics nginx-hpa -o yaml @@ -47,7 +58,7 @@ kubectl get analytics nginx-hpa -o yaml The output is similar to: -```yaml +```yaml hl_lines="32" apiVersion: analysis.crane.io/v1alpha1 kind: Analytics metadata: @@ -154,10 +165,10 @@ status: ### Inspecting -1. Workload with low replicas: If the replicas is too low, may not be suitable for hpa recommendation. Associated configuration: ehpa.deployment-min-replicas | ehpa.statefulset-min-replicas | ehpa.workload-min-replicas -2. Workload with a certain percentage of not running pods: if the workload of Pod mostly can't run normally, may not be suitable for flexibility. Associated configuration: ehpa.pod-min-ready-seconds | ehpa.pod-available-ratio -3. Workload with low cpu usage: The low CPU usage workload means that there is no load pressure. In this case, we can't estimate it. Associated configuration: ehpa.min-cpu-usage-threshold -4. Workload with low fluctuation of cpu usage: dividing of the maximum and minimum usage is defined as the fluctuation rate. If the fluctuation rate is too low, the workload will not benefit much from hpa. Associated configuration: ehpa.fluctuation-threshold +1. Workload with low replicas: If the replicas is too low, may not be suitable for hpa recommendation. Associated configuration: `ehpa.deployment-min-replicas` | `ehpa.statefulset-min-replicas` | `ehpa.workload-min-replicas` +2. Workload with a certain percentage of not running pods: if the workload of Pod mostly can't run normally, may not be suitable for flexibility. Associated configuration: `ehpa.pod-min-ready-seconds` | `ehpa.pod-available-ratio` +3. Workload with low CPU usage: The low CPU usage workload means that there is no load pressure. In this case, we can't estimate it. Associated configuration: `ehpa.min-cpu-usage-threshold` +4. Workload with low fluctuation of CPU usage: dividing of the maximum and minimum usage is defined as the fluctuation rate. If the fluctuation rate is too low, the workload will not benefit much from hpa. Associated configuration: `ehpa.fluctuation-threshold` ### Advising diff --git a/docs/tutorials/hpa-recommendation.zh.md b/docs/tutorials/hpa-recommendation.zh.md index 2a9f6fd8c..b2baee76a 100644 --- a/docs/tutorials/hpa-recommendation.zh.md +++ b/docs/tutorials/hpa-recommendation.zh.md @@ -6,11 +6,22 @@ 创建一个**弹性分析** `Analytics`,这里我们通过实例 deployment: `nginx` 作为一个例子 -```bash -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-hpa.yaml -kubectl get analytics -``` +=== "Main" + + ```bash + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-hpa.yaml + kubectl get analytics + ``` + +=== "Mirror" + + ```bash + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/nginx-deployment.yaml?download=false + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/analytics-hpa.yaml?download=false + kubectl get analytics + ``` + ```yaml title="analytics-hpa.yaml" apiVersion: analysis.crane.io/v1alpha1 @@ -47,7 +58,7 @@ kubectl get analytics nginx-hpa -o yaml 结果如下: -```yaml +```yaml hl_lines="32" apiVersion: analysis.crane.io/v1alpha1 kind: Analytics metadata: @@ -154,10 +165,10 @@ status: ### 筛选阶段 -1. 低副本数的工作负载: 过低的副本数可能弹性需求不高,关联配置: ehpa.deployment-min-replicas | ehpa.statefulset-min-replicas | ehpa.workload-min-replicas -2. 存在一定比例非 Running Pod 的工作负载: 如果工作负载的 Pod 大多不能正常运行,可能不适合弹性,关联配置: ehpa.pod-min-ready-seconds | ehpa.pod-available-ratio -3. 低 CPU 使用量的工作负载: 过低使用量的工作负载意味着没有业务压力,此时通过使用率推荐弹性不准,关联配置: ehpa.min-cpu-usage-threshold -4. CPU 使用量的波动率过低: 使用量的最大值和最小值的倍数定义为波动率,波动率过低的工作负载通过弹性降本的收益不大,关联配置: ehpa.fluctuation-threshold +1. 低副本数的工作负载: 过低的副本数可能弹性需求不高,关联配置: `ehpa.deployment-min-replicas` | `ehpa.statefulset-min-replicas` | `ehpa.workload-min-replicas` +2. 存在一定比例非 Running Pod 的工作负载: 如果工作负载的 Pod 大多不能正常运行,可能不适合弹性,关联配置: `ehpa.pod-min-ready-seconds` | `ehpa.pod-available-ratio` +3. 低 CPU 使用量的工作负载: 过低使用量的工作负载意味着没有业务压力,此时通过使用率推荐弹性不准,关联配置: `ehpa.min-cpu-usage-threshold` +4. CPU 使用量的波动率过低: 使用量的最大值和最小值的倍数定义为波动率,波动率过低的工作负载通过弹性降本的收益不大,关联配置: `ehpa.fluctuation-threshold` ### 推荐 diff --git a/docs/tutorials/resource-recommendation.md b/docs/tutorials/resource-recommendation.md index 6eb46324b..bbc50b077 100644 --- a/docs/tutorials/resource-recommendation.md +++ b/docs/tutorials/resource-recommendation.md @@ -6,11 +6,23 @@ Resource recommendation allows you to obtain recommended values for resources in Create an **Resource** `Analytics` to give recommendation for deployment: `nginx-deployment` as a sample. -```bash -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-resource.yaml -kubectl get analytics -n crane-system -``` + +=== "Main" + + ```bash + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-resource.yaml + kubectl get analytics -n crane-system + ``` + +=== "Mirror" + + ```bash + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/nginx-deployment.yaml?download=false + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/analytics-resource.yaml?download=false + kubectl get analytics -n crane-system + ``` + ```yaml title="analytics-resource.yaml" hl_lines="7 24 11-14 28-31" apiVersion: analysis.crane.io/v1alpha1 @@ -43,7 +55,7 @@ kubectl get analytics nginx-resource -o yaml The output is similar to: -```yaml +```yaml hl_lines="27" apiVersion: analysis.crane.io/v1alpha1 kind: Analytics metadata: @@ -85,7 +97,7 @@ The recommendation name presents on `status.recommendations[0].name`. Then you c ## Recommendation: Analytics result ```bash -kubectl get recommend -n crane-system craned-resource-resource-j7shb -o yaml +kubectl get recommend -n crane-system nginx-resource-resource-w45nq -o yaml ``` The output is similar to: diff --git a/docs/tutorials/resource-recommendation.zh.md b/docs/tutorials/resource-recommendation.zh.md index 2a1d65497..36fe16e29 100644 --- a/docs/tutorials/resource-recommendation.zh.md +++ b/docs/tutorials/resource-recommendation.zh.md @@ -6,11 +6,23 @@ 创建一个**资源分析** `Analytics`,这里我们通过实例 deployment: `nginx` 作为一个例子 -```bash -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml -kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-resource.yaml -kubectl get analytics -``` + +=== "Main" + + ```bash + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/nginx-deployment.yaml + kubectl apply -f https://raw.githubusercontent.com/gocrane/crane/main/examples/analytics/analytics-resource.yaml + kubectl get analytics + ``` + +=== "Mirror" + + ```bash + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/nginx-deployment.yaml?download=false + kubectl apply -f https://finops.coding.net/p/gocrane/d/crane/git/raw/main/examples/analytics/analytics-resource.yaml?download=false + kubectl get analytics + ``` + ```yaml title="analytics-resource.yaml" apiVersion: analysis.crane.io/v1alpha1 @@ -43,7 +55,7 @@ kubectl get analytics nginx-resource -o yaml 结果如下: -```yaml +```yaml hl_lines="27" apiVersion: analysis.crane.io/v1alpha1 kind: Analytics metadata: diff --git a/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md b/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md index 9037ac4a3..0347177ab 100644 --- a/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md +++ b/docs/tutorials/using-effective-hpa-to-scaling-with-effectiveness.zh.md @@ -213,8 +213,8 @@ status: ``` 原因:不是所有的工作负载的 CPU 使用率都是可预测的,当无法预测时就会显示以上错误。 -reason: Not all workload's cpu metric are predictable, if predict your workload failed, it will show above errors. 解决方案: + - 等一段时间再看。预测算法 `DSP` 需要一定时间的数据才能进行预测。希望了解算法细节的可以查看算法的文档。 - EffectiveHorizontalPodAutoscaler 提供一种保护机制,当预测失效时依然能通过实际的 CPU 使用率工作。 \ No newline at end of file