mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-18 14:40:26 +01:00
dba0553e11
Rounding GOMAXPROCS to the upper interger value of cpuQuota increases chances of CPU starvation, non-optimimal goroutine scheduling and additional CPU overhead related to context switching. So it is better to round GOMAXPROCS to the lower integer value of cpuQuota.
158 lines
3.8 KiB
Go
158 lines
3.8 KiB
Go
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
|
|
}
|