Set CPU number to CPU quota

When application is not cgroups-aware, it can spawn excessive threads
which often defaults to CPU number.
Introduce a opt-in flag that will set CPU number accordingly to CPU
quota (if available).

Fixes #1391
This commit is contained in:
Aleksandr Razumov
2019-12-15 21:12:43 +03:00
parent 6b42453039
commit 8782f0e287
4 changed files with 45 additions and 0 deletions
+9
View File
@@ -250,6 +250,12 @@ type Config struct {
// multiple tests are run in parallel, since there is no way to pass
// parameters to the runtime from docker.
TestOnlyTestNameEnv string
// CPUNumFromQuota sets CPU number count to available CPU quota, using
// least integer value greater than or equal to quota.
//
// E.g. 0.2 CPU quota would result in 1, and 1.9 in 2.
CPUNumFromQuota bool
}
// ToFlags returns a slice of flags that correspond to the given Config.
@@ -282,6 +288,9 @@ func (c *Config) ToFlags() []string {
"--software-gso=" + strconv.FormatBool(c.SoftwareGSO),
"--overlayfs-stale-read=" + strconv.FormatBool(c.OverlayfsStaleRead),
}
if c.CPUNumFromQuota {
f = append(f, "--cpu-num-from-quota")
}
// Only include these if set since it is never to be used by users.
if c.TestOnlyAllowRunAsCurrentUserWithoutChroot {
f = append(f, "--TESTONLY-unsafe-nonroot=true")
+24
View File
@@ -101,6 +101,14 @@ func getValue(path, name string) (string, error) {
return string(out), nil
}
func getInt(path, name string) (int, error) {
s, err := getValue(path, name)
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(s))
}
// fillFromAncestor sets the value of a cgroup file from the first ancestor
// that has content. It does nothing if the file in 'path' has already been set.
func fillFromAncestor(path string) (string, error) {
@@ -323,6 +331,22 @@ func (c *Cgroup) Join() (func(), error) {
return undo, nil
}
func (c *Cgroup) CPUQuota() (float64, error) {
path := c.makePath("cpu")
quota, err := getInt(path, "cpu.cfs_quota_us")
if err != nil {
return -1, err
}
period, err := getInt(path, "cpu.cfs_period_us")
if err != nil {
return -1, err
}
if quota <= 0 || period <= 0 {
return -1, err
}
return float64(quota) / float64(period), nil
}
// NumCPU returns the number of CPUs configured in 'cpuset/cpuset.cpus'.
func (c *Cgroup) NumCPU() (int, error) {
path := c.makePath("cpuset")
+2
View File
@@ -82,6 +82,7 @@ var (
numNetworkChannels = flag.Int("num-network-channels", 1, "number of underlying channels(FDs) to use for network link endpoints.")
rootless = flag.Bool("rootless", false, "it allows the sandbox to be started with a user that is not root. Sandbox and Gofer processes may run with same privileges as current user.")
referenceLeakMode = flag.String("ref-leak-mode", "disabled", "sets reference leak check mode: disabled (default), log-names, log-traces.")
cpuNumFromQuota = flag.Bool("cpu-num-from-quota", false, "set cpu number to cpu quota (least integer greater than quota value)")
// Test flags, not to be used outside tests, ever.
testOnlyAllowRunAsCurrentUserWithoutChroot = flag.Bool("TESTONLY-unsafe-nonroot", false, "TEST ONLY; do not ever use! This skips many security measures that isolate the host from the sandbox.")
@@ -225,6 +226,7 @@ func main() {
AlsoLogToStderr: *alsoLogToStderr,
ReferenceLeakMode: refsLeakMode,
OverlayfsStaleRead: *overlayfsStaleRead,
CPUNumFromQuota: *cpuNumFromQuota,
TestOnlyAllowRunAsCurrentUserWithoutChroot: *testOnlyAllowRunAsCurrentUserWithoutChroot,
TestOnlyTestNameEnv: *testOnlyTestNameEnv,
+10
View File
@@ -18,6 +18,7 @@ package sandbox
import (
"context"
"fmt"
"math"
"os"
"os/exec"
"strconv"
@@ -631,6 +632,15 @@ func (s *Sandbox) createSandboxProcess(conf *boot.Config, args *Args, startSyncF
if err != nil {
return fmt.Errorf("getting cpu count from cgroups: %v", err)
}
if conf.CPUNumFromQuota {
quota, err := s.Cgroup.CPUQuota()
if err != nil {
return fmt.Errorf("getting cpu qouta from cgroups: %v", err)
}
if quota > 0 {
cpuNum = int(math.Ceil(quota))
}
}
cmd.Args = append(cmd.Args, "--cpu-num", strconv.Itoa(cpuNum))
mem, err := s.Cgroup.MemoryLimit()