From f4db2fb570dce20e51636ce10003250e90d25286 Mon Sep 17 00:00:00 2001 From: Mickael Gaillard Date: Tue, 19 Aug 2025 16:00:41 +0200 Subject: [PATCH 1/3] Add Vertical and Custom(force) scale --- README.md | 17 +- cmd/scaling-instance.go | 35 ++- go.mod | 2 - internal/logic/scaling_resource.go | 337 ++++++++++++++++++++++------- 4 files changed, 307 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index cda697a..7790cda 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ env:UPSUN_CLI_TOKEN ``` Usage of scalsun: --name string Apps or Service name + --include_service Autoscale the services + --type string Type of scaling (horizontal or vertical or timming) (default "horizontal") + --min_size_count: float Minimum host size (default 0.1) + --max_size_count float Maximum host size (default 8) --min_host_count: int Minimum host count (default 1) --max_host_count int Maximum host count (default 3) --min_cpu_usage_upscale float Minimum CPU usage in % (for upscale event only) (default 75) @@ -56,7 +60,14 @@ Usage of scalsun: ``` #### Samples -- Auto-scale all app/service +- Auto-scale (Horizontal) all app/service `scalsun --silent --max_host_count=${H_SCALING_HOST_MAX:-3}` -- Auto-scale only specific app (if app name is web) -`scalsun --silent --max_host_count=${H_SCALING_HOST_MAX:-3} --name=web` + +- Auto-scale (Horizontal) only specific app (if app name is web) +`scalsun --silent --type=horizontal --max_host_count=${H_SCALING_HOST_MAX:-3} --name=web` + +- Auto-scale (Vertical) only specific app (if app name is web) +`scalsun --silent --type=vertical --max_cpu_usage_downscale=20 --max_host_size=1 --min_host_size=0.1 --name=web` + +- Auto-scale (Custom) only specific app (if app name is web) +`scalsun --silent --type=custom --max_host_size=1 --max_host_count=0.1 --name=web` diff --git a/cmd/scaling-instance.go b/cmd/scaling-instance.go index eafb074..c41d414 100644 --- a/cmd/scaling-instance.go +++ b/cmd/scaling-instance.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "log" "os" + "strings" flag "github.com/spf13/pflag" lib "github.com/upsun/lib-sun" @@ -13,14 +15,29 @@ import ( ) const ( - APP_NAME = "scalsun" + APP_NAME = "scalsun" + TYPE_HORIZONTAL = "horizontal" + TYPE_VERTICAL = "vertical" + TYPE_CUSTOM = "custom" ) func init() { + // Common arguments flag.StringVarP(&app.ArgsS.Name, "name", "", "", "Apps or Service name") flag.BoolVarP(&app.ArgsS.IncludeServices, "include_service", "", false, "Autoscale the services") + + flag.StringVarP(&app.ArgsS.Type, "type", "", TYPE_HORIZONTAL, fmt.Sprintf("Type of scaling (%s or %s or %s)", TYPE_HORIZONTAL, TYPE_VERTICAL, TYPE_CUSTOM)) + + // Vertical scaling arguments + flag.Float64VarP(&app.ArgsS.HostSizeMin, "min_size_count:", "", 0.1, "Minimum host size") + flag.Float64VarP(&app.ArgsS.HostSizeMax, "max_size_count", "", 8, "Maximum host size") + + // Horizontal scaling arguments flag.IntVarP(&app.ArgsS.HostCountMin, "min_host_count:", "", 1, "Minimum host count") flag.IntVarP(&app.ArgsS.HostCountMax, "max_host_count", "", 3, "Maximum host count") + + // Trigger + //flag.StringVarP(&app.ArgsS.RangeTime, "", "", "", "") flag.Float64VarP(&app.ArgsS.CpuUsageMin, "min_cpu_usage_upscale", "", 75.0, "Minimum CPU usage in % (for upscale event only)") flag.Float64VarP(&app.ArgsS.CpuUsageMax, "max_cpu_usage_downscale", "", 60.0, "Maximum CPU usage in % (for downscale event only)") flag.Float64VarP(&app.ArgsS.MemUsageMin, "min_mem_usage_upscale", "", 80.0, "Minimum memory usage in % (for upscale event only)") @@ -54,11 +71,25 @@ func main() { lib.Args = app.Args } + if !entity.IsValidType(app.ArgsS.Type) { + fmt.Fprintf(os.Stderr, "Invalid value for --type: %q\n", app.ArgsS.Type) + fmt.Fprintf(os.Stderr, "Valid values are: %s\n", strings.Join(entity.ValidTypes, ", ")) + os.Exit(1) + } + // Init projectContext := entity.MakeProjectContext( "upsun", projectID, branch, ) - logic.ScalingInstance(projectContext) + + switch app.ArgsS.Type { + case TYPE_CUSTOM: + logic.ForceContainerInstance(projectContext) + case TYPE_VERTICAL: + logic.ScalingContainer(projectContext) + case TYPE_HORIZONTAL: + logic.ScalingInstance(projectContext) + } } diff --git a/go.mod b/go.mod index 9e05313..42b3df0 100644 --- a/go.mod +++ b/go.mod @@ -13,5 +13,3 @@ require ( golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -//replace github.com/upsun/lib-sun => ../lib-sun diff --git a/internal/logic/scaling_resource.go b/internal/logic/scaling_resource.go index 0a0623b..b73c0d8 100644 --- a/internal/logic/scaling_resource.go +++ b/internal/logic/scaling_resource.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "math" + "slices" "strconv" "strings" @@ -18,18 +19,227 @@ type UsageValue struct { } type UsageApp struct { - Name string - Values []UsageValue - Update bool - InstanceOld int - InstanceNew int + Name string + Values []UsageValue + + // Horizontal scaling + InstanceUpdate bool + InstanceOld int + InstanceNew int + + // Vertical scaling + IdProfileUpdate bool + IdProfileOld float64 + IdProfileNew float64 +} + +type SizeProfile struct { + Id string + Cpu float64 + Mem int32 +} + +func ForceContainerInstance(projectContext entity.ProjectGlobal) { + usages := map[string]UsageApp{} + + log.Println("Process Force Scaling... (only use max_host_count & max_size_count argument)") + + sizesAvailable := ListContainerSize(projectContext) + if slices.Contains(sizesAvailable, app.ArgsS.HostSizeMax) { + + } + + GetContainersUsage(projectContext, usages) + + for _, usage := range usages { + if usage.IdProfileOld < app.ArgsS.HostSizeMax { + usage.IdProfileNew = app.ArgsS.HostSizeMax + usage.IdProfileUpdate = true + } + + if usage.InstanceOld < app.ArgsS.HostCountMax { + usage.InstanceNew = app.ArgsS.HostCountMax + usage.InstanceUpdate = true + } + + usages[usage.Name] = usage + } + + SetResources(projectContext, usages) +} + +func ScalingContainer(projectContext entity.ProjectGlobal) { + usages := map[string]UsageApp{} + + log.Println("Process Vertical Auto-Scaling...") + + GetProjectMetrics(projectContext, usages) + GetContainersUsage(projectContext, usages) + RemoveUnscaleContainers(projectContext, usages) + + // Compute + log.Println("Compute Horizontal trend...") + for _, usage := range usages { + + //// Algo Threshold-limit + lastMetric := usage.Values[len(usage.Values)-1] + + // CPU case + if lastMetric.Cpu > app.ArgsS.CpuUsageMin && usage.IdProfileOld < app.ArgsS.HostSizeMax { + idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMin))) + if usage.IdProfileNew < idProfilProposal { + usage.IdProfileNew = idProfilProposal + if int(usage.IdProfileNew) != int(usage.IdProfileOld) { + log.Printf("Upscale instance %v from %v to %v ! (Cpu: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Cpu, app.ArgsS.CpuUsageMin) + usage.IdProfileUpdate = true + } + } + usages[usage.Name] = usage + } else if lastMetric.Cpu < app.ArgsS.CpuUsageMax && usage.IdProfileOld > app.ArgsS.HostSizeMin { + idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMax))) + if usage.IdProfileNew < idProfilProposal { + usage.IdProfileNew = idProfilProposal + if usage.IdProfileNew != usage.IdProfileOld { + log.Printf("Downscale instance %v from %v to %v ! (Cpu: %v < %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Cpu, app.ArgsS.CpuUsageMax) + usage.IdProfileUpdate = true + } + } + usages[usage.Name] = usage + } + + // Mem case + if lastMetric.Mem > app.ArgsS.MemUsageMin && usage.IdProfileOld < app.ArgsS.HostSizeMax { + idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Mem / app.ArgsS.MemUsageMin))) + if usage.IdProfileNew < idProfilProposal { + usage.IdProfileNew = idProfilProposal + if usage.IdProfileNew != usage.IdProfileOld { + log.Printf("Upscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Mem, app.ArgsS.MemUsageMin) + usage.InstanceUpdate = true + } + } + usages[usage.Name] = usage + } else if lastMetric.Mem < app.ArgsS.MemUsageMax && usage.IdProfileOld > app.ArgsS.HostSizeMin { + idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Mem / app.ArgsS.MemUsageMax))) + if usage.IdProfileNew < idProfilProposal { + usage.IdProfileNew = idProfilProposal + if usage.IdProfileNew != usage.IdProfileOld { + log.Printf("Downscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Mem, app.ArgsS.MemUsageMax) + usage.InstanceUpdate = true + } + } + usages[usage.Name] = usage + } + } + + SetResources(projectContext, usages) } func ScalingInstance(projectContext entity.ProjectGlobal) { usages := map[string]UsageApp{} - // Get metrics + log.Println("Process Horizontal Auto-Scaling...") + + GetProjectMetrics(projectContext, usages) + GetContainersUsage(projectContext, usages) + RemoveUnscaleContainers(projectContext, usages) + + // Compute + log.Println("Compute Horizontal trend...") + for _, usage := range usages { + + //// Algo Threshold-limit + lastMetric := usage.Values[len(usage.Values)-1] + + // CPU case + if lastMetric.Cpu > app.ArgsS.CpuUsageMin && usage.InstanceOld < app.ArgsS.HostCountMax { + instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMin))) + if usage.InstanceNew < instanceProposal { + usage.InstanceNew = instanceProposal + if usage.InstanceNew != usage.InstanceOld { + log.Printf("Upscale instance %v from %v to %v ! (Cpu: %v > %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Cpu, app.ArgsS.CpuUsageMin) + usage.InstanceUpdate = true + } + } + usages[usage.Name] = usage + } else if lastMetric.Cpu < app.ArgsS.CpuUsageMax && usage.InstanceOld > app.ArgsS.HostCountMin { + instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMax))) + if usage.InstanceNew < instanceProposal { + usage.InstanceNew = instanceProposal + if usage.InstanceNew != usage.InstanceOld { + log.Printf("Downscale instance %v from %v to %v ! (Cpu: %v < %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Cpu, app.ArgsS.CpuUsageMax) + usage.InstanceUpdate = true + } + } + usages[usage.Name] = usage + } + + // Mem case + if lastMetric.Mem > app.ArgsS.MemUsageMin && usage.InstanceOld < app.ArgsS.HostCountMax { + instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Mem / app.ArgsS.MemUsageMin))) + if usage.InstanceNew < instanceProposal { + usage.InstanceNew = instanceProposal + if usage.InstanceNew != usage.InstanceOld { + log.Printf("Upscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Mem, app.ArgsS.MemUsageMin) + usage.InstanceUpdate = true + } + } + usages[usage.Name] = usage + } else if lastMetric.Mem < app.ArgsS.MemUsageMax && usage.InstanceOld > app.ArgsS.HostCountMin { + instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Mem / app.ArgsS.MemUsageMax))) + if usage.InstanceNew < instanceProposal { + usage.InstanceNew = instanceProposal + if usage.InstanceNew != usage.InstanceOld { + log.Printf("Downscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Mem, app.ArgsS.MemUsageMax) + usage.InstanceUpdate = true + } + } + usages[usage.Name] = usage + } + } + + SetResources(projectContext, usages) +} + +// ListContainerSize retrieves the size profiles of containers in the project. +func ListContainerSize(projectContext entity.ProjectGlobal) []float64 { // SizeProfile { + log.Println("Get Size of Profile available...") + payload := []string{ + "--environment=" + projectContext.DefaultEnv, + "--service=" + app.ArgsS.Name, + "--no-header", + "--format=csv", + "--no-interaction", + "--yes", + } + output, err := utils.CallCLIString(projectContext, "resources:size:list", payload...) + if err != nil { + log.Fatalf("command execution failed: %s", err) + } + + // Parse output + size_tpl := strings.Split(output, "\n") + var sizeProfiles []float64 // SizeProfile + for _, size_str := range size_tpl[:len(size_tpl)-1] { + size := strings.Split(size_str, ",") + + // Normalize + // cpu, _ := strconv.ParseFloat(size[1], 64) + // mem, _ := strconv.ParseInt(size[2], 10, 32) + // profile := SizeProfile{ + // Id: size[0], + // Cpu: cpu, + // Mem: int32(mem), + // } + sizeId, _ := strconv.ParseFloat(size[0], 64) + sizeProfiles = append(sizeProfiles, sizeId) // profile) + } + + return sizeProfiles +} + +func GetProjectMetrics(projectContext entity.ProjectGlobal, usages map[string]UsageApp) { log.Println("Get metrics of project...") + payload := []string{ "--environment=" + projectContext.DefaultEnv, "--interval=1m", @@ -65,29 +275,32 @@ func ScalingInstance(projectContext entity.ProjectGlobal) { usage.Values = append(usage.Values, value) usages[name] = usage } +} + +func GetContainersUsage(projectContext entity.ProjectGlobal, usages map[string]UsageApp) { + log.Println("Get size of containers and number of instance of project...") - // Get instance - log.Println("Get number of instance of project...") - payload = []string{ + payload := []string{ "--environment=" + projectContext.DefaultEnv, "--format=csv", - "--columns=service,instance_count", + "--columns=service,instance_count,cpu", "--no-header", "--no-interaction", "--yes", } - output, err = utils.CallCLIString(projectContext, "resources:get", payload...) + output, err := utils.CallCLIString(projectContext, "resources:get", payload...) if err != nil { log.Fatalf("command execution failed: %s", err) } // Parse output - apps = strings.Split(output, "\n") + apps := strings.Split(output, "\n") for _, app_str := range apps[:len(apps)-1] { app_raw := strings.Split(app_str, ",") // Normalize name := app_raw[0] + idProfil, _ := strconv.ParseFloat(app_raw[2], 64) instance, err := strconv.Atoi(app_raw[1]) if err != nil { break @@ -102,14 +315,18 @@ func ScalingInstance(projectContext entity.ProjectGlobal) { // Assign usage.InstanceOld = instance + usage.IdProfileOld = idProfil usages[name] = usage } } +} + +func RemoveUnscaleContainers(projectContext entity.ProjectGlobal, usages map[string]UsageApp) { if !app.ArgsS.IncludeServices { // Get Services log.Println("Dectect available services of project... (to exclude)") - payload = []string{ + payload := []string{ "--environment=" + projectContext.DefaultEnv, "--format=csv", "--columns=name", @@ -117,7 +334,7 @@ func ScalingInstance(projectContext entity.ProjectGlobal) { "--no-interaction", "--yes", } - output, err = utils.CallCLIString(projectContext, "service:list", payload...) + output, err := utils.CallCLIString(projectContext, "service:list", payload...) if err != nil { log.Fatalf("command execution failed: %s", err) } @@ -128,86 +345,52 @@ func ScalingInstance(projectContext entity.ProjectGlobal) { delete(usages, srv) } } +} - // Compute - log.Println("Compute trend...") +func SetResources(projectContext entity.ProjectGlobal, usages map[string]UsageApp) { + log.Println("Set new number of instance...") + var updateInstance string + var updateContainer string for _, usage := range usages { - - //// Algo Threshold-limit - lastMetric := usage.Values[len(usage.Values)-1] - - // CPU case - if lastMetric.Cpu > app.ArgsS.CpuUsageMin && usage.InstanceOld < app.ArgsS.HostCountMax { - instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMin))) - if usage.InstanceNew < instanceProposal { - usage.InstanceNew = instanceProposal - if usage.InstanceNew != usage.InstanceOld { - log.Printf("Upscale instance %v from %v to %v ! (Cpu: %v > %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Cpu, app.ArgsS.CpuUsageMin) - usage.Update = true - } + if usage.InstanceUpdate { + if updateInstance != "" { + updateInstance += "," } - usages[usage.Name] = usage - } else if lastMetric.Cpu < app.ArgsS.CpuUsageMax && usage.InstanceOld > app.ArgsS.HostCountMin { - instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMax))) - if usage.InstanceNew < instanceProposal { - usage.InstanceNew = instanceProposal - if usage.InstanceNew != usage.InstanceOld { - log.Printf("Downscale instance %v from %v to %v ! (Cpu: %v < %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Cpu, app.ArgsS.CpuUsageMax) - usage.Update = true - } - } - usages[usage.Name] = usage - } - - // Mem case - if lastMetric.Mem > app.ArgsS.MemUsageMin && usage.InstanceOld < app.ArgsS.HostCountMax { - instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Mem / app.ArgsS.MemUsageMin))) - if usage.InstanceNew < instanceProposal { - usage.InstanceNew = instanceProposal - if usage.InstanceNew != usage.InstanceOld { - log.Printf("Upscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Mem, app.ArgsS.MemUsageMin) - usage.Update = true - } - } - usages[usage.Name] = usage - } else if lastMetric.Mem < app.ArgsS.MemUsageMax && usage.InstanceOld > app.ArgsS.HostCountMin { - instanceProposal := int(math.Ceil(float64(usage.InstanceOld) * (lastMetric.Mem / app.ArgsS.MemUsageMax))) - if usage.InstanceNew < instanceProposal { - usage.InstanceNew = instanceProposal - if usage.InstanceNew != usage.InstanceOld { - log.Printf("Downscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.InstanceOld, usage.InstanceNew, lastMetric.Mem, app.ArgsS.MemUsageMax) - usage.Update = true - } - } - usages[usage.Name] = usage + updateInstance += usage.Name + updateInstance += ":" + updateInstance += strconv.Itoa(usage.InstanceNew) } } - // Reassign resource - log.Println("Set new number of instance...") - var update string + log.Println("Set new size of containers...") for _, usage := range usages { - if usage.Update { - if update != "" { - update += "," + if usage.IdProfileUpdate { + if updateContainer != "" { + updateContainer += "," } - update += usage.Name - update += ":" - update += strconv.Itoa(usage.InstanceNew) + updateContainer += usage.Name + updateContainer += ":" + updateContainer += fmt.Sprintf("%.1f", usage.IdProfileNew) } } - if update != "" { - payload = []string{ + if updateInstance != "" || updateContainer != "" { + log.Println("Apply new resources...") + payload := []string{ "--environment=" + projectContext.DefaultEnv, "--no-interaction", "--yes", "--no-wait", - "--count", - update, } - output, err = utils.CallCLIString(projectContext, "resources:set", payload...) + if updateInstance != "" { + payload = append(payload, "--count", updateInstance) + } + if updateContainer != "" { + payload = append(payload, "--size", updateContainer) + } + + output, err := utils.CallCLIString(projectContext, "resources:set", payload...) if err != nil { log.Fatalf("command execution failed: %s", err) } From b7edde62b09e29a9324ed4b28fa2ea53e8e76ff1 Mon Sep 17 00:00:00 2001 From: NDuncan Date: Tue, 23 Sep 2025 15:00:28 +0200 Subject: [PATCH 2/3] updating Discord link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4133514..be5d527 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This tool provide simple auto horizontal scaling on upsun project for your appli > [!CAUTION] > **This project is owned by the Upsun Advocacy team. It is in early stage of development [experimental] and only intended to be used with caution by Upsun customers/community.

This project is not supported by Upsun and does not qualify for Support plans. Use this repository at your own risks, it is provided without guarantee or warranty!** > -> PS: if you have downloaded that binary and appreciated it, I kindly ask you to let us know on [Discord](https://discord.com/invite/platformsh) (you can then later discuss with one of our Product manager to share your feedback on it). +> PS: if you have downloaded that binary and appreciated it, I kindly ask you to let us know on [Discord](https://discord.gg/upsun) (you can then later discuss with one of our Product manager to share your feedback on it). ## Usage/install From 265ba62e3ad7c0f56a7fcb12a8be529c0f740d27 Mon Sep 17 00:00:00 2001 From: Mickael Gaillard Date: Thu, 2 Oct 2025 16:47:02 +0200 Subject: [PATCH 3/3] Final version - Autoscalling Vertical - Custom trigger --- cmd/scaling-instance.go | 6 +- internal/logic/scaling_resource.go | 155 ++++++++++++++++++++--------- 2 files changed, 109 insertions(+), 52 deletions(-) diff --git a/cmd/scaling-instance.go b/cmd/scaling-instance.go index c41d414..53d32c3 100644 --- a/cmd/scaling-instance.go +++ b/cmd/scaling-instance.go @@ -24,16 +24,16 @@ const ( func init() { // Common arguments flag.StringVarP(&app.ArgsS.Name, "name", "", "", "Apps or Service name") - flag.BoolVarP(&app.ArgsS.IncludeServices, "include_service", "", false, "Autoscale the services") + flag.BoolVarP(&app.ArgsS.IncludeServices, "include_service", "", false, "Autoscale the services (not applicable)") flag.StringVarP(&app.ArgsS.Type, "type", "", TYPE_HORIZONTAL, fmt.Sprintf("Type of scaling (%s or %s or %s)", TYPE_HORIZONTAL, TYPE_VERTICAL, TYPE_CUSTOM)) // Vertical scaling arguments - flag.Float64VarP(&app.ArgsS.HostSizeMin, "min_size_count:", "", 0.1, "Minimum host size") + flag.Float64VarP(&app.ArgsS.HostSizeMin, "min_size_count", "", 0.1, "Minimum host size") flag.Float64VarP(&app.ArgsS.HostSizeMax, "max_size_count", "", 8, "Maximum host size") // Horizontal scaling arguments - flag.IntVarP(&app.ArgsS.HostCountMin, "min_host_count:", "", 1, "Minimum host count") + flag.IntVarP(&app.ArgsS.HostCountMin, "min_host_count", "", 1, "Minimum host count") flag.IntVarP(&app.ArgsS.HostCountMax, "max_host_count", "", 3, "Maximum host count") // Trigger diff --git a/internal/logic/scaling_resource.go b/internal/logic/scaling_resource.go index b73c0d8..f627dff 100644 --- a/internal/logic/scaling_resource.go +++ b/internal/logic/scaling_resource.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "math" - "slices" "strconv" "strings" @@ -30,7 +29,7 @@ type UsageApp struct { // Vertical scaling IdProfileUpdate bool IdProfileOld float64 - IdProfileNew float64 + IdProfileNew SizeProfile } type SizeProfile struct { @@ -44,21 +43,22 @@ func ForceContainerInstance(projectContext entity.ProjectGlobal) { log.Println("Process Force Scaling... (only use max_host_count & max_size_count argument)") - sizesAvailable := ListContainerSize(projectContext) - if slices.Contains(sizesAvailable, app.ArgsS.HostSizeMax) { - - } - GetContainersUsage(projectContext, usages) + RemoveUnscaleContainers(projectContext, usages) + hostSizeMax := app.ArgsS.HostSizeMax + hostCountMax := app.ArgsS.HostCountMax for _, usage := range usages { - if usage.IdProfileOld < app.ArgsS.HostSizeMax { - usage.IdProfileNew = app.ArgsS.HostSizeMax + profils := ListContainerSize(projectContext, usage.Name) + idProfilProposal := findBestUpProfile(profils, hostSizeMax, hostSizeMax) + + if usage.IdProfileOld != hostSizeMax { + usage.IdProfileNew = idProfilProposal usage.IdProfileUpdate = true } - if usage.InstanceOld < app.ArgsS.HostCountMax { - usage.InstanceNew = app.ArgsS.HostCountMax + if usage.InstanceOld != hostCountMax { + usage.InstanceNew = hostCountMax usage.InstanceUpdate = true } @@ -71,7 +71,7 @@ func ForceContainerInstance(projectContext entity.ProjectGlobal) { func ScalingContainer(projectContext entity.ProjectGlobal) { usages := map[string]UsageApp{} - log.Println("Process Vertical Auto-Scaling...") + log.Println("Process Vertical Auto-Scaling... (with down-time)") GetProjectMetrics(projectContext, usages) GetContainersUsage(projectContext, usages) @@ -83,24 +83,34 @@ func ScalingContainer(projectContext entity.ProjectGlobal) { //// Algo Threshold-limit lastMetric := usage.Values[len(usage.Values)-1] + cpuUsageMin := app.ArgsS.CpuUsageMin + cpuUsageMax := app.ArgsS.CpuUsageMax + memUsageMin := app.ArgsS.MemUsageMin + memUsageMax := app.ArgsS.MemUsageMax + hostSizeMax := app.ArgsS.HostSizeMax + hostSizeMin := app.ArgsS.HostSizeMin + + profils := ListContainerSize(projectContext, usage.Name) // CPU case - if lastMetric.Cpu > app.ArgsS.CpuUsageMin && usage.IdProfileOld < app.ArgsS.HostSizeMax { - idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMin))) - if usage.IdProfileNew < idProfilProposal { + if lastMetric.Cpu > cpuUsageMin && usage.IdProfileOld <= hostSizeMax { + idProfilCpuTheorical := usage.IdProfileOld * (lastMetric.Cpu / cpuUsageMin) + idProfilProposal := findBestUpProfile(profils, idProfilCpuTheorical, hostSizeMax) + if usage.IdProfileNew.Cpu < idProfilProposal.Cpu { usage.IdProfileNew = idProfilProposal - if int(usage.IdProfileNew) != int(usage.IdProfileOld) { - log.Printf("Upscale instance %v from %v to %v ! (Cpu: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Cpu, app.ArgsS.CpuUsageMin) + if usage.IdProfileNew.Cpu != usage.IdProfileOld { + log.Printf("Upscale profile %v from %v to %v ! (Cpu: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew.Id, lastMetric.Cpu, cpuUsageMin) usage.IdProfileUpdate = true } } usages[usage.Name] = usage - } else if lastMetric.Cpu < app.ArgsS.CpuUsageMax && usage.IdProfileOld > app.ArgsS.HostSizeMin { - idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Cpu / app.ArgsS.CpuUsageMax))) - if usage.IdProfileNew < idProfilProposal { + } else if lastMetric.Cpu < cpuUsageMax && usage.IdProfileOld >= hostSizeMin { + idProfilCpuTheorical := usage.IdProfileOld * (lastMetric.Cpu / cpuUsageMax) + idProfilProposal := findBestDownProfile(profils, idProfilCpuTheorical, hostSizeMin) + if usage.IdProfileNew.Cpu < idProfilProposal.Cpu { usage.IdProfileNew = idProfilProposal - if usage.IdProfileNew != usage.IdProfileOld { - log.Printf("Downscale instance %v from %v to %v ! (Cpu: %v < %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Cpu, app.ArgsS.CpuUsageMax) + if usage.IdProfileNew.Cpu != usage.IdProfileOld { + log.Printf("Downscale profile %v from %v to %v ! (Cpu: %v < %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew.Id, lastMetric.Cpu, cpuUsageMax) usage.IdProfileUpdate = true } } @@ -108,23 +118,25 @@ func ScalingContainer(projectContext entity.ProjectGlobal) { } // Mem case - if lastMetric.Mem > app.ArgsS.MemUsageMin && usage.IdProfileOld < app.ArgsS.HostSizeMax { - idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Mem / app.ArgsS.MemUsageMin))) - if usage.IdProfileNew < idProfilProposal { + if lastMetric.Mem > memUsageMin && usage.IdProfileOld <= hostSizeMax { + idProfilCpuTheorical := usage.IdProfileOld * (lastMetric.Mem / memUsageMin) + idProfilProposal := findBestUpProfile(profils, idProfilCpuTheorical, hostSizeMax) + if usage.IdProfileNew.Cpu < idProfilProposal.Cpu { usage.IdProfileNew = idProfilProposal - if usage.IdProfileNew != usage.IdProfileOld { - log.Printf("Upscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Mem, app.ArgsS.MemUsageMin) - usage.InstanceUpdate = true + if usage.IdProfileNew.Cpu != usage.IdProfileOld { + log.Printf("Upscale profile %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew.Id, lastMetric.Mem, memUsageMin) + usage.IdProfileUpdate = true } } usages[usage.Name] = usage - } else if lastMetric.Mem < app.ArgsS.MemUsageMax && usage.IdProfileOld > app.ArgsS.HostSizeMin { - idProfilProposal := float64(math.Ceil(float64(usage.IdProfileOld) * (lastMetric.Mem / app.ArgsS.MemUsageMax))) - if usage.IdProfileNew < idProfilProposal { + } else if lastMetric.Mem < memUsageMax && usage.IdProfileOld >= hostSizeMin { + idProfilCpuTheorical := usage.IdProfileOld * (lastMetric.Mem / memUsageMax) + idProfilProposal := findBestDownProfile(profils, idProfilCpuTheorical, hostSizeMin) + if usage.IdProfileNew.Cpu < idProfilProposal.Cpu { usage.IdProfileNew = idProfilProposal - if usage.IdProfileNew != usage.IdProfileOld { - log.Printf("Downscale instance %v from %v to %v ! (Mem: %v > %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew, lastMetric.Mem, app.ArgsS.MemUsageMax) - usage.InstanceUpdate = true + if usage.IdProfileNew.Cpu != usage.IdProfileOld { + log.Printf("Downscale profile %v from %v to %v ! (Mem: %v < %v)", usage.Name, usage.IdProfileOld, usage.IdProfileNew.Id, lastMetric.Mem, memUsageMax) + usage.IdProfileUpdate = true } } usages[usage.Name] = usage @@ -134,10 +146,48 @@ func ScalingContainer(projectContext entity.ProjectGlobal) { SetResources(projectContext, usages) } +func findBestUpProfile(profils []SizeProfile, idProfilTheorical float64, hostSizeMax float64) SizeProfile { + profilProposal := profils[0] + + for idx, profile := range profils { + + if profile.Cpu > hostSizeMax { + profilProposal = profils[idx-1] + break + } + + if profile.Cpu >= idProfilTheorical { + profilProposal = profile + break + } + } + return profilProposal +} + +func findBestDownProfile(profils []SizeProfile, idProfilTheorical float64, hostSizeMin float64) SizeProfile { + profilProposal := profils[0] + + // Iterate in reverse order to find the first profile below the theoretical value + for idx := len(profils) - 1; idx >= 0; idx-- { + profile := profils[idx] + + if profile.Cpu < hostSizeMin { + profilProposal = profils[idx+1] + break + } + + if profile.Cpu <= idProfilTheorical { + profilProposal = profile + break + } + } + return profilProposal +} + func ScalingInstance(projectContext entity.ProjectGlobal) { usages := map[string]UsageApp{} - log.Println("Process Horizontal Auto-Scaling...") + log.Println("Process Horizontal Auto-Scaling... (without down-time)") GetProjectMetrics(projectContext, usages) GetContainersUsage(projectContext, usages) @@ -201,11 +251,11 @@ func ScalingInstance(projectContext entity.ProjectGlobal) { } // ListContainerSize retrieves the size profiles of containers in the project. -func ListContainerSize(projectContext entity.ProjectGlobal) []float64 { // SizeProfile { +func ListContainerSize(projectContext entity.ProjectGlobal, name string) []SizeProfile { log.Println("Get Size of Profile available...") payload := []string{ "--environment=" + projectContext.DefaultEnv, - "--service=" + app.ArgsS.Name, + "--service=" + name, "--no-header", "--format=csv", "--no-interaction", @@ -213,25 +263,24 @@ func ListContainerSize(projectContext entity.ProjectGlobal) []float64 { // SizeP } output, err := utils.CallCLIString(projectContext, "resources:size:list", payload...) if err != nil { - log.Fatalf("command execution failed: %s", err) + log.Printf("No profile for this container") } // Parse output size_tpl := strings.Split(output, "\n") - var sizeProfiles []float64 // SizeProfile + var sizeProfiles []SizeProfile for _, size_str := range size_tpl[:len(size_tpl)-1] { size := strings.Split(size_str, ",") // Normalize - // cpu, _ := strconv.ParseFloat(size[1], 64) - // mem, _ := strconv.ParseInt(size[2], 10, 32) - // profile := SizeProfile{ - // Id: size[0], - // Cpu: cpu, - // Mem: int32(mem), - // } - sizeId, _ := strconv.ParseFloat(size[0], 64) - sizeProfiles = append(sizeProfiles, sizeId) // profile) + cpu, _ := strconv.ParseFloat(size[1], 64) + mem, _ := strconv.ParseInt(size[2], 10, 32) + profile := SizeProfile{ + Id: size[0], + Cpu: cpu, + Mem: int32(mem), + } + sizeProfiles = append(sizeProfiles, profile) } return sizeProfiles @@ -345,6 +394,14 @@ func RemoveUnscaleContainers(projectContext entity.ProjectGlobal, usages map[str delete(usages, srv) } } + + for _, usage := range usages { + if strings.Contains(usage.Name, "---internal---storage") { + delete(usages, usage.Name) + } + + //if (usage.Name) + } } func SetResources(projectContext entity.ProjectGlobal, usages map[string]UsageApp) { @@ -371,7 +428,7 @@ func SetResources(projectContext entity.ProjectGlobal, usages map[string]UsageAp } updateContainer += usage.Name updateContainer += ":" - updateContainer += fmt.Sprintf("%.1f", usage.IdProfileNew) + updateContainer += usage.IdProfileNew.Id } }