diff --git a/core/stat/internal/cgroup_linux.go b/core/stat/internal/cgroup_linux.go index 6ac19853..a9503703 100644 --- a/core/stat/internal/cgroup_linux.go +++ b/core/stat/internal/cgroup_linux.go @@ -2,6 +2,7 @@ package internal import ( "bufio" + "errors" "fmt" "math" "os" @@ -18,6 +19,7 @@ import ( const ( cgroupDir = "/sys/fs/cgroup" + cpuMaxFile = cgroupDir + "/cpu.max" cpuStatFile = cgroupDir + "/cpu.stat" cpusetFile = cgroupDir + "/cpuset.cpus.effective" ) @@ -30,10 +32,9 @@ var ( ) type cgroup interface { - cpuQuotaUs() (int64, error) - cpuPeriodUs() (uint64, error) - cpus() ([]uint64, error) - usageAllCpus() (uint64, error) + cpuQuota() (float64, error) + cpuUsage() (uint64, error) + effectiveCpus() (int, error) } func currentCgroup() (cgroup, error) { @@ -48,13 +49,22 @@ type cgroupV1 struct { cgroups map[string]string } -func (c *cgroupV1) cpuQuotaUs() (int64, error) { - data, err := iox.ReadText(path.Join(c.cgroups["cpu"], "cpu.cfs_quota_us")) +func (c *cgroupV1) cpuQuota() (float64, error) { + quotaUs, err := c.cpuQuotaUs() if err != nil { return 0, err } - return strconv.ParseInt(data, 10, 64) + if quotaUs == -1 { + return -1, nil + } + + periodUs, err := c.cpuPeriodUs() + if err != nil { + return 0, err + } + + return float64(quotaUs) / float64(periodUs), nil } func (c *cgroupV1) cpuPeriodUs() (uint64, error) { @@ -66,16 +76,16 @@ func (c *cgroupV1) cpuPeriodUs() (uint64, error) { return parseUint(data) } -func (c *cgroupV1) cpus() ([]uint64, error) { - data, err := iox.ReadText(path.Join(c.cgroups["cpuset"], "cpuset.cpus")) +func (c *cgroupV1) cpuQuotaUs() (int64, error) { + data, err := iox.ReadText(path.Join(c.cgroups["cpu"], "cpu.cfs_quota_us")) if err != nil { - return nil, err + return 0, err } - return parseUints(data) + return strconv.ParseInt(data, 10, 64) } -func (c *cgroupV1) usageAllCpus() (uint64, error) { +func (c *cgroupV1) cpuUsage() (uint64, error) { data, err := iox.ReadText(path.Join(c.cgroups["cpuacct"], "cpuacct.usage")) if err != nil { return 0, err @@ -84,38 +94,53 @@ func (c *cgroupV1) usageAllCpus() (uint64, error) { return parseUint(data) } +func (c *cgroupV1) effectiveCpus() (int, error) { + data, err := iox.ReadText(path.Join(c.cgroups["cpuset"], "cpuset.cpus")) + if err != nil { + return 0, err + } + + cpus, err := parseUints(data) + if err != nil { + return 0, err + } + + return len(cpus), nil +} + type cgroupV2 struct { cgroups map[string]string } -func (c *cgroupV2) cpuQuotaUs() (int64, error) { - data, err := iox.ReadText(path.Join(cgroupDir, "cpu.cfs_quota_us")) +func (c *cgroupV2) cpuQuota() (float64, error) { + data, err := iox.ReadText(cpuMaxFile) if err != nil { return 0, err } - return strconv.ParseInt(data, 10, 64) -} + fields := strings.Fields(data) + if len(fields) != 2 { + return 0, fmt.Errorf("cgroup: bad /sys/fs/cgroup/cpu.max file: %s", data) + } -func (c *cgroupV2) cpuPeriodUs() (uint64, error) { - data, err := iox.ReadText(path.Join(cgroupDir, "cpu.cfs_period_us")) + if fields[0] == "max" { + return -1, nil + } + + quotaUs, err := strconv.ParseInt(fields[0], 10, 64) if err != nil { return 0, err } - return parseUint(data) -} - -func (c *cgroupV2) cpus() ([]uint64, error) { - data, err := iox.ReadText(cpusetFile) + periodUs, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { - return nil, err + return 0, err } - return parseUints(data) + return float64(quotaUs) / float64(periodUs), nil } -func (c *cgroupV2) usageAllCpus() (uint64, error) { +func (c *cgroupV2) cpuUsage() (uint64, error) { usec, err := parseUint(c.cgroups["usage_usec"]) if err != nil { return 0, err @@ -124,6 +149,20 @@ func (c *cgroupV2) usageAllCpus() (uint64, error) { return usec * uint64(time.Microsecond), nil } +func (c *cgroupV2) effectiveCpus() (int, error) { + data, err := iox.ReadText(cpusetFile) + if err != nil { + return 0, err + } + + cpus, err := parseUints(data) + if err != nil { + return 0, err + } + + return len(cpus), nil +} + func currentCgroupV1() (cgroup, error) { cgroupFile := fmt.Sprintf("/proc/%d/cgroup", os.Getpid()) lines, err := iox.ReadTextLines(cgroupFile, iox.WithoutBlank()) @@ -200,7 +239,7 @@ func isCgroup2UnifiedMode() bool { func parseUint(s string) (uint64, error) { v, err := strconv.ParseInt(s, 10, 64) if err != nil { - if err.(*strconv.NumError).Err == strconv.ErrRange { + if errors.Is(err, strconv.ErrRange) { return 0, nil } @@ -225,21 +264,21 @@ func parseUints(val string) ([]uint64, error) { for _, r := range cols { if strings.Contains(r, "-") { fields := strings.SplitN(r, "-", 2) - min, err := parseUint(fields[0]) + minimum, err := parseUint(fields[0]) if err != nil { return nil, fmt.Errorf("cgroup: bad int list format: %s", val) } - max, err := parseUint(fields[1]) + maximum, err := parseUint(fields[1]) if err != nil { return nil, fmt.Errorf("cgroup: bad int list format: %s", val) } - if max < min { + if maximum < minimum { return nil, fmt.Errorf("cgroup: bad int list format: %s", val) } - for i := min; i <= max; i++ { + for i := minimum; i <= maximum; i++ { if _, ok := ints[i]; !ok { ints[i] = lang.Placeholder sets = append(sets, i) diff --git a/core/stat/internal/cgroup_linux_test.go b/core/stat/internal/cgroup_linux_test.go index 15ab1d43..5b2eaeaa 100644 --- a/core/stat/internal/cgroup_linux_test.go +++ b/core/stat/internal/cgroup_linux_test.go @@ -16,15 +16,23 @@ func TestCgroupV1(t *testing.T) { if isCgroup2UnifiedMode() { cg, err := currentCgroupV1() assert.NoError(t, err) - _, err = cg.cpus() + _, err = cg.effectiveCpus() assert.Error(t, err) - _, err = cg.cpuPeriodUs() + _, err = cg.cpuQuota() assert.Error(t, err) - _, err = cg.cpuQuotaUs() - assert.Error(t, err) - _, err = cg.usageAllCpus() + _, err = cg.cpuUsage() assert.Error(t, err) } + + // test cgroup v2 + cg, err := currentCgroupV2() + assert.NoError(t, err) + _, err = cg.effectiveCpus() + assert.NoError(t, err) + _, err = cg.cpuQuota() + assert.Error(t, err) + _, err = cg.cpuUsage() + assert.NoError(t, err) } func TestParseUint(t *testing.T) { diff --git a/core/stat/internal/cpu_linux.go b/core/stat/internal/cpu_linux.go index 1d686ce0..245d0ad8 100644 --- a/core/stat/internal/cpu_linux.go +++ b/core/stat/internal/cpu_linux.go @@ -15,40 +15,31 @@ const ( cpuTicks = 100 cpuFields = 8 cpuMax = 1000 - statDir = "/proc/stat" + statFile = "/proc/stat" ) var ( preSystem uint64 preTotal uint64 - quota float64 + limit float64 cores uint64 initOnce sync.Once ) // if /proc not present, ignore the cpu calculation, like wsl linux func initialize() { - cpus, err := cpuSets() + cpus, err := effectiveCpus() if err != nil { logx.Error(err) return } - cores = uint64(len(cpus)) - quota = float64(len(cpus)) - cq, err := cpuQuota() - if err == nil { - if cq != -1 { - period, err := cpuPeriod() - if err != nil { - logx.Error(err) - return - } - - limit := float64(cq) / float64(period) - if limit < quota { - quota = limit - } + cores = uint64(cpus) + limit = float64(cpus) + quota, err := cpuQuota() + if err == nil && quota > 0 { + if quota < limit { + limit = quota } } @@ -58,7 +49,7 @@ func initialize() { return } - preTotal, err = totalCpuUsage() + preTotal, err = cpuUsage() if err != nil { logx.Error(err) return @@ -69,7 +60,7 @@ func initialize() { func RefreshCpu() uint64 { initOnce.Do(initialize) - total, err := totalCpuUsage() + total, err := cpuUsage() if err != nil { return 0 } @@ -83,7 +74,7 @@ func RefreshCpu() uint64 { cpuDelta := total - preTotal systemDelta := system - preSystem if cpuDelta > 0 && systemDelta > 0 { - usage = uint64(float64(cpuDelta*cores*cpuMax) / (float64(systemDelta) * quota)) + usage = uint64(float64(cpuDelta*cores*cpuMax) / (float64(systemDelta) * limit)) if usage > cpuMax { usage = cpuMax } @@ -94,35 +85,35 @@ func RefreshCpu() uint64 { return usage } -func cpuQuota() (int64, error) { +func cpuQuota() (float64, error) { cg, err := currentCgroup() if err != nil { return 0, err } - return cg.cpuQuotaUs() + return cg.cpuQuota() } -func cpuPeriod() (uint64, error) { +func cpuUsage() (uint64, error) { cg, err := currentCgroup() if err != nil { return 0, err } - return cg.cpuPeriodUs() + return cg.cpuUsage() } -func cpuSets() ([]uint64, error) { +func effectiveCpus() (int, error) { cg, err := currentCgroup() if err != nil { - return nil, err + return 0, err } - return cg.cpus() + return cg.effectiveCpus() } func systemCpuUsage() (uint64, error) { - lines, err := iox.ReadTextLines(statDir, iox.WithoutBlank()) + lines, err := iox.ReadTextLines(statFile, iox.WithoutBlank()) if err != nil { return 0, err } @@ -150,12 +141,3 @@ func systemCpuUsage() (uint64, error) { return 0, errors.New("bad stats format") } - -func totalCpuUsage() (usage uint64, err error) { - var cg cgroup - if cg, err = currentCgroup(); err != nil { - return - } - - return cg.usageAllCpus() -}