mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-07-24 01:31:20 +03:00
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
185 lines
4.7 KiB
Go
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
|
|
}
|