VictoriaMetrics/lib/cgroup/cpu.go

158 lines
3.8 KiB
Go
Raw Permalink Normal View History

package cgroup
import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
"github.com/VictoriaMetrics/metrics"
)
// 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 := cpuQuota
if cpuCoresAvailable <= 0 {
cpuCoresAvailable = float64(runtime.NumCPU())
}
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
}
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
}
func getCPUQuotaV2(sysPrefix, cgroupPath string) (float64, error) {
data, err := getFileContents("cpu.max", sysPrefix, cgroupPath, "")
if err != nil {
return 0, err
}
data = strings.TrimSpace(data)
n, err := parseCPUMax(data)
if err != nil {
return 0, fmt.Errorf("cannot parse cpu.max file contents: %w", err)
}
return n, 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)
}
period, err := strconv.ParseUint(bounds[1], 10, 64)
if err != nil {
return 0, fmt.Errorf("cannot parse period: %w", err)
}
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
}