Skip to content

Commit

Permalink
auto: add automatic runtime configuration
Browse files Browse the repository at this point in the history
This change adds an init hook that adjusts GOMAXPROCS, if not set in the
environment already, according to the runtime environment. On Linux,
this means examining CPU quotas.

Doing this will help control latencies by avoiding repeatedly blowing
the CPU budget in containers when run on a very big machine.

Signed-off-by: Hank Donnay <hdonnay@redhat.com>
  • Loading branch information
hdonnay committed Dec 6, 2021
1 parent 136f8e6 commit 0016375
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/clair/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/quay/clair/v4/health"
"github.com/quay/clair/v4/httptransport"
"github.com/quay/clair/v4/initialize"
"github.com/quay/clair/v4/initialize/auto"
"github.com/quay/clair/v4/introspection"
)

Expand Down Expand Up @@ -80,6 +81,7 @@ func main() {
zlog.Info(ctx).
AnErr("lint", &w).Send()
}
auto.PrintLogs(ctx)

// Some machinery for starting and stopping server goroutines:
down := &Shutdown{}
Expand Down
24 changes: 24 additions & 0 deletions initialize/auto/auto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Package auto does automatic detection and runtime configuration for certain
// environments.
//
// All top-level functions are not safe to call concurrently.
package auto

import (
"context"
)

var msgs = []func(context.Context){}

func init() {
CPU()
}

// PrintLogs uses zlog to report any messages queued up from the runs of
// functions since the last call to PrintLogs.
func PrintLogs(ctx context.Context) {
for _, f := range msgs {
f(ctx)
}
msgs = msgs[:0]
}
13 changes: 13 additions & 0 deletions initialize/auto/auto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package auto

import (
"os"
"testing"
)

func TestMain(m *testing.M) {
// Reset the logging slice, as the init function will have triggered and
// written things into it.
msgs = msgs[:0]
os.Exit(m.Run())
}
7 changes: 7 additions & 0 deletions initialize/auto/cpu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !linux
// +build !linux

package auto

// CPU is a no-op on this platform.
func CPU() {}
131 changes: 131 additions & 0 deletions initialize/auto/cpu_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package auto

import (
"bufio"
"bytes"
"context"
"io/fs"
"os"
"path"
"runtime"
"strconv"

"github.com/quay/zlog"
)

// CPU guesses a good number for GOMAXPROCS based on information gleaned from
// the current process's cgroup.
func CPU() {
if os.Getenv("GOMAXPROCS") != "" {
msgs = append(msgs, func(ctx context.Context) {
zlog.Info(ctx).Msg("GOMAXPROCS set in the environment, skipping auto detection")
})
return
}
root := os.DirFS("/")
gmp, err := cgLookup(root)
if err != nil {
msgs = append(msgs, func(ctx context.Context) {
zlog.Error(ctx).
Err(err).
Msg("unable to guess GOMAXPROCS value")
})
return
}
prev := runtime.GOMAXPROCS(gmp)
msgs = append(msgs, func(ctx context.Context) {
zlog.Info(ctx).
Int("cur", gmp).
Int("prev", prev).
Msg("set GOMAXPROCS value")
})
}

func cgLookup(r fs.FS) (int, error) {
var gmp int
b, err := fs.ReadFile(r, "proc/self/cgroup")
if err != nil {
return gmp, err
}
var q, p uint64 = 0, 1
s := bufio.NewScanner(bytes.NewReader(b))
s.Split(bufio.ScanLines)
for s.Scan() {
sl := bytes.SplitN(s.Bytes(), []byte(":"), 3)
hid, ctls, pb := sl[0], sl[1], sl[2]
if bytes.Equal(hid, []byte("0")) && len(ctls) == 0 { // If cgroupsv2:
msgs = append(msgs, func(ctx context.Context) {
zlog.Debug(ctx).Msg("found cgroups v2")
})
n := path.Join("sys/fs/cgroup", string(pb), "cpu.max")
b, err := fs.ReadFile(r, n)
if err != nil {
return gmp, err
}
l := bytes.Fields(b)
qt, per := string(l[0]), string(l[1])
if qt == "max" {
// No quota, so bail.
msgs = append(msgs, func(ctx context.Context) {
zlog.Info(ctx).Msg("no CPU quota set, using default")
})
return gmp, nil
}
q, err = strconv.ParseUint(qt, 10, 64)
if err != nil {
return gmp, err
}
p, err = strconv.ParseUint(per, 10, 64)
if err != nil {
return gmp, err
}
break
}
// If here, we're doing cgroups v1.
isCPU := false
for _, b := range bytes.Split(ctls, []byte(",")) {
if bytes.Equal(b, []byte("cpu")) {
isCPU = true
break
}
}
if !isCPU {
// This line is not the cpu group.
continue
}
msgs = append(msgs, func(ctx context.Context) {
zlog.Debug(ctx).Msg("found cgroups v1 and cpu controller")
})
prefix := path.Join("sys/fs/cgroup", string(ctls), string(pb))
b, err = fs.ReadFile(r, path.Join(prefix, "cpu.cfs_quota_us"))
if err != nil {
return gmp, err
}
qi, err := strconv.ParseInt(string(bytes.TrimSpace(b)), 10, 64)
if err != nil {
return gmp, err
}
if qi == -1 {
// No quota, so bail.
msgs = append(msgs, func(ctx context.Context) {
zlog.Info(ctx).Msg("no CPU quota set, using default")
})
return gmp, nil
}
q = uint64(qi)
b, err = fs.ReadFile(r, path.Join(prefix, "cpu.cfs_period_us"))
if err != nil {
return gmp, err
}
p, err = strconv.ParseUint(string(bytes.TrimSpace(b)), 10, 64)
if err != nil {
return gmp, err
}
break
}
if err := s.Err(); err != nil {
return gmp, err
}
gmp = int(q / p)
return gmp, nil
}
113 changes: 113 additions & 0 deletions initialize/auto/cpu_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//go:build linux
// +build linux

package auto

import (
"context"
"testing"
"testing/fstest"

"github.com/quay/zlog"
)

type cgTestcase struct {
In fstest.MapFS
Err error
Name string
Want int
}

func (tc cgTestcase) Run(ctx context.Context, t *testing.T) {
t.Run(tc.Name, func(t *testing.T) {
ctx := zlog.Test(ctx, t)
gmp, err := cgLookup(tc.In)
if err != tc.Err {
t.Error(err)
}
if got, want := gmp, tc.Want; tc.Err == nil && got != want {
t.Errorf("got: %v, want: %v", got, want)
}
PrintLogs(ctx)
})
}

func TestCPUDetection(t *testing.T) {
ctx := zlog.Test(context.Background(), t)
t.Run("V1", func(t *testing.T) {
const cgmap = `11:pids:/user.slice/user-1000.slice/session-4.scope
10:cpuset:/
9:blkio:/user.slice
8:hugetlb:/
7:perf_event:/
6:devices:/user.slice
5:net_cls,net_prio:/
4:cpu,cpuacct:/user.slice
3:freezer:/
2:memory:/user.slice/user-1000.slice/session-4.scope
1:name=systemd:/user.slice/user-1000.slice/session-4.scope
0::/user.slice/user-1000.slice/session-4.scope
`
tt := []cgTestcase{
{
Name: "NoLimit",
In: fstest.MapFS{
"proc/self/cgroup": &fstest.MapFile{Data: []byte(cgmap)},
"sys/fs/cgroup/cpu,cpuacct/user.slice/cpu.cfs_quota_us": &fstest.MapFile{
Data: []byte("-1\n"),
},
},
Want: 0,
},
{
Name: "Limit1",
In: fstest.MapFS{
"proc/self/cgroup": &fstest.MapFile{Data: []byte(cgmap)},
"sys/fs/cgroup/cpu,cpuacct/user.slice/cpu.cfs_quota_us": &fstest.MapFile{
Data: []byte("100000\n"),
},
"sys/fs/cgroup/cpu,cpuacct/user.slice/cpu.cfs_period_us": &fstest.MapFile{
Data: []byte("100000\n"),
},
},
Want: 1,
},
}
ctx := zlog.Test(ctx, t)
for _, tc := range tt {
tc.Run(ctx, t)
}
})
t.Run("V2", func(t *testing.T) {
tt := []cgTestcase{
{
Name: "NoLimit",
In: fstest.MapFS{
"proc/self/cgroup": &fstest.MapFile{
Data: []byte("0::/\n"),
},
"sys/fs/cgroup/cpu.max": &fstest.MapFile{
Data: []byte("max 100000\n"),
},
},
Want: 0,
},
{
Name: "Limit4",
In: fstest.MapFS{
"proc/self/cgroup": &fstest.MapFile{
Data: []byte("0::/\n"),
},
"sys/fs/cgroup/cpu.max": &fstest.MapFile{
Data: []byte("400000 100000\n"),
},
},
Want: 4,
},
}
ctx := zlog.Test(ctx, t)
for _, tc := range tt {
tc.Run(ctx, t)
}
})
}

0 comments on commit 0016375

Please sign in to comment.