Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable cpu limiting using cgroup v2 #8471

Merged
merged 2 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions components/common-go/cgroups/cgroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package cgroups

import (
"os"
"path/filepath"
"strings"
)

const DefaultCGroupMount = "/sys/fs/cgroup"

type CgroupSetup int

const (
Unknown CgroupSetup = iota
Legacy
Unified
)

func (s CgroupSetup) String() string {
return [...]string{"Legacy", "Unified"}[s]
}

func GetCgroupSetup() (CgroupSetup, error) {
controllers := filepath.Join(DefaultCGroupMount, "cgroup.controllers")
_, err := os.Stat(controllers)

if os.IsNotExist(err) {
return Legacy, nil
}

if err == nil {
return Unified, nil
}

return Unknown, err
}

func IsUnifiedCgroupSetup() (bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reflecting my opinion❤️

setup, err := GetCgroupSetup()
if err != nil {
return false, err
}

return setup == Unified, nil
}

func IsLegacyCgroupSetup() (bool, error) {
setup, err := GetCgroupSetup()
if err != nil {
return false, err
}

return setup == Legacy, nil
}

func EnsureCpuControllerEnabled(basePath, cgroupPath string) error {
targetPath := filepath.Join(basePath, cgroupPath)
if enabled, err := isCpuControllerEnabled(targetPath); err != nil || enabled {
return err
}

err := writeCpuController(basePath)
if err != nil {
return err
}

levelPath := basePath
cgroupPath = strings.TrimPrefix(cgroupPath, "/")
levels := strings.Split(cgroupPath, string(os.PathSeparator))
for _, l := range levels[:len(levels)-1] {
levelPath = filepath.Join(levelPath, l)
err = writeCpuController(levelPath)
if err != nil {
return err
}
}

return nil
}

func isCpuControllerEnabled(path string) (bool, error) {
controllerFile := filepath.Join(path, "cgroup.controllers")
controllers, err := os.ReadFile(controllerFile)
if err != nil {
return false, err
}

for _, ctrl := range strings.Fields(string(controllers)) {
if ctrl == "cpu" {
// controller is already activated
return true, nil
}
}

return false, nil
}

func writeCpuController(path string) error {
f, err := os.OpenFile(filepath.Join(path, "cgroup.subtree_control"), os.O_WRONLY, 0)
if err != nil {
return err
}
defer f.Close()

_, err = f.Write([]byte("+cpu"))
if err != nil {
return err
}

return nil
}
89 changes: 89 additions & 0 deletions components/common-go/cgroups/cgroups_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package cgroups

import (
"os"
"path/filepath"
"testing"
)

var cgroupPath = []string{"kubepods", "burstable", "pods234sdf", "234as8df34"}

func createHierarchy(t *testing.T, cpuEnabled bool) (string, string) {
testRoot := t.TempDir()
if err := os.WriteFile(filepath.Join(testRoot, "cgroup.controllers"), []byte(""), 0755); err != nil {
t.Fatal(err)
}

if err := os.WriteFile(filepath.Join(testRoot, "cgroup.subtree_control"), []byte(""), 0755); err != nil {
t.Fatal(err)
}

testCgroup := ""
for i, level := range cgroupPath {
testCgroup = filepath.Join(testCgroup, level)
fullPath := filepath.Join(testRoot, testCgroup)
if err := os.Mkdir(fullPath, 0o755); err != nil {
t.Fatal(err)
}

ctrlFile, err := os.Create(filepath.Join(fullPath, "cgroup.controllers"))
if err != nil {
t.Fatal(err)
}
defer ctrlFile.Close()

if cpuEnabled {
if _, err := ctrlFile.WriteString("cpu"); err != nil {
t.Fatal(err)
}
}

subTreeFile, err := os.Create(filepath.Join(fullPath, "cgroup.subtree_control"))
if err != nil {
t.Fatal(err)
}
defer subTreeFile.Close()

if cpuEnabled && i < len(cgroupPath)-1 {
if _, err := subTreeFile.WriteString("cpu"); err != nil {
t.Fatal(err)
}
}
}

return testRoot, testCgroup
}

func TestEnableController(t *testing.T) {
root, cgroup := createHierarchy(t, false)
if err := EnsureCpuControllerEnabled(root, cgroup); err != nil {
t.Fatal(err)
}

levelPath := root
for _, level := range cgroupPath {
verifyCpuControllerToggled(t, levelPath, true)
levelPath = filepath.Join(levelPath, level)
}

verifyCpuControllerToggled(t, levelPath, false)
}

func verifyCpuControllerToggled(t *testing.T, path string, enabled bool) {
t.Helper()

content, err := os.ReadFile(filepath.Join(path, "cgroup.subtree_control"))
if err != nil {
t.Fatal(err)
}

if enabled && string(content) != "+cpu" {
t.Fatalf("%s should have enabled cpu controller", path)
} else if !enabled && string(content) == "+cpu" {
t.Fatalf("%s should not have enabled cpu controller", path)
}
}
22 changes: 11 additions & 11 deletions components/ws-daemon/pkg/cpulimit/cfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import (
"golang.org/x/xerrors"
)

// CgroupCFSController controls a cgroup's CFS settings
type CgroupCFSController string
// CgroupV1CFSController controls a cgroup's CFS settings
type CgroupV1CFSController string

// Usage returns the cpuacct.usage value of the cgroup
func (basePath CgroupCFSController) Usage() (usage CPUTime, err error) {
func (basePath CgroupV1CFSController) Usage() (usage CPUTime, err error) {
cputime, err := basePath.readCpuUsage()
if err != nil {
return 0, xerrors.Errorf("cannot read cpuacct.usage: %w", err)
Expand All @@ -30,7 +30,7 @@ func (basePath CgroupCFSController) Usage() (usage CPUTime, err error) {
}

// SetQuota sets a new CFS quota on the cgroup
func (basePath CgroupCFSController) SetLimit(limit Bandwidth) (changed bool, err error) {
func (basePath CgroupV1CFSController) SetLimit(limit Bandwidth) (changed bool, err error) {
period, err := basePath.readCfsPeriod()
if err != nil {
err = xerrors.Errorf("cannot parse CFS period: %w", err)
Expand All @@ -55,8 +55,8 @@ func (basePath CgroupCFSController) SetLimit(limit Bandwidth) (changed bool, err
return true, nil
}

func (basePath CgroupCFSController) readParentQuota() time.Duration {
parent := CgroupCFSController(filepath.Dir(string(basePath)))
func (basePath CgroupV1CFSController) readParentQuota() time.Duration {
parent := CgroupV1CFSController(filepath.Dir(string(basePath)))
pq, err := parent.readCfsQuota()
if err != nil {
return time.Duration(0)
Expand All @@ -65,7 +65,7 @@ func (basePath CgroupCFSController) readParentQuota() time.Duration {
return time.Duration(pq) * time.Microsecond
}

func (basePath CgroupCFSController) readString(path string) (string, error) {
func (basePath CgroupV1CFSController) readString(path string) (string, error) {
fn := filepath.Join(string(basePath), path)
fc, err := os.ReadFile(fn)
if err != nil {
Expand All @@ -76,7 +76,7 @@ func (basePath CgroupCFSController) readString(path string) (string, error) {
return s, nil
}

func (basePath CgroupCFSController) readCfsPeriod() (time.Duration, error) {
func (basePath CgroupV1CFSController) readCfsPeriod() (time.Duration, error) {
s, err := basePath.readString("cpu.cfs_period_us")
if err != nil {
return 0, err
Expand All @@ -89,7 +89,7 @@ func (basePath CgroupCFSController) readCfsPeriod() (time.Duration, error) {
return time.Duration(uint64(p)) * time.Microsecond, nil
}

func (basePath CgroupCFSController) readCfsQuota() (time.Duration, error) {
func (basePath CgroupV1CFSController) readCfsQuota() (time.Duration, error) {
s, err := basePath.readString("cpu.cfs_quota_us")
if err != nil {
return 0, err
Expand All @@ -106,7 +106,7 @@ func (basePath CgroupCFSController) readCfsQuota() (time.Duration, error) {
return time.Duration(p) * time.Microsecond, nil
}

func (basePath CgroupCFSController) readCpuUsage() (time.Duration, error) {
func (basePath CgroupV1CFSController) readCpuUsage() (time.Duration, error) {
s, err := basePath.readString("cpuacct.usage")
if err != nil {
return 0, err
Expand All @@ -120,7 +120,7 @@ func (basePath CgroupCFSController) readCpuUsage() (time.Duration, error) {
}

// NrThrottled returns the number of CFS periods the cgroup was throttled in
func (basePath CgroupCFSController) NrThrottled() (uint64, error) {
func (basePath CgroupV1CFSController) NrThrottled() (uint64, error) {
f, err := os.Open(filepath.Join(string(basePath), "cpu.stat"))
if err != nil {
return 0, xerrors.Errorf("cannot read cpu.stat: %w", err)
Expand Down
8 changes: 4 additions & 4 deletions components/ws-daemon/pkg/cpulimit/cfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestCfsSetLimit(t *testing.T) {
t.Fatal(err)
}

cfs := CgroupCFSController(tempdir)
cfs := CgroupV1CFSController(tempdir)
changed, err := cfs.SetLimit(tc.bandWidth)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -110,7 +110,7 @@ func TestReadCfsQuota(t *testing.T) {
t.Fatal(err)
}

cfs := CgroupCFSController(tempdir)
cfs := CgroupV1CFSController(tempdir)
v, err := cfs.readCfsQuota()
if err != nil {
t.Fatal(err)
Expand All @@ -132,7 +132,7 @@ func TestReadCfsPeriod(t *testing.T) {
t.Fatal(err)
}

cfs := CgroupCFSController(tempdir)
cfs := CgroupV1CFSController(tempdir)
v, err := cfs.readCfsPeriod()
if err != nil {
t.Fatal(err)
Expand All @@ -155,7 +155,7 @@ func TestReadCpuUsage(t *testing.T) {
t.Fatal(err)
}

cfs := CgroupCFSController(tempdir)
cfs := CgroupV1CFSController(tempdir)
v, err := cfs.readCpuUsage()
if err != nil {
t.Fatal(err)
Expand Down
Loading