From 1b24d1a70f4a5f7495c091a55a43cf53bb7e41c2 Mon Sep 17 00:00:00 2001 From: Arsalan Khan Date: Tue, 31 Dec 2024 19:54:22 +0100 Subject: [PATCH] Add CPU Utilization (cpuutil) Metric Example (#54) * CPUutil metric example * add docs --- .tool-versions | 4 +- Makefile | 51 ++++- README.md | 216 +-------------------- app/server/app_handler.go | 96 ++++++++- app/server/cpu.go | 66 +++++++ app/server/server.go | 14 +- deploy/app-manifest.yml | 6 +- deploy/cpu-utilization-policy.json | 22 +++ deploy/custom-metrics-policy-explicit.json | 22 +++ docs/cpu-utilization.md | 16 ++ docs/custom-metrics-mtls.md | 212 ++++++++++++++++++++ go.mod | 23 +-- go.sum | 39 ++-- main.go | 13 +- 14 files changed, 526 insertions(+), 274 deletions(-) create mode 100644 app/server/cpu.go create mode 100644 deploy/cpu-utilization-policy.json create mode 100644 deploy/custom-metrics-policy-explicit.json create mode 100644 docs/cpu-utilization.md create mode 100644 docs/custom-metrics-mtls.md diff --git a/.tool-versions b/.tool-versions index df55863..5883dba 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -ginkgo 2.5.1 -golang 1.19.3 \ No newline at end of file +ginkgo 2.22.0 +golang 1.22.5 \ No newline at end of file diff --git a/Makefile b/Makefile index 34c6bff..09bbfa7 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,49 @@ SHELL := /bin/bash -.SHELLFLAGS := -eu -o pipefail -c ${SHELLFLAGS} +.SHELLFLAGS := -euo pipefail -c ${SHELLFLAGS} GINKGO_OPTS=-r --race --require-suite --randomize-all --cover ${OPTS} +# Metrics +cpu_app_name :=golang-autoscaler-cpuutil-metric +custom_metric_app_name :=golang-autoscaler-custom-metrics -.PHONY: build -build: - echo "# building app" - rm -rf deploy/build/* || true - mkdir -p deploy/build/ - go mod tidy +.PHONY: build-for-custom-metrics +build: clean + @echo "# building app ${custom_metric_app_name}" + @go mod tidy # build for linux/amd64 - env GOOS=linux GOARCH=amd64 go build -o deploy/build/golang-autoscaler-custom-metrics + @env GOOS=linux GOARCH=amd64 go build -o deploy/build/${custom_metric_app_name} + +.PHONY: build-for-cpu +build-for-cpu: clean + @echo "# building app ${cpu_app_name}" + @go mod tidy + # build for linux/amd64 + @env GOOS=linux GOARCH=amd64 go build -o deploy/build/${cpu_app_name} + +build-for-arm: clean + @echo "# building app ${custom_metric_app_name}" + @go mod tidy + @go build -o deploy/build/${custom_metric_app_name} + + + +# deploy app with cpuutil metric on cloud foundry +deploy-with-cpu: build-for-cpu + @echo "# deploying app ${cpu_app_name}" + @cf push -f deploy/app-manifest.yml -p deploy/build --no-start --var app_name=${cpu_app_name} + @cf create-service autoscaler standard ak-test-autoscaler-${cpu_app_name} + @cf bind-service ${cpu_app_name} ak-test-autoscaler-${cpu_app_name} -c deploy/cpu-utilization-policy.json + @cf start ${cpu_app_name} + +# remove all created resources in cf for app +clean-cpu-app: + @echo "# removing all created resources in cf for app ${cpu_app_name}" + @cf delete-service -f ak-test-autoscaler${cpu_app_name} + @cf delete -f ${cpu_app_name} test: @echo "Running tests" - ginkgo run ${GINKGO_OPTS} + @ginkgo run ${GINKGO_OPTS} buildtools: @echo "# Installing build tools" @@ -24,4 +53,6 @@ buildtools: clean: @echo "# cleaning app" - @rm -rf build \ No newline at end of file + @rm -rf build + @rm -rf deploy/build/* || true + @mkdir -p deploy/build/ diff --git a/README.md b/README.md index d9cdbd0..2114d89 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,10 @@ [![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cf-autoscaler-custom-metrics-mtls)](https://api.reuse.software/info/github.com/SAP-samples/cf-autoscaler-custom-metrics-mtls) -# Demonstrate Custom Metrics Feature in Application Autoscaler - -## Description +# Demonstrate Scaling Metrics in Application Autoscaler The SAP Business Technology Platform (BTP) provides a runtime environment for running your applications at scale. This repository include a sample Golang application that use the Cloud Foundry runtime for SAP BTP. -Check out the step-by-step guide on [SAP Blogs](https://blogs.sap.com/?p=1613870&preview=true&preview_id=1613870) - Application Autoscaler, an autoscaling service in Cloud Foundry Runtime, automatically adds or removes application instances/nodes based on the workload. This can be done by defining an autoscaling policy, containing scaling rules. This repository explains about how to send custom metrics to CF Autoscaler Service using mTLS authentication @@ -16,216 +12,20 @@ This repository explains about how to send custom metrics to CF Autoscaler Servi - Is your Cloud Foundry (CF) application unable to cope with large amounts of requests during peak hours? - Have you seen a decrease in application performance in high-traffic conditions? - Are you looking to reduce your runtime costs when there is less traffic? -- Do you want to scale your CF application based on your custom metrics (other than CPU, memory, throughput, reponsetime) ? - -This tutorial includes the following steps - - Build and Deploy sample Golang Application - - Create Autoscaler Service Instance - - Bind App with Autoscaler Service - - Start Golang App - - Send Custom Metrics to Autoscaler Service - - Monitor Scale-Out and Scale-In - -## Requirements - -- You should have access to Cloud Foundry Runtime - -## Build and Deploy sample Golang Application - -### Build App -#### Option 1 `env GOOS=linux GOARCH=amd64 go build -o build/golang-autoscaler-custom-metrics` -#### Option 2 `make build` - -### Run Test - -`make test` - -### Deploy App - -`cf push --no-start -f deploy/app-manifest.yml -p deploy/build` - -sample-output -```shell -$ cf push --no-start -f deploy/app-manifest.yml -p deploy/build -Pushing app golang-autoscaler-custom-metrics to org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... -Applying manifest file deploy/app-manifest.yml... - -Updating with these attributes... - --- - applications: - - name: golang-autoscaler-custom-metrics - disk-quota: 128M - instances: 1 - path: /Users/development/golang-autoscaler-custom-metrics/deploy/build - memory: 128M -+ default-route: true - stack: cflinuxfs4 - buildpacks: - binary_buildpack - command: ./golang-autoscaler-custom-metrics -Manifest applied -All files found in remote cache; nothing to upload. -Waiting for API to complete processing files... - -name: golang-autoscaler-custom-metrics -requested state: stopped -routes: golang-autoscaler-custom-metrics.cfapps.sap.hana.ondemand.com -last uploaded: -stack: -buildpacks: - -type: web -sidecars: -instances: 0/1 -memory usage: 128M -start command: ./golang-autoscaler-custom-metrics - state since cpu memory disk details -#0 down 2022-12-30T14:22:58Z 0.0% 0 of 0 0 of 0 - -``` - -## Create Autoscaler Service Instance - -```shell -$ cf create-service autoscaler standard ak-test-autoscaler -Creating service instance ak-test-autoscaler in org ak_autoscalerxxx / space ak-test-space as as ak-user@xxx.com... - -Service instance ak-test-autoscaler created. -OK -``` -## Bind App with Autoscaler Service -`$ cf bind-service golang-autoscaler-custom-metrics ak-test-autoscaler -c deploy/custom-metrics-policy.json` - -```shell -$ cf bind-service golang-autoscaler-custom-metrics ak-test-autoscaler -c deploy/custom-metrics-policy.json -Binding service instance ak-test-autoscaler to app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... -OK - -TIP: Use 'cf restage golang-autoscaler-custom-metrics' to ensure your env variable changes take effect - -``` -### Check VCAP_SERVICE Environment Variable - -```shell -$ cf env golang-autoscaler-custom-metrics -Getting env variables for app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... -System-Provided: -VCAP_SERVICES: { - "autoscaler": [ - { - "binding_guid": "c02fba90-83b2-4753-a31f-1d0827978632", - "binding_name": null, - "credentials": { - "custom_metrics": { - "mtls_url": "https://autoscaler-metrics-mtls.cf.xxx.com", - "password": "xxxxxxxxx", - "url": "https://autoscaler-metrics.cf.xxx.com", - "username": "xxxxxxxxx" - } - }, - "instance_guid": "3eeac1a2-ced1-4748-a3b4-a87188a7d3c6", - "instance_name": "ak-test-autoscaler", - "label": "autoscaler", - "name": "ak-test-autoscaler", - "plan": "standard", - "provider": null, - "syslog_drain_url": null, - "tags": [ - "autoscaler", - "app-autoscaler", - "cf-autoscaler" - ], - "volume_mounts": [] - } - ] -} - - -VCAP_APPLICATION: { - ... - .... - ... - -``` -## Start Golang App -`cf start golang-autoscaler-custom-metrics` - - -```shell -cf start golang-autoscaler-custom-metrics -Starting app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... - -Staging app and tracing logs... - Downloading binary_buildpack... - Downloaded binary_buildpack - Cell 038ca894-a748-49d5-85eb-be08569dfe48 creating container for instance 95b8140e-065c-4116-8c42-085f3f33db27 - Cell 038ca894-a748-49d5-85eb-be08569dfe48 successfully created container for instance 95b8140e-065c-4116-8c42-085f3f33db27 - Downloading app package... - Downloading build artifacts cache... - Downloaded build artifacts cache (215B) - Downloaded app package (6M) - -----> Binary Buildpack version 1.0.47 - Exit status 0 - Uploading droplet, build artifacts cache... - Uploading droplet... - Uploading build artifacts cache... - Uploaded build artifacts cache (216B) - -Starting app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... - -Waiting for app to start... - -Instances starting... -Instances starting... -Instances starting... - -name: golang-autoscaler-custom-metrics -requested state: started -routes: golang-autoscaler-custom-metrics.cfapps.sap.hana.ondemand.com -last uploaded: Fri 30 Dec 16:04:06 CET 2022 -stack: cflinuxfs4 -buildpacks: - name version detect output buildpack name - binary_buildpack 1.0.47 binary binary - -type: web -sidecars: -instances: 1/1 -memory usage: 128M - state since cpu memory disk details -#0 running 2022-12-30T15:04:16Z 0.0% 0 of 128M 0 of 128M - -``` - -## Send Custom Metrics to Autoscaler Service +- Do you want to scale your CF application based on your standard metrics (cpu, cpuutil, memory, throughput, reponsetime) or user-defined metrics ? -Scale out > `$ curl https://golang-autoscaler-custom-metrics.cfapps.abc.com/busy/301` -```shell -Every 2.0s: cf app golang-autoscaler-custom-metrics XNQNV6VGJC: Fri Dec 30 16:19:22 2022 -Showing health and status for app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... +This is a sample repo containing golang application, demonstrating the following user cases: -name: golang-autoscaler-custom-metrics -requested state: started -routes: golang-autoscaler-custom-metrics.cfapps.sap.hana.ondemand.com -last uploaded: Fri 30 Dec 16:16:35 CET 2022 -stack: cflinuxfs4 -buildpacks: - name version detect output buildpack name - binary_buildpack 1.0.47 binary binary +- Send custom metrics to Cloud Foundry Autoscaler Service +- Use CPU Utilization to Scale Out and Scale In -type: web -sidecars: -instances: 2/2 -memory usage: 128M - state since cpu memory disk details -#0 running 2022-12-30T15:16:44Z 0.9% 23.6M of 128M 10.9M of 128M -#1 running 2022-12-30T15:18:13Z 0.9% 22.9M of 128M 10.9M of 128M +## Practical Examples -``` +- [Demonstrate Custom Metric MTLS Feature in Application Autoscaler](docs/custom-metrics-mtls.md) +- [Demonstrate CPU Utilization Metric Feature in Application Autoscaler](docs/cpu-utilization.md) -Scale In > `$ curl https://golang-autoscaler-custom-metrics.cfapps.abc.com/not-busy/190` ## License Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This project is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE) file. diff --git a/app/server/app_handler.go b/app/server/app_handler.go index 57fa6f3..34bc007 100644 --- a/app/server/app_handler.go +++ b/app/server/app_handler.go @@ -10,28 +10,36 @@ import ( "fmt" "github.com/cloudfoundry-community/go-cfenv" "github.com/gin-gonic/gin" - "io/ioutil" "log" "net/http" "os" "strconv" + "time" +) + +const ( + cpuLowerBounds = 0 + cpuUpperBounds = 100 ) type AppHandler struct { logger lager.Logger appConfig *cfenv.App + metrics map[string]interface{} metricsServerBaseUrl string } -func NewAppHandler(appConfig *cfenv.App, metricsUrl string) *AppHandler { +func NewAppHandler(appConfig *cfenv.App, metricsUrl string, metrics map[string]interface{}) *AppHandler { return &AppHandler{ logger: lager.NewLogger("appHandler"), appConfig: appConfig, - metricsServerBaseUrl: metricsUrl} + metrics: metrics, + metricsServerBaseUrl: metricsUrl, + } } func (ah *AppHandler) GetHome(context *gin.Context) { - fmt.Sprintf("I am GetHome") + fmt.Printf("I am GetHome") context.JSON(http.StatusOK, gin.H{ "CF_INSTANCE_KEY": os.Getenv("CF_INSTANCE_KEY"), "CF_INSTANCE_CERT": os.Getenv("CF_INSTANCE_CERT"), @@ -39,7 +47,6 @@ func (ah *AppHandler) GetHome(context *gin.Context) { }) } -// Used to mock in the test var SubmitScaleUpEventToAutoscaler = func(logger lager.Logger, appConfig *cfenv.App, metricsValue float64, autoscalerApiServerUrl string) (*http.Response, error) { return postScaleUpEventToAutoscaler(logger, appConfig, metricsValue, autoscalerApiServerUrl) } @@ -54,6 +61,75 @@ func (ah *AppHandler) Busy(context *gin.Context) { ah.sendMetrics("I am busy with value", context) } +func (ah *AppHandler) IncreaseCPU(context *gin.Context) { + + utilizationParam := context.Param("utilization") + minutesParam := context.Param("minutes") + ah.logger.Info("Increasing CPU utilization", lager.Data{"utilization": utilizationParam, "minutes": minutesParam}) + + utilization, err := strconv.Atoi(utilizationParam) + if err != nil || utilization < cpuLowerBounds || utilization > cpuUpperBounds { + context.JSON(http.StatusBadRequest, gin.H{ + "message": fmt.Sprintf("Invalid utilization value. It must be between %d and %d.", cpuUpperBounds, cpuUpperBounds), + }) + return + } + + minutes, err := strconv.Atoi(minutesParam) + if err != nil || minutes <= 0 { + context.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid minutes value. It must be a positive integer.", + }) + return + } + + cpuWaster, ok := ah.metrics["cpu"].(*CPUWaster) + if !ok { + context.JSON(http.StatusInternalServerError, gin.H{ + "message": "CPU waster not configured properly.", + }) + return + } + go func() { + err := cpuWaster.IncreaseCPU(int64(utilization), time.Duration(minutes)*time.Minute) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{ + "message": fmt.Sprintf("Error: %s", err.Error()), + }) + return + } + }() + if !cpuWaster.isRunning { + context.JSON(http.StatusOK, gin.H{ + "message": fmt.Sprintf("Increased CPU utilization to %d%% for %d minutes", utilization, minutes), + }) + } + + return +} + +func (ah *AppHandler) StopCPU(context *gin.Context) { + ah.logger.Info("Stopping CPU utilization") + cpuWaster, ok := ah.metrics["cpu"].(*CPUWaster) + if !ok { + context.JSON(http.StatusInternalServerError, gin.H{ + "message": "CPU waster not configured properly.", + }) + return + } + err := cpuWaster.StopCPU() + if err != nil { + context.JSON(http.StatusConflict, gin.H{ + "message": err.Error(), + }) + return + } + + context.JSON(http.StatusOK, gin.H{ + "message": "Stopped CPU utilization", + }) +} + func (ah *AppHandler) sendMetrics(msg string, context *gin.Context) { param, err := strconv.Atoi(context.Params.ByName("metricValue")) @@ -84,7 +160,6 @@ func (ah *AppHandler) sendMetrics(msg string, context *gin.Context) { }) } -// follow here for POST cert and key : https://smallstep.com/hello-mtls/doc/client/go func postScaleUpEventToAutoscaler(logger lager.Logger, appConfig *cfenv.App, metricsValue float64, autoscalerApiServerUrl string) (*http.Response, error) { if appConfig == nil { return nil, fmt.Errorf("appConfig cannot be empty") @@ -108,7 +183,7 @@ func postScaleUpEventToAutoscaler(logger lager.Logger, appConfig *cfenv.App, met caCertBytes, err := os.ReadFile(cfInstanceCert) if err != nil { log.Printf("Error opening cert file %s, Error: %s", caCertBytes, err) - logger.Error("unable to read CFinstanceCert keypair", err, lager.Data{cfInstanceCert: cfInstanceCert}) + logger.Error("unable to read CFInstanceCert keypair", err, lager.Data{cfInstanceCert: cfInstanceCert}) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCertBytes) @@ -134,7 +209,6 @@ func postScaleUpEventToAutoscaler(logger lager.Logger, appConfig *cfenv.App, met } func sendRequestToAutoscaler(logger lager.Logger, appId string, client *http.Client, autoscalerApiServerUrl string, metricsValueBody []byte, cfInstanceCert string) (*http.Response, error) { - log.Printf("sending POST to autoscaler") logger.Info("sending POST to autoscaler") customMetricsURL := autoscalerApiServerUrl + "/v1/apps/" + appId + "/metrics" logger.Info("sending POST to autoscaler", lager.Data{"autoscalerURL": customMetricsURL}) @@ -146,7 +220,6 @@ func sendRequestToAutoscaler(logger lager.Logger, appId string, client *http.Cli resp, err := client.Do(request) if err != nil { logger.Error("failed sending POST to autoscaler", err) - return nil, fmt.Errorf("unable to send %s %w", request.URL, err) } defer func() { @@ -156,7 +229,10 @@ func sendRequestToAutoscaler(logger lager.Logger, appId string, client *http.Cli } func mustReadXFCCcert(fileName string) string { - file, _ := ioutil.ReadFile(fileName) + file, err := os.ReadFile(fileName) + if err != nil { + log.Fatalf("Error reading file %s", fileName) + } block, _ := pem.Decode(file) return base64.StdEncoding.EncodeToString(block.Bytes) } diff --git a/app/server/cpu.go b/app/server/cpu.go new file mode 100644 index 0000000..6dd677d --- /dev/null +++ b/app/server/cpu.go @@ -0,0 +1,66 @@ +package server + +import ( + "errors" + "fmt" + "sync" + "time" +) + +type CPU interface { + IncreaseCPU(utilization int64, duration time.Duration) error + StopCPU() error + isCPURunning() bool +} + +type CPUWaster struct { + isRunning bool + mutex sync.RWMutex +} + +func (c *CPUWaster) IncreaseCPU(utilization int64, duration time.Duration) error { + c.mutex.Lock() + defer c.mutex.Unlock() + if c.isRunning { + return errors.New("CPU test is already running") + } + c.isRunning = true + + go func() { + defer func() { + c.mutex.Lock() + defer c.mutex.Unlock() + c.isRunning = false + }() + endTime := time.Now().Add(duration) + for time.Now().Before(endTime) { + if !c.isRunning { + break + } + busyLoop(utilization) + } + }() + return nil +} + +func (c *CPUWaster) StopCPU() error { + c.mutex.Lock() + defer c.mutex.Unlock() + if !c.isRunning { + return errors.New("CPU test is not running") + } + c.isRunning = false + + return nil +} + +func busyLoop(utilization int64) { + start := time.Now() + desiredUtilization := float64(utilization) / 100.0 + fmt.Println("putting load on CPU") + for time.Since(start).Seconds() < desiredUtilization { + // Busy loop to simulate CPU load + fmt.Printf(".") + } + time.Sleep(time.Duration(100-utilization) * time.Millisecond) +} diff --git a/app/server/server.go b/app/server/server.go index 0243588..3f25d40 100644 --- a/app/server/server.go +++ b/app/server/server.go @@ -14,14 +14,22 @@ func NewServer(appConfig *cfenv.App) *Server { router := gin.Default() server := &Server{router: router} - ah := NewAppHandler(appConfig, "https://autoscaler-metrics-mtls.cf.sap.hana.ondemand.com") + metrics := map[string]interface{}{ + "cpu": &CPUWaster{}, + } + + ah := NewAppHandler(appConfig, "https://autoscaler-metrics-mtls.cf.sap.hana.ondemand.com", metrics) + router.GET("/", ah.GetHome) - // send POST request to custom metrics URL + // custom metrics handlers router.GET("/busy/:metricValue", ah.Busy) - router.GET("/not-busy/:metricValue", ah.NotBusy) + // cpu handlers + router.GET("/cpu/:utilization/:minutes", ah.IncreaseCPU) + router.GET("/cpu/stop", ah.StopCPU) + return server } diff --git a/deploy/app-manifest.yml b/deploy/app-manifest.yml index 31d5fcc..d7e5a97 100644 --- a/deploy/app-manifest.yml +++ b/deploy/app-manifest.yml @@ -1,11 +1,11 @@ # app-manifest.yml --- applications: - - name: golang-autoscaler-custom-metrics + - name: ((app_name)) instances: 1 - memory: 128M + memory: 1G disk_quota: 128M stack: cflinuxfs4 buildpacks: - binary_buildpack - command: ./golang-autoscaler-custom-metrics \ No newline at end of file + command: ./((app_name)) \ No newline at end of file diff --git a/deploy/cpu-utilization-policy.json b/deploy/cpu-utilization-policy.json new file mode 100644 index 0000000..2ad97e4 --- /dev/null +++ b/deploy/cpu-utilization-policy.json @@ -0,0 +1,22 @@ +{ + "instance_min_count": 1, + "instance_max_count": 2, + "scaling_rules": [ + { + "metric_type": "cpuutil", + "breach_duration_secs": 60, + "threshold": 80, + "operator": ">=", + "cool_down_secs": 60, + "adjustment": "+1" + }, + { + "metric_type": "cpuutil", + "breach_duration_secs": 60, + "threshold": 50, + "operator": "<=", + "cool_down_secs": 60, + "adjustment": "-1" + } + ] +} \ No newline at end of file diff --git a/deploy/custom-metrics-policy-explicit.json b/deploy/custom-metrics-policy-explicit.json new file mode 100644 index 0000000..e68522f --- /dev/null +++ b/deploy/custom-metrics-policy-explicit.json @@ -0,0 +1,22 @@ +{ + "instance_min_count": 1, + "instance_max_count": 2, + "scaling_rules": [ + { + "metric_type": "tooManyRequestCustomMetrics", + "breach_duration_secs": 300, + "threshold": 75, + "operator": ">", + "cool_down_secs": 300, + "adjustment": "+1" + }, + { + "metric_type": "tooManyRequestCustomMetrics", + "breach_duration_secs": 300, + "threshold": 25, + "operator": "<", + "cool_down_secs": 300, + "adjustment": "-1" + } + ] +} \ No newline at end of file diff --git a/docs/cpu-utilization.md b/docs/cpu-utilization.md new file mode 100644 index 0000000..e5f5ce4 --- /dev/null +++ b/docs/cpu-utilization.md @@ -0,0 +1,16 @@ +# Demonstrate CPU Utilization Metric Feature in Application Autoscaler + +Cloud Foundry (CF) is a powerful platform that allows developers to deploy and scale applications with ease. To further enhance the scalability of applications, a new dynamic scaling metric called CPU Utilization (cpuutil) has been added to Application Autoscaler. This metric is linked to the CPU entitlement usage of an app, making it easier for developers to scale their applications based on CPU usage. + +## Introducing CPU Utilization (cpuutil) metric in Application Autoscaler + +The newly introduced CPU utilization metric (cpuutil) in Application Autoscaler allows users to define scaling rules based on CPU usage in percentage. With cpuutil, developers no longer need to manually calculate the CPU entitlement of their app and adjust the thresholds accordingly. Instead, they can simply make use of cpuutil metric and set thresholds from 0% to 100%. + +How CPU Utilization Metric is different from CPU metric + +Previously, scaling applications using CPU metric require calculating the CPU entitlement manually. This requires manual adjustment and careful monitoring of CPU usage thresholds. Also, the calculated thresholds (defined in the scaling policy) do not guarantee the expected results as the CPU entitlement can be increased or decreased (good vs bad apps - discussed later) by the platform. This manual adjustment process was time-consuming and often prone to human error. +The CPU Utilization aka cpuutil metric makes user’s life easier by defining CPU utilization as a percentage ranging from 0 to 100. This also eliminates the need of calculating CPU entitlement explicitly by the app developer. + +In summary, CPU usage shows the actual consumption, while CPU entitlement indicates the allocated limit based on application’s memory. + +Checkout the [blog](https://community.sap.com/t5/technology-blogs-by-sap/application-scalability-with-cpu-utilization-metric-in-cloud-foundry/ba-p/13696223) for more details on how to use CPU Utilization metric in Application Autoscaler. \ No newline at end of file diff --git a/docs/custom-metrics-mtls.md b/docs/custom-metrics-mtls.md new file mode 100644 index 0000000..d54ffa6 --- /dev/null +++ b/docs/custom-metrics-mtls.md @@ -0,0 +1,212 @@ +# Demonstrate Custom Metric Mtls Feature in Application Autoscaler + +This tutorial includes the following steps +- Build and Deploy sample Golang Application +- Create Autoscaler Service Instance +- Bind App with Autoscaler Service +- Start Golang App +- Send Custom Metrics to Autoscaler Service +- Monitor Scale-Out and Scale-In + +## Requirements + +- You should have access to Cloud Foundry Runtime + +## Build and Deploy sample Golang Application + +### Build App +#### Option 1 `env GOOS=linux GOARCH=amd64 go build -o build/golang-autoscaler-custom-metrics` +#### Option 2 `make build` + +### Run Test + +`make test` + +### Deploy App + +`cf push --no-start -f deploy/app-manifest.yml -p deploy/build` + +sample-output +```shell +$ cf push --no-start -f deploy/app-manifest.yml -p deploy/build +Pushing app golang-autoscaler-custom-metrics to org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... +Applying manifest file deploy/app-manifest.yml... + +Updating with these attributes... + --- + applications: + - name: golang-autoscaler-custom-metrics + disk-quota: 128M + instances: 1 + path: /Users/development/golang-autoscaler-custom-metrics/deploy/build + memory: 128M ++ default-route: true + stack: cflinuxfs4 + buildpacks: + binary_buildpack + command: ./golang-autoscaler-custom-metrics +Manifest applied +All files found in remote cache; nothing to upload. +Waiting for API to complete processing files... + +name: golang-autoscaler-custom-metrics +requested state: stopped +routes: golang-autoscaler-custom-metrics.cfapps.sap.hana.ondemand.com +last uploaded: +stack: +buildpacks: + +type: web +sidecars: +instances: 0/1 +memory usage: 128M +start command: ./golang-autoscaler-custom-metrics + state since cpu memory disk details +#0 down 2022-12-30T14:22:58Z 0.0% 0 of 0 0 of 0 + +``` + +## Create Autoscaler Service Instance + +```shell +$ cf create-service autoscaler standard ak-test-autoscaler +Creating service instance ak-test-autoscaler in org ak_autoscalerxxx / space ak-test-space as as ak-user@xxx.com... + +Service instance ak-test-autoscaler created. +OK +``` +## Bind App with Autoscaler Service +`$ cf bind-service golang-autoscaler-custom-metrics ak-test-autoscaler -c deploy/custom-metrics-policy.json` + +```shell +$ cf bind-service golang-autoscaler-custom-metrics ak-test-autoscaler -c deploy/custom-metrics-policy.json +Binding service instance ak-test-autoscaler to app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... +OK + +TIP: Use 'cf restage golang-autoscaler-custom-metrics' to ensure your env variable changes take effect + +``` +### Check VCAP_SERVICE Environment Variable + +```shell +$ cf env golang-autoscaler-custom-metrics +Getting env variables for app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... +System-Provided: +VCAP_SERVICES: { + "autoscaler": [ + { + "binding_guid": "c02fba90-83b2-4753-a31f-1d0827978632", + "binding_name": null, + "credentials": { + "custom_metrics": { + "mtls_url": "https://autoscaler-metrics-mtls.cf.xxx.com", + "password": "xxxxxxxxx", + "url": "https://autoscaler-metrics.cf.xxx.com", + "username": "xxxxxxxxx" + } + }, + "instance_guid": "3eeac1a2-ced1-4748-a3b4-a87188a7d3c6", + "instance_name": "ak-test-autoscaler", + "label": "autoscaler", + "name": "ak-test-autoscaler", + "plan": "standard", + "provider": null, + "syslog_drain_url": null, + "tags": [ + "autoscaler", + "app-autoscaler", + "cf-autoscaler" + ], + "volume_mounts": [] + } + ] +} + + +VCAP_APPLICATION: { + ... + .... + ... + +``` +## Start Golang App +`cf start golang-autoscaler-custom-metrics` + + +```shell +cf start golang-autoscaler-custom-metrics +Starting app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... + +Staging app and tracing logs... + Downloading binary_buildpack... + Downloaded binary_buildpack + Cell 038ca894-a748-49d5-85eb-be08569dfe48 creating container for instance 95b8140e-065c-4116-8c42-085f3f33db27 + Cell 038ca894-a748-49d5-85eb-be08569dfe48 successfully created container for instance 95b8140e-065c-4116-8c42-085f3f33db27 + Downloading app package... + Downloading build artifacts cache... + Downloaded build artifacts cache (215B) + Downloaded app package (6M) + -----> Binary Buildpack version 1.0.47 + Exit status 0 + Uploading droplet, build artifacts cache... + Uploading droplet... + Uploading build artifacts cache... + Uploaded build artifacts cache (216B) + +Starting app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... + +Waiting for app to start... + +Instances starting... +Instances starting... +Instances starting... + +name: golang-autoscaler-custom-metrics +requested state: started +routes: golang-autoscaler-custom-metrics.cfapps.sap.hana.ondemand.com +last uploaded: Fri 30 Dec 16:04:06 CET 2022 +stack: cflinuxfs4 +buildpacks: + name version detect output buildpack name + binary_buildpack 1.0.47 binary binary + +type: web +sidecars: +instances: 1/1 +memory usage: 128M + state since cpu memory disk details +#0 running 2022-12-30T15:04:16Z 0.0% 0 of 128M 0 of 128M + +``` + +## Send Custom Metrics to Autoscaler Service + +Scale out > `$ curl https://golang-autoscaler-custom-metrics.cfapps.abc.com/busy/301` + +```shell +Every 2.0s: cf app golang-autoscaler-custom-metrics XNQNV6VGJC: Fri Dec 30 16:19:22 2022 + +Showing health and status for app golang-autoscaler-custom-metrics in org ak_autoscalerxxx / space ak-test-space as ak-user@xxx.com... + +name: golang-autoscaler-custom-metrics +requested state: started +routes: golang-autoscaler-custom-metrics.cfapps.sap.hana.ondemand.com +last uploaded: Fri 30 Dec 16:16:35 CET 2022 +stack: cflinuxfs4 +buildpacks: + name version detect output buildpack name + binary_buildpack 1.0.47 binary binary + +type: web +sidecars: +instances: 2/2 +memory usage: 128M + state since cpu memory disk details +#0 running 2022-12-30T15:16:44Z 0.9% 23.6M of 128M 10.9M of 128M +#1 running 2022-12-30T15:18:13Z 0.9% 22.9M of 128M 10.9M of 128M + +``` + +Scale In > `$ curl https://golang-autoscaler-custom-metrics.cfapps.abc.com/not-busy/190` + +Checkout the [blog](https://community.sap.com/t5/technology-blogs-by-sap/scale-your-applications-using-custom-metrics-feature-of-application/ba-p/13524412) for more details on how to use Custom Metrics in Application Autoscaler. \ No newline at end of file diff --git a/go.mod b/go.mod index 3a7e364..f23dd19 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,16 @@ module github.com/asalan316/golang-autoscaler-custom-metrics -go 1.19 +go 1.22.0 + +toolchain go1.22.5 require ( code.cloudfoundry.org/lager v2.0.0+incompatible github.com/cloudfoundry-community/go-cfenv v1.18.0 github.com/gin-gonic/gin v1.10.0 github.com/joefitzgerald/rainbow-reporter v0.1.0 - github.com/onsi/ginkgo/v2 v2.20.2 - github.com/onsi/gomega v1.34.1 + github.com/onsi/ginkgo/v2 v2.22.0 + github.com/onsi/gomega v1.36.0 github.com/sclevine/spec v1.4.0 ) @@ -26,7 +28,7 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -38,12 +40,11 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index db08670..9793d60 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -37,8 +38,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/joefitzgerald/rainbow-reporter v0.1.0 h1:AuMG652zjdzI0YCCnXAqATtRBpGXMcAnrajcaTrSeuo= @@ -63,11 +64,11 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y= +github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -94,26 +95,24 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/main.go b/main.go index e5b89ef..5c8d34b 100644 --- a/main.go +++ b/main.go @@ -10,17 +10,16 @@ import ( const serverAddress = "0.0.0.0:8080" func main() { + var appConfig *cfenv.App appConfig, err := app_config.GetAppEnv(cfenv.Current) if err != nil { - log.Fatal("app configuration not found %w", err) + log.Fatal("app configuration not found: ", err) } - server := server.NewServer(appConfig) - err = server.Start(serverAddress) + httpServer := server.NewServer(appConfig) + err = httpServer.Start(serverAddress) if err != nil { log.Fatal("cannot start server:", err) } -} -//TODO -// use this for better structuring the code -//https://dev.to/techschoolguru/implement-restful-http-api-in-go-using-gin-4ap1 + log.Printf("server started on %s\n", serverAddress) +}