Files
VictoriaMetrics/lib/cgroup/cpu.go
andriibeee a3df0f890b lib/cgroup: support reading cpu/memory limits from systemd slices
cgroup v2 version supports slices ( aka path hierarchy) for resource limits. It's mostly supported by systemd
and container runtime build on top of it.

 This commit reads subpath for systemd slices and traverse it with reading minimal limit value.

Related docs:
https://docs.oracle.com/en/operating-systems/oracle-linux/9/systemd/SystemdMngCgroupsV2.html#SlicesServicesScopesHierarchy
https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10635
2026-04-22 10:18:03 +02:00

185 lines
4.7 KiB
Go

package cgroup
import (
"fmt"
"os"
"path"
"runtime"
"strconv"
"strings"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// AvailableCPUs returns the number of available CPU cores for the app.
//
// The number is rounded to the next integer value if fractional number of CPU cores are available.
func AvailableCPUs() int {
return runtime.GOMAXPROCS(-1)
}
func init() {
cpuQuota := getCPUQuota()
if cpuQuota > 0 {
updateGOMAXPROCSToCPUQuota(cpuQuota)
}
cpuCoresAvailable := float64(runtime.NumCPU())
if cpuQuota > 0 && cpuCoresAvailable > cpuQuota {
cpuCoresAvailable = cpuQuota
}
metrics.NewGauge(`process_cpu_cores_available`, func() float64 {
return cpuCoresAvailable
})
}
// updateGOMAXPROCSToCPUQuota updates GOMAXPROCS to cpuQuota if GOMAXPROCS isn't set in environment var.
func updateGOMAXPROCSToCPUQuota(cpuQuota float64) {
if v := os.Getenv("GOMAXPROCS"); v != "" {
// Do not override explicitly set GOMAXPROCS.
return
}
// Round gomaxprocs to the floor of cpuQuota, since Go runtime doesn't work well
// with fractional available CPU cores.
gomaxprocs := int(cpuQuota)
if gomaxprocs <= 0 {
gomaxprocs = 1
}
if cpuQuota > float64(gomaxprocs) {
logger.Warnf("rounding CPU quota %.1f to %d CPUs for performance reasons - see https://docs.victoriametrics.com/victoriametrics/bestpractices/#kubernetes", cpuQuota, gomaxprocs)
}
numCPU := runtime.NumCPU()
if gomaxprocs > numCPU {
// There is no sense in setting more GOMAXPROCS than the number of available CPU cores.
gomaxprocs = numCPU
}
runtime.GOMAXPROCS(gomaxprocs)
}
func getCPUQuota() float64 {
cpuQuota, err := getCPUQuotaGeneric()
if err != nil {
return 0
}
if cpuQuota <= 0 {
// The quota isn't set. This may be the case in multilevel containers.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/685#issuecomment-674423728
return getOnlineCPUCount()
}
return cpuQuota
}
func getCPUQuotaGeneric() (float64, error) {
quotaUS, err := getCPUStat("cpu.cfs_quota_us")
if err == nil {
periodUS, err := getCPUStat("cpu.cfs_period_us")
if err == nil {
return float64(quotaUS) / float64(periodUS), nil
}
}
return getCPUQuotaV2("/sys/fs/cgroup", "/proc/self/cgroup")
}
func getCPUStat(statName string) (int64, error) {
return getStatGeneric(statName, "/sys/fs/cgroup/cpu", "/proc/self/cgroup", "cpu,")
}
func getOnlineCPUCount() float64 {
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/685#issuecomment-674423728
data, err := os.ReadFile("/sys/devices/system/cpu/online")
if err != nil {
return -1
}
n := float64(countCPUs(string(data)))
if n <= 0 {
return -1
}
return n
}
// See https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html
func getCPUQuotaV2(sysfsPrefix, cgroupPath string) (float64, error) {
subPath, err := readCgroupV2SubPath(cgroupPath)
if err != nil {
subPath = "/"
}
var minQuota float64 = -1
for {
// travers sub path hierarchy and use a minimal value for stat
data, err := os.ReadFile(path.Join(sysfsPrefix, subPath, "cpu.max"))
if err == nil {
quota, err := parseCPUMax(strings.TrimSpace(string(data)))
if err != nil {
return 0, fmt.Errorf("cannot parse cpu.max at %s: %w", subPath, err)
}
if quota > 0 && (minQuota < 0 || quota < minQuota) {
minQuota = quota
}
}
if subPath == "/" || subPath == "." {
break
}
subPath = path.Dir(subPath)
}
return minQuota, nil
}
// See https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#cpu
func parseCPUMax(data string) (float64, error) {
bounds := strings.Split(data, " ")
if len(bounds) > 2 {
return 0, fmt.Errorf("unexpected line format: want 'quota period'; got: %s", data)
}
if bounds[0] == "max" {
return -1, nil
}
quota, err := strconv.ParseUint(bounds[0], 10, 64)
if err != nil {
return 0, fmt.Errorf("cannot parse quota: %w", err)
}
// The default is “max 100000”.
period := uint64(100_000)
if len(bounds) == 2 {
period, err = strconv.ParseUint(bounds[1], 10, 64)
if err != nil {
return 0, fmt.Errorf("cannot parse period: %w", err)
}
if period == 0 {
return 0, fmt.Errorf("zero value for period is not allowed")
}
}
return float64(quota) / float64(period), nil
}
func countCPUs(data string) int {
data = strings.TrimSpace(data)
n := 0
for _, s := range strings.Split(data, ",") {
n++
if !strings.Contains(s, "-") {
if _, err := strconv.Atoi(s); err != nil {
return -1
}
continue
}
bounds := strings.Split(s, "-")
if len(bounds) != 2 {
return -1
}
start, err := strconv.Atoi(bounds[0])
if err != nil {
return -1
}
end, err := strconv.Atoi(bounds[1])
if err != nil {
return -1
}
n += end - start
}
return n
}