diff --git a/api/types/types.go b/api/types/types.go index 2a641fbaa3..01b5d38f1f 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -132,6 +132,7 @@ type Info struct { DriverStatus [][2]string MemoryLimit bool SwapLimit bool + CpuCfsQuota bool IPv4Forwarding bool Debug bool NFd int diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index f669352119..7f87e50f5d 100755 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -770,6 +770,7 @@ _docker_run() { --cidfile --cpuset --cpu-shares -c + --cpu-quota --device --dns --dns-search diff --git a/daemon/container.go b/daemon/container.go index 5c90f1406d..9dc0696ea7 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -356,6 +356,7 @@ func populateCommand(c *Container, env []string) error { CpuShares: c.hostConfig.CpuShares, CpusetCpus: c.hostConfig.CpusetCpus, CpusetMems: c.hostConfig.CpusetMems, + CpuQuota: c.hostConfig.CpuQuota, Rlimits: rlimits, } diff --git a/daemon/daemon.go b/daemon/daemon.go index 621577286f..d08d22cfdf 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1250,6 +1250,10 @@ func (daemon *Daemon) verifyHostConfig(hostConfig *runconfig.HostConfig) ([]stri if hostConfig.Memory == 0 && hostConfig.MemorySwap > 0 { return warnings, fmt.Errorf("You should always set the Memory limit when using Memoryswap limit, see usage.") } + if hostConfig.CpuQuota > 0 && !daemon.SystemConfig().CpuCfsQuota { + warnings = append(warnings, "Your kernel does not support CPU cfs quota. Quota discarded.") + hostConfig.CpuQuota = 0 + } return warnings, nil } diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index fc3b5caba4..ce196df201 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -111,6 +111,7 @@ type Resources struct { CpuShares int64 `json:"cpu_shares"` CpusetCpus string `json:"cpuset_cpus"` CpusetMems string `json:"cpuset_mems"` + CpuQuota int64 `json:"cpu_quota"` Rlimits []*ulimit.Rlimit `json:"rlimits"` } @@ -206,6 +207,7 @@ func SetupCgroups(container *configs.Config, c *Command) error { container.Cgroups.MemorySwap = c.Resources.MemorySwap container.Cgroups.CpusetCpus = c.Resources.CpusetCpus container.Cgroups.CpusetMems = c.Resources.CpusetMems + container.Cgroups.CpuQuota = c.Resources.CpuQuota } return nil diff --git a/daemon/execdriver/lxc/lxc_template.go b/daemon/execdriver/lxc/lxc_template.go index ece924d38f..b3be7f8c51 100644 --- a/daemon/execdriver/lxc/lxc_template.go +++ b/daemon/execdriver/lxc/lxc_template.go @@ -113,6 +113,9 @@ lxc.cgroup.cpuset.cpus = {{.Resources.CpusetCpus}} {{if .Resources.CpusetMems}} lxc.cgroup.cpuset.mems = {{.Resources.CpusetMems}} {{end}} +{{if .Resources.CpuQuota}} +lxc.cgroup.cpu.cfs_quota_us = {{.Resources.CpuQuota}} +{{end}} {{end}} {{if .LxcConfig}} diff --git a/daemon/info.go b/daemon/info.go index fa019fe08f..270abda598 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -60,6 +60,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) { DriverStatus: daemon.GraphDriver().Status(), MemoryLimit: daemon.SystemConfig().MemoryLimit, SwapLimit: daemon.SystemConfig().SwapLimit, + CpuCfsQuota: daemon.SystemConfig().CpuCfsQuota, IPv4Forwarding: !daemon.SystemConfig().IPv4ForwardingDisabled, Debug: os.Getenv("DEBUG") != "", NFd: fileutils.GetTotalUsedFds(), diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index 6ce7fe6bdc..bb9cbdc8fc 100644 --- a/docs/man/docker-create.1.md +++ b/docs/man/docker-create.1.md @@ -14,6 +14,7 @@ docker-create - Create a new container [**--cidfile**[=*CIDFILE*]] [**--cpuset-cpus**[=*CPUSET-CPUS*]] [**--cpuset-mems**[=*CPUSET-MEMS*]] +[**--cpu-quota**[=*0*]] [**--device**[=*[]*]] [**--dns-search**[=*[]*]] [**--dns**[=*[]*]] @@ -82,6 +83,9 @@ IMAGE [COMMAND] [ARG...] then processes in your Docker container will only use memory from the first two memory nodes. +**-cpu-quota**=0 + Limit the CPU CFS (Completely Fair Scheduler) quota + **--device**=[] Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc:rwm) diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 42eeeb349d..2893437bbe 100644 --- a/docs/man/docker-run.1.md +++ b/docs/man/docker-run.1.md @@ -15,6 +15,7 @@ docker-run - Run a command in a new container [**--cpuset-cpus**[=*CPUSET-CPUS*]] [**--cpuset-mems**[=*CPUSET-MEMS*]] [**-d**|**--detach**[=*false*]] +[**--cpu-quota**[=*0*]] [**--device**[=*[]*]] [**--dns-search**[=*[]*]] [**--dns**[=*[]*]] @@ -142,6 +143,13 @@ division of CPU shares: then processes in your Docker container will only use memory from the first two memory nodes. +**--cpu-quota**=0 + Limit the CPU CFS (Completely Fair Scheduler) quota + + Limit the container's CPU usage. By default, containers run with the full +CPU resource. This flag tell the kernel to restrict the container's CPU usage +to the quota you specify. + **-d**, **--detach**=*true*|*false* Detached mode: run the container in the background and print the new container ID. The default is *false*. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 88250f7625..642aa735a9 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -925,6 +925,7 @@ Creates a new container. --cidfile="" Write the container ID to the file --cpuset-cpus="" CPUs in which to allow execution (0-3, 0,1) --cpuset-mems="" Memory nodes (MEMs) in which to allow execution (0-3, 0,1) + --cpu-quota=0 Limit the CPU CFS (Completely Fair Scheduler) quota --device=[] Add a host device to the container --dns=[] Set custom DNS servers --dns-search=[] Set custom DNS search domains @@ -1879,6 +1880,7 @@ To remove an image using its digest: --cidfile="" Write the container ID to the file --cpuset-cpus="" CPUs in which to allow execution (0-3, 0,1) --cpuset-mems="" Memory nodes (MEMs) in which to allow execution (0-3, 0,1) + --cpu-quota=0 Limit the CPU CFS (Completely Fair Scheduler) quota -d, --detach=false Run container in background and print container ID --device=[] Add a host device to the container --dns=[] Set custom DNS servers diff --git a/docs/sources/reference/run.md b/docs/sources/reference/run.md index b5784cba78..10178b382a 100644 --- a/docs/sources/reference/run.md +++ b/docs/sources/reference/run.md @@ -475,6 +475,7 @@ container: -c, --cpu-shares=0: CPU shares (relative weight) --cpuset-cpus="": CPUs in which to allow execution (0-3, 0,1) --cpuset-mems="": Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. + --cpu-quota=0: Limit the CPU CFS (Completely Fair Scheduler) quota ### Memory constraints @@ -615,6 +616,15 @@ memory nodes 1 and 3. This example restricts the processes in the container to only use memory from memory nodes 0, 1 and 2. +### CPU quota constraint + +The `--cpu-quota` flag limits the container's CPU usage. The default 0 value +allows the container to take 100% of a CPU resource (1 CPU). The CFS (Completely Fair +Scheduler) handles resource allocation for executing processes and is default +Linux Scheduler used by the kernel. Set this value to 50000 to limit the container +to 50% of a CPU resource. For multiple CPUs, adjust the `--cpu-quota` as necessary. +For more information, see the [CFS documentation on bandwidth limiting](https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt). + ## Runtime privilege, Linux capabilities, and LXC configuration --cap-add: Add Linux capabilities diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 4088a80dde..98f923fa4c 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -105,6 +105,36 @@ func TestRunEchoStdoutWithCPUAndMemoryLimit(t *testing.T) { logDone("run - echo with CPU and memory limit") } +// "test" should be printed +func TestRunEchoStdoutWitCPUQuota(t *testing.T) { + defer deleteAllContainers() + + runCmd := exec.Command(dockerBinary, "run", "--cpu-quota", "8000", "--name", "test", "busybox", "echo", "test") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + if err != nil { + t.Fatalf("failed to run container: %v, output: %q", err, out) + } + out = strings.TrimSpace(out) + if strings.Contains(out, "Your kernel does not support CPU cfs quota") { + t.Skip("Your kernel does not support CPU cfs quota, skip this test") + } + if out != "test" { + t.Errorf("container should've printed 'test'") + } + + cmd := exec.Command(dockerBinary, "inspect", "-f", "{{.HostConfig.CpuQuota}}", "test") + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to inspect container: %s, %v", out, err) + } + out = strings.TrimSpace(out) + if out != "8000" { + t.Errorf("setting the CPU CFS quota failed") + } + + logDone("run - echo with CPU quota") +} + // "test" should be printed func TestRunEchoNamedContainer(t *testing.T) { defer deleteAllContainers() diff --git a/pkg/sysinfo/sysinfo.go b/pkg/sysinfo/sysinfo.go index 16839bcb4c..d1dcea3bf8 100644 --- a/pkg/sysinfo/sysinfo.go +++ b/pkg/sysinfo/sysinfo.go @@ -13,6 +13,7 @@ import ( type SysInfo struct { MemoryLimit bool SwapLimit bool + CpuCfsQuota bool IPv4ForwardingDisabled bool AppArmor bool } @@ -39,6 +40,19 @@ func New(quiet bool) *SysInfo { } } + if cgroupCpuMountpoint, err := cgroups.FindCgroupMountpoint("cpu"); err != nil { + if !quiet { + logrus.Warnf("WARING: %s\n", err) + } + } else { + _, err1 := ioutil.ReadFile(path.Join(cgroupCpuMountpoint, "cpu.cfs_quota_us")) + logrus.Warnf("%s", cgroupCpuMountpoint) + sysInfo.CpuCfsQuota = err1 == nil + if !sysInfo.CpuCfsQuota && !quiet { + logrus.Warnf("WARING: Your kernel does not support cgroup cfs quotas") + } + } + // Check if AppArmor is supported. if _, err := os.Stat("/sys/kernel/security/apparmor"); os.IsNotExist(err) { sysInfo.AppArmor = false diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 9d338d7fb8..171671b6ef 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -167,6 +167,7 @@ type HostConfig struct { CpuShares int64 // CPU shares (relative weight vs. other containers) CpusetCpus string // CpusetCpus 0-2, 0,1 CpusetMems string // CpusetMems 0-2, 0,1 + CpuQuota int64 Privileged bool PortBindings nat.PortMap Links []string diff --git a/runconfig/parse.go b/runconfig/parse.go index 81dbf2d491..2cdb2d331d 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -65,6 +65,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flCpuShares = cmd.Int64([]string{"c", "-cpu-shares"}, 0, "CPU shares (relative weight)") flCpusetCpus = cmd.String([]string{"#-cpuset", "-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") flCpusetMems = cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") + flCpuQuota = cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") flNetMode = cmd.String([]string{"-net"}, "bridge", "Set the Network mode for the container") flMacAddress = cmd.String([]string{"-mac-address"}, "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") flIpcMode = cmd.String([]string{"-ipc"}, "", "IPC namespace to use") @@ -312,6 +313,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe CpuShares: *flCpuShares, CpusetCpus: *flCpusetCpus, CpusetMems: *flCpusetMems, + CpuQuota: *flCpuQuota, Privileged: *flPrivileged, PortBindings: portBindings, Links: flLinks.GetAll(),