Skip to content

Commit

Permalink
Support cgroups v2 (#44)
Browse files Browse the repository at this point in the history
Add support for setting GOMAXPROCS based on CPU allotment in a system
with cgroups v2.

This works by adding a new internal CGroups2 type that is able to
provide the CPU quota if the system is using cgroups v2.
In the main GOMAXPROCS assignment logic, we use this variant if we're
able to, falling back to the v1 version if not.

Resolves #21 

Co-authored-by: Abhinav Gupta <[email protected]>
Co-authored-by: Matt Way <[email protected]>
  • Loading branch information
3 people authored Apr 5, 2022
1 parent 7ff767a commit ad6f05b
Show file tree
Hide file tree
Showing 17 changed files with 461 additions and 2 deletions.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module go.uber.org/automaxprocs

go 1.18

require github.com/stretchr/testify v1.7.1
require (
github.com/prashantv/gostub v1.1.0
github.com/stretchr/testify v1.7.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
151 changes: 151 additions & 0 deletions internal/cgroups/cgroups2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) 2022 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

//go:build linux
// +build linux

package cgroups

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path"
"strconv"
"strings"
)

const (
// _cgroupv2CPUMax is the file name for the CGroup-V2 CPU max and period
// parameter.
_cgroupv2CPUMax = "cpu.max"
// _cgroupFSType is the Linux CGroup-V2 file system type used in
// `/proc/$PID/mountinfo`.
_cgroupv2FSType = "cgroup2"

_cgroupv2MountPoint = "/sys/fs/cgroup"

_cgroupV2CPUMaxDefaultPeriod = 100000
_cgroupV2CPUMaxQuotaMax = "max"
)

const (
_cgroupv2CPUMaxQuotaIndex = iota
_cgroupv2CPUMaxPeriodIndex
)

// ErrNotV2 indicates that the system is not using cgroups2.
var ErrNotV2 = errors.New("not using cgroups2")

// CGroups2 provides access to cgroups data for systems using cgroups2.
type CGroups2 struct {
mountPoint string
cpuMaxFile string
}

// NewCGroups2ForCurrentProcess builds a CGroups2 for the current process.
//
// This returns ErrNotV2 if the system is not using cgroups2.
func NewCGroups2ForCurrentProcess() (*CGroups2, error) {
return newCGroups2FromMountInfo(_procPathMountInfo)
}

func newCGroups2FromMountInfo(mountInfoPath string) (*CGroups2, error) {
isV2, err := isCGroupV2(mountInfoPath)
if err != nil {
return nil, err
}

if !isV2 {
return nil, ErrNotV2
}

return &CGroups2{
mountPoint: _cgroupv2MountPoint,
cpuMaxFile: _cgroupv2CPUMax,
}, nil
}

func isCGroupV2(procPathMountInfo string) (bool, error) {
var (
isV2 bool
newMountPoint = func(mp *MountPoint) error {
isV2 = mp.FSType == _cgroupv2FSType && mp.MountPoint == _cgroupv2MountPoint
return nil
}
)

if err := parseMountInfo(procPathMountInfo, newMountPoint); err != nil {
return false, err
}

return isV2, nil
}

// CPUQuota returns the CPU quota applied with the CPU cgroup2 controller.
// It is a result of reading cpu quota and period from cpu.max file.
// It will return `cpu.max / cpu.period`. If cpu.max is set to max, it returns
// (-1, false, nil)
func (cg *CGroups2) CPUQuota() (float64, bool, error) {
cpuMaxParams, err := os.Open(path.Join(cg.mountPoint, cg.cpuMaxFile))
if err != nil {
if os.IsNotExist(err) {
return -1, false, nil
}
return -1, false, err
}

scanner := bufio.NewScanner(cpuMaxParams)
if scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) == 0 || len(fields) > 2 {
return -1, false, fmt.Errorf("invalid format")
}

if fields[_cgroupv2CPUMaxQuotaIndex] == _cgroupV2CPUMaxQuotaMax {
return -1, false, nil
}

max, err := strconv.Atoi(fields[_cgroupv2CPUMaxQuotaIndex])
if err != nil {
return -1, false, err
}

var period int
if len(fields) == 1 {
period = _cgroupV2CPUMaxDefaultPeriod
} else {
period, err = strconv.Atoi(fields[_cgroupv2CPUMaxPeriodIndex])
if err != nil {
return -1, false, err
}
}

return float64(max) / float64(period), true, nil
}

if err := scanner.Err(); err != nil {
return -1, false, err
}

return 0, false, io.ErrUnexpectedEOF
}
161 changes: 161 additions & 0 deletions internal/cgroups/cgroups2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2022 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

//go:build linux
// +build linux

package cgroups

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

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCGroupsIsCGroupV2(t *testing.T) {
tests := []struct {
name string
isV2 bool
wantErr bool // should be false if isV2 is true
}{
{
name: "mountinfo",
isV2: false,
wantErr: false,
},
{
name: "mountinfo-v1-v2",
isV2: false,
wantErr: false,
},
{
name: "mountinfo-v2",
isV2: true,
wantErr: false,
},
{
name: "mountinfo-nonexistent",
isV2: false,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mountInfoPath := filepath.Join(testDataProcPath, "v2", tt.name)
_, err := newCGroups2FromMountInfo(mountInfoPath)
switch {
case tt.wantErr:
assert.Error(t, err)
case !tt.isV2:
assert.ErrorIs(t, err, ErrNotV2)
default:
assert.NoError(t, err)
}
})
}
}

func TestCGroupsCPUQuotaV2(t *testing.T) {
tests := []struct {
name string
want float64
wantOK bool
wantErr string
}{
{
name: "set",
want: 2.5,
wantOK: true,
},
{
name: "unset",
want: -1.0,
wantOK: false,
},
{
name: "only-max",
want: 5.0,
wantOK: true,
},
{
name: "invalid-max",
wantErr: `parsing "asdf": invalid syntax`,
},
{
name: "invalid-period",
wantErr: `parsing "njn": invalid syntax`,
},
{
name: "nonexistent",
want: -1.0,
wantOK: false,
},
{
name: "empty",
wantErr: "unexpected EOF",
},
{
name: "too-few-fields",
wantErr: "invalid format",
},
{
name: "too-many-fields",
wantErr: "invalid format",
},
}

mountPoint := filepath.Join(testDataCGroupsPath, "v2")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
quota, defined, err := (&CGroups2{
mountPoint: mountPoint,
cpuMaxFile: tt.name,
}).CPUQuota()

if len(tt.wantErr) > 0 {
require.Error(t, err, tt.name)
assert.Contains(t, err.Error(), tt.wantErr)
} else {
require.NoError(t, err, tt.name)
assert.Equal(t, tt.want, quota, tt.name)
assert.Equal(t, tt.wantOK, defined, tt.name)
}
})
}
}

func TestCGroupsCPUQuotaV2_OtherErrors(t *testing.T) {
t.Run("no permissions to open", func(t *testing.T) {
t.Parallel()

const name = "foo"

mountPoint := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(mountPoint, name), nil /* write only*/, 0222))

_, _, err := (&CGroups2{mountPoint: mountPoint, cpuMaxFile: name}).CPUQuota()
require.Error(t, err)
assert.Contains(t, err.Error(), "permission denied")
})
}
Empty file.
1 change: 1 addition & 0 deletions internal/cgroups/testdata/cgroups/v2/invalid-max
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
asdf 100000
1 change: 1 addition & 0 deletions internal/cgroups/testdata/cgroups/v2/invalid-period
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
500000 njn
1 change: 1 addition & 0 deletions internal/cgroups/testdata/cgroups/v2/only-max
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
500000
1 change: 1 addition & 0 deletions internal/cgroups/testdata/cgroups/v2/set
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
250000 100000
1 change: 1 addition & 0 deletions internal/cgroups/testdata/cgroups/v2/too-few-fields
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions internal/cgroups/testdata/cgroups/v2/too-many-fields
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
250000 100000 100
1 change: 1 addition & 0 deletions internal/cgroups/testdata/cgroups/v2/unset
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
max 100000
8 changes: 8 additions & 0 deletions internal/cgroups/testdata/proc/v2/mountinfo
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
1 0 8:1 / / rw,noatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro,data=reordered
2 1 0:1 / /dev rw,relatime shared:2 - devtmpfs udev rw,size=10240k,nr_inodes=16487629,mode=755
3 1 0:2 / /proc rw,nosuid,nodev,noexec,relatime shared:3 - proc proc rw
4 1 0:3 / /sys rw,nosuid,nodev,noexec,relatime shared:4 - sysfs sysfs rw
5 4 0:4 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:5 - tmpfs tmpfs ro,mode=755
6 5 0:5 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:6 - cgroup cgroup rw,cpuset
7 5 0:6 /docker /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup rw,cpu,cpuacct
8 5 0:7 /docker /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:8 - cgroup cgroup rw,memory
15 changes: 15 additions & 0 deletions internal/cgroups/testdata/proc/v2/mountinfo-v1-v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
33 24 0:28 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755,inode64
34 33 0:29 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:10 - cgroup2 cgroup2 rw,nsdelegate
35 33 0:30 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:11 - cgroup cgroup rw,xattr,name=systemd
39 33 0:34 / /sys/fs/cgroup/misc rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,misc
40 33 0:35 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,net_cls,net_prio
41 33 0:36 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,rdma
42 33 0:37 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,memory
43 33 0:38 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,blkio
44 33 0:39 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,cpu,cpuacct
45 33 0:40 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,pids
46 33 0:41 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,hugetlb
47 33 0:42 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,freezer
48 33 0:43 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,perf_event
49 33 0:44 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices
50 33 0:45 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,cpuset
1 change: 1 addition & 0 deletions internal/cgroups/testdata/proc/v2/mountinfo-v2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
34 33 0:29 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:10 - cgroup2 cgroup rw,nsdelegate
Loading

0 comments on commit ad6f05b

Please sign in to comment.