2020-08-11 19:54:13 +00:00
package cgroup
import (
2021-05-13 06:02:13 +00:00
"fmt"
2020-08-11 19:54:13 +00:00
"os"
"runtime"
2020-09-22 20:26:44 +00:00
"strconv"
"strings"
2022-01-31 18:07:50 +00:00
"github.com/VictoriaMetrics/metrics"
2025-01-29 12:19:08 +00:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
2020-08-11 19:54:13 +00:00
)
2020-12-08 18:49:32 +00:00
// AvailableCPUs returns the number of available CPU cores for the app.
2022-01-31 18:07:50 +00:00
//
// The number is rounded to the next integer value if fractional number of CPU cores are available.
2020-12-08 18:49:32 +00:00
func AvailableCPUs ( ) int {
return runtime . GOMAXPROCS ( - 1 )
}
2021-07-05 09:31:26 +00:00
func init ( ) {
2022-01-31 18:30:06 +00:00
cpuQuota := getCPUQuota ( )
if cpuQuota > 0 {
updateGOMAXPROCSToCPUQuota ( cpuQuota )
}
cpuCoresAvailable := cpuQuota
if cpuCoresAvailable <= 0 {
cpuCoresAvailable = float64 ( runtime . NumCPU ( ) )
}
2022-01-31 18:07:50 +00:00
metrics . NewGauge ( ` process_cpu_cores_available ` , func ( ) float64 {
return cpuCoresAvailable
} )
2021-07-05 09:31:26 +00:00
}
2020-12-08 18:49:32 +00:00
2022-01-31 18:30:06 +00:00
// updateGOMAXPROCSToCPUQuota updates GOMAXPROCS to cpuQuota if GOMAXPROCS isn't set in environment var.
func updateGOMAXPROCSToCPUQuota ( cpuQuota float64 ) {
2020-08-11 19:54:13 +00:00
if v := os . Getenv ( "GOMAXPROCS" ) ; v != "" {
2020-08-28 06:40:53 +00:00
// Do not override explicitly set GOMAXPROCS.
2020-08-11 19:54:13 +00:00
return
}
2024-09-23 14:09:10 +00:00
// 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
}
2025-01-29 12:19:08 +00:00
if cpuQuota > float64 ( gomaxprocs ) {
logger . Warnf ( "rounding CPU quota %.1f to %d CPUs for performance reasons - see https://docs.victoriametrics.com/bestpractices/#kubernetes" , cpuQuota , gomaxprocs )
}
2024-09-23 14:09:10 +00:00
2020-08-28 06:40:53 +00:00
numCPU := runtime . NumCPU ( )
if gomaxprocs > numCPU {
// There is no sense in setting more GOMAXPROCS than the number of available CPU cores.
2022-01-31 18:07:50 +00:00
gomaxprocs = numCPU
2020-08-28 06:40:53 +00:00
}
2024-09-23 14:09:10 +00:00
2020-08-11 19:54:13 +00:00
runtime . GOMAXPROCS ( gomaxprocs )
}
func getCPUQuota ( ) float64 {
2021-05-13 06:26:20 +00:00
cpuQuota , err := getCPUQuotaGeneric ( )
2020-08-11 19:54:13 +00:00
if err != nil {
return 0
}
2021-05-13 06:02:13 +00:00
if cpuQuota <= 0 {
2020-09-22 20:26:44 +00:00
// 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 ( )
}
2021-05-13 06:02:13 +00:00
return cpuQuota
}
2021-05-13 06:26:20 +00:00
func getCPUQuotaGeneric ( ) ( float64 , error ) {
2021-05-13 06:02:13 +00:00
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
}
2020-08-11 19:54:13 +00:00
}
2021-05-13 06:26:20 +00:00
return getCPUQuotaV2 ( "/sys/fs/cgroup" , "/proc/self/cgroup" )
2020-08-11 19:54:13 +00:00
}
2020-09-22 20:26:44 +00:00
2021-02-08 13:49:02 +00:00
func getCPUStat ( statName string ) ( int64 , error ) {
return getStatGeneric ( statName , "/sys/fs/cgroup/cpu" , "/proc/self/cgroup" , "cpu," )
}
2020-09-22 20:26:44 +00:00
func getOnlineCPUCount ( ) float64 {
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/685#issuecomment-674423728
2022-08-21 20:51:13 +00:00
data , err := os . ReadFile ( "/sys/devices/system/cpu/online" )
2020-09-22 20:26:44 +00:00
if err != nil {
return - 1
}
n := float64 ( countCPUs ( string ( data ) ) )
if n <= 0 {
return - 1
}
2020-09-29 10:54:25 +00:00
return n
2020-09-22 20:26:44 +00:00
}
2021-05-13 06:26:20 +00:00
func getCPUQuotaV2 ( sysPrefix , cgroupPath string ) ( float64 , error ) {
2021-05-13 06:02:13 +00:00
data , err := getFileContents ( "cpu.max" , sysPrefix , cgroupPath , "" )
if err != nil {
return 0 , err
}
2021-05-13 06:26:20 +00:00
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
2021-05-13 06:02:13 +00:00
}
2021-05-13 06:26:20 +00:00
// See https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#cpu
2021-05-13 06:02:13 +00:00
func parseCPUMax ( data string ) ( float64 , error ) {
bounds := strings . Split ( data , " " )
if len ( bounds ) != 2 {
2021-05-13 06:26:20 +00:00
return 0 , fmt . Errorf ( "unexpected line format: want 'quota period'; got: %s" , data )
2021-05-13 06:02:13 +00:00
}
if bounds [ 0 ] == "max" {
return - 1 , nil
}
quota , err := strconv . ParseUint ( bounds [ 0 ] , 10 , 64 )
if err != nil {
2021-05-13 06:26:20 +00:00
return 0 , fmt . Errorf ( "cannot parse quota: %w" , err )
2021-05-13 06:02:13 +00:00
}
period , err := strconv . ParseUint ( bounds [ 1 ] , 10 , 64 )
if err != nil {
2021-05-13 06:26:20 +00:00
return 0 , fmt . Errorf ( "cannot parse period: %w" , err )
2021-05-13 06:02:13 +00:00
}
return float64 ( quota ) / float64 ( period ) , nil
}
2020-09-22 20:26:44 +00:00
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
}