-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
auto: add automatic runtime configuration
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
Showing
6 changed files
with
290 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} |