internal/checks/efficiency.go
Rama main · réplica de lectura del repositorio original.
package checks
import (
"strconv"
"strings"
"k8s.io/apimachinery/pkg/api/resource"
"github.com/sophotechlabs/spinoza/internal/api"
)
const (
overprovisionRatio = 4
bytesPerMi = 1024 * 1024
requests = "requests"
limits = "limits"
cpuName = "cpu"
memoryName = "memory"
)
func efficiencyChecks() []check {
return []check{
{
id: "requests-missing",
title: "No resource requests",
category: categoryEfficiency,
severity: severityMedium,
wrong: "The scheduler places it as if it needed nothing, so the node fills past what it can serve.",
remedy: "Set resources.requests.cpu and resources.requests.memory to what it needs at rest.",
find: overContainers(requestsMissing),
},
{
id: "limits-missing",
title: "No resource limits",
category: categoryEfficiency,
severity: severityLow,
wrong: "One runaway process can take the node and evict everything on it.",
remedy: "Set resources.limits.memory at minimum; that is what stops a leak.",
find: overContainers(limitsMissing),
},
{
id: "limits-far-above-requests",
title: "Limits far above requests",
category: categoryEfficiency,
severity: severityLow,
wrong: "The node was scheduled for the request and can be asked for the limit.",
remedy: "Raise the request to what it uses, or lower the limit.",
find: overContainers(limitsAboveRequests),
},
{
id: "requests-far-above-usage",
title: "Requests far above measured usage",
category: categoryEfficiency,
severity: severityLow,
needsUsage: true,
wrong: "Reserved capacity nothing uses: the cluster looks full while nodes sit idle.",
remedy: "Lower the requests towards measured usage, leaving headroom for peaks.",
find: overUsage(requestsAboveUsage),
},
}
}
func quantityAt(spec map[string]any, section, name string) (resource.Quantity, bool) {
resources, ok := spec["resources"].(map[string]any)
if !ok {
return resource.Quantity{}, false
}
part, ok := resources[section].(map[string]any)
if !ok {
return resource.Quantity{}, false
}
return quantityFrom(part[name])
}
func quantityFrom(raw any) (resource.Quantity, bool) {
switch value := raw.(type) {
case string:
return parsed(value)
case int64:
return parsed(strconv.FormatInt(value, 10))
case float64:
return parsed(strconv.FormatFloat(value, 'f', -1, 64))
default:
return resource.Quantity{}, false
}
}
func parsed(raw string) (resource.Quantity, bool) {
value, err := resource.ParseQuantity(raw)
if err != nil {
return resource.Quantity{}, false
}
return value, true
}
func missingIn(container Container, section string) []string {
out := []string{}
for _, name := range []string{cpuName, memoryName} {
_, ok := quantityAt(container.Spec, section, name)
if !ok {
out = append(out, name)
}
}
return out
}
func requestsMissing(_ Subject, container Container) (string, string) {
missing := missingIn(container, requests)
if len(missing) == 0 {
return "", ""
}
return "no " + strings.Join(missing, " or ") + " request", ""
}
func limitsMissing(_ Subject, container Container) (string, string) {
missing := missingIn(container, limits)
if len(missing) == 0 {
return "", ""
}
return "no " + strings.Join(missing, " or ") + " limit", ""
}
func limitsAboveRequests(_ Subject, container Container) (string, string) {
cpu := ratioDetail(container, cpuName, milliOf)
if cpu != "" {
return cpu, ""
}
return ratioDetail(container, memoryName, plainOf), ""
}
func milliOf(value resource.Quantity) int64 {
return value.MilliValue()
}
func plainOf(value resource.Quantity) int64 {
return value.Value()
}
func ratioDetail(container Container, name string, scale func(resource.Quantity) int64) string {
request, hasRequest := quantityAt(container.Spec, requests, name)
limit, hasLimit := quantityAt(container.Spec, limits, name)
if !hasRequest || !hasLimit {
return ""
}
asked := scale(request)
allowed := scale(limit)
if asked <= 0 {
return ""
}
ratio := allowed / asked
if ratio < overprovisionRatio {
return ""
}
return name + " limit " + limit.String() + " is " + strconv.FormatInt(ratio, 10) +
"x the " + request.String() + " request"
}
func requestsAboveUsage(subject Subject, usage map[string]api.ResourceUsage) (string, string) {
measured, ok := meanUsage(subject, usage)
if !ok {
return "", ""
}
cpu := askedFor(subject, cpuName, milliOf)
if overprovisioned(cpu, measured.CPUMilli) {
return usageDetail("cpu", cpu, measured.CPUMilli, "m"), ""
}
memory := askedFor(subject, memoryName, plainOf) / bytesPerMi
if overprovisioned(memory, measured.MemoryMi) {
return usageDetail(memoryName, memory, measured.MemoryMi, "Mi"), ""
}
return "", ""
}
func overprovisioned(asked, used int64) bool {
if used <= 0 {
return false
}
return asked/used >= overprovisionRatio
}
func usageDetail(name string, asked, used int64, unit string) string {
return "pods request " + strconv.FormatInt(asked, 10) + unit + " " + name +
" and use " + strconv.FormatInt(used, 10) + unit
}
func askedFor(subject Subject, name string, scale func(resource.Quantity) int64) int64 {
var total int64
for _, container := range subject.Containers {
if container.Init {
continue
}
value, ok := quantityAt(container.Spec, requests, name)
if !ok {
continue
}
total += scale(value)
}
return total
}
func meanUsage(subject Subject, usage map[string]api.ResourceUsage) (api.ResourceUsage, bool) {
var total api.ResourceUsage
var seen int64
for _, pod := range subject.Pods {
found, ok := usage[subject.Ref.Namespace+"/"+pod.Name]
if !ok {
continue
}
total.CPUMilli += found.CPUMilli
total.MemoryMi += found.MemoryMi
seen++
}
if seen == 0 {
return api.ResourceUsage{}, false
}
return api.ResourceUsage{CPUMilli: total.CPUMilli / seen, MemoryMi: total.MemoryMi / seen}, true
}