diff --git a/daemon/daemon_windows.go b/daemon/daemon_windows.go index dd327ad6c7..e283d76f5e 100644 --- a/daemon/daemon_windows.go +++ b/daemon/daemon_windows.go @@ -27,10 +27,13 @@ import ( ) const ( - defaultNetworkSpace = "172.16.0.0/12" - platformSupported = true - windowsMinCPUShares = 1 - windowsMaxCPUShares = 10000 + defaultNetworkSpace = "172.16.0.0/12" + platformSupported = true + windowsMinCPUShares = 1 + windowsMaxCPUShares = 10000 + windowsMinCPUPercent = 1 + windowsMaxCPUPercent = 100 + windowsMinCPUCount = 1 ) func getBlkioWeightDevices(config *containertypes.HostConfig) ([]blkiodev.WeightDevice, error) { @@ -80,6 +83,15 @@ func (daemon *Daemon) adaptContainerSettings(hostConfig *containertypes.HostConf return nil } + numCPU := int64(sysinfo.NumCPU()) + if hostConfig.CPUCount < 0 { + logrus.Warnf("Changing requested CPUCount of %d to minimum allowed of %d", hostConfig.CPUCount, windowsMinCPUCount) + hostConfig.CPUCount = windowsMinCPUCount + } else if hostConfig.CPUCount > numCPU { + logrus.Warnf("Changing requested CPUCount of %d to current number of processors, %d", hostConfig.CPUCount, numCPU) + hostConfig.CPUCount = numCPU + } + if hostConfig.CPUShares < 0 { logrus.Warnf("Changing requested CPUShares of %d to minimum allowed of %d", hostConfig.CPUShares, windowsMinCPUShares) hostConfig.CPUShares = windowsMinCPUShares @@ -88,19 +100,42 @@ func (daemon *Daemon) adaptContainerSettings(hostConfig *containertypes.HostConf hostConfig.CPUShares = windowsMaxCPUShares } + if hostConfig.CPUPercent < 0 { + logrus.Warnf("Changing requested CPUPercent of %d to minimum allowed of %d", hostConfig.CPUPercent, windowsMinCPUPercent) + hostConfig.CPUPercent = windowsMinCPUPercent + } else if hostConfig.CPUPercent > windowsMaxCPUPercent { + logrus.Warnf("Changing requested CPUPercent of %d to maximum allowed of %d", hostConfig.CPUPercent, windowsMaxCPUPercent) + hostConfig.CPUPercent = windowsMaxCPUPercent + } + return nil } -func verifyContainerResources(resources *containertypes.Resources, sysInfo *sysinfo.SysInfo) ([]string, error) { +func verifyContainerResources(resources *containertypes.Resources, isHyperv bool) ([]string, error) { warnings := []string{} - // cpu subsystem checks and adjustments - if resources.CPUPercent < 0 || resources.CPUPercent > 100 { - return warnings, fmt.Errorf("Range of CPU percent is from 1 to 100") - } - - if resources.CPUPercent > 0 && resources.CPUShares > 0 { - return warnings, fmt.Errorf("Conflicting options: CPU Shares and CPU Percent cannot both be set") + if !isHyperv { + // The processor resource controls are mutually exclusive on + // Windows Server Containers, the order of precedence is + // CPUCount first, then CPUShares, and CPUPercent last. + if resources.CPUCount > 0 { + if resources.CPUShares > 0 { + warnings = append(warnings, "Conflicting options: CPU count takes priority over CPU shares on Windows Server Containers. CPU shares discarded") + logrus.Warn("Conflicting options: CPU count takes priority over CPU shares on Windows Server Containers. CPU shares discarded") + resources.CPUShares = 0 + } + if resources.CPUPercent > 0 { + warnings = append(warnings, "Conflicting options: CPU count takes priority over CPU percent on Windows Server Containers. CPU percent discarded") + logrus.Warn("Conflicting options: CPU count takes priority over CPU percent on Windows Server Containers. CPU percent discarded") + resources.CPUPercent = 0 + } + } else if resources.CPUShares > 0 { + if resources.CPUPercent > 0 { + warnings = append(warnings, "Conflicting options: CPU shares takes priority over CPU percent on Windows Server Containers. CPU percent discarded") + logrus.Warn("Conflicting options: CPU shares takes priority over CPU percent on Windows Server Containers. CPU percent discarded") + resources.CPUPercent = 0 + } + } } if resources.NanoCPUs > 0 && resources.CPUPercent > 0 { @@ -154,7 +189,7 @@ func verifyContainerResources(resources *containertypes.Resources, sysInfo *sysi func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *containertypes.HostConfig, config *containertypes.Config, update bool) ([]string, error) { warnings := []string{} - w, err := verifyContainerResources(&hostConfig.Resources, nil) + w, err := verifyContainerResources(&hostConfig.Resources, daemon.runAsHyperVContainer(hostConfig)) warnings = append(warnings, w...) if err != nil { return warnings, err @@ -388,14 +423,14 @@ func setupDaemonRoot(config *Config, rootDir string, rootUID, rootGID int) error } // runasHyperVContainer returns true if we are going to run as a Hyper-V container -func (daemon *Daemon) runAsHyperVContainer(container *container.Container) bool { - if container.HostConfig.Isolation.IsDefault() { +func (daemon *Daemon) runAsHyperVContainer(hostConfig *containertypes.HostConfig) bool { + if hostConfig.Isolation.IsDefault() { // Container is set to use the default, so take the default from the daemon configuration return daemon.defaultIsolation.IsHyperV() } // Container is requesting an isolation mode. Honour it. - return container.HostConfig.Isolation.IsHyperV() + return hostConfig.Isolation.IsHyperV() } @@ -403,7 +438,7 @@ func (daemon *Daemon) runAsHyperVContainer(container *container.Container) bool // container start to call mount. func (daemon *Daemon) conditionalMountOnStart(container *container.Container) error { // We do not mount if a Hyper-V container - if !daemon.runAsHyperVContainer(container) { + if !daemon.runAsHyperVContainer(container.HostConfig) { return daemon.Mount(container) } return nil @@ -413,7 +448,7 @@ func (daemon *Daemon) conditionalMountOnStart(container *container.Container) er // during the cleanup of a container to unmount. func (daemon *Daemon) conditionalUnmountOnCleanup(container *container.Container) error { // We do not unmount if a Hyper-V container - if !daemon.runAsHyperVContainer(container) { + if !daemon.runAsHyperVContainer(container.HostConfig) { return daemon.Unmount(container) } return nil diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go index 602b9bf85a..778bb41c95 100644 --- a/daemon/oci_windows.go +++ b/daemon/oci_windows.go @@ -86,11 +86,13 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { if c.HostConfig.NanoCPUs > 0 { cpuPercent = uint8(c.HostConfig.NanoCPUs * 100 / int64(sysinfo.NumCPU()) / 1e9) } + cpuCount := uint64(c.HostConfig.CPUCount) memoryLimit := uint64(c.HostConfig.Memory) s.Windows.Resources = &specs.WindowsResources{ CPU: &specs.WindowsCPUResources{ Percent: &cpuPercent, Shares: &cpuShares, + Count: &cpuCount, }, Memory: &specs.WindowsMemoryResources{ Limit: &memoryLimit, diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index a8e62d10f9..1e1dd1ee25 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -166,6 +166,7 @@ This section lists each version from latest to oldest. Each listing includes a * `GET /nodes` and `GET /node/(id or name)` now return `Addr` as part of a node's `Status`, which is the address that that node connects to the manager from. * The `HostConfig` field now includes `NanoCPUs` that represents CPU quota in units of 10-9 CPUs. * `GET /info` now returns more structured information about security options. +* The `HostConfig` field now includes `CpuCount` that represents the number of CPUs available for execution by the container. Windows daemon only. ### v1.24 API changes diff --git a/docs/reference/api/docker_remote_api_v1.25.md b/docs/reference/api/docker_remote_api_v1.25.md index a174e1c068..a820b49986 100644 --- a/docs/reference/api/docker_remote_api_v1.25.md +++ b/docs/reference/api/docker_remote_api_v1.25.md @@ -303,6 +303,7 @@ Create a container "MemoryReservation": 0, "KernelMemory": 0, "NanoCPUs": 500000, + "CpuCount": 4, "CpuPercent": 80, "CpuShares": 512, "CpuPeriod": 100000, @@ -427,7 +428,14 @@ Create a container - **MemoryReservation** - Memory soft limit in bytes. - **KernelMemory** - Kernel memory limit in bytes. - **NanoCPUs** - CPU quota in units of 10-9 CPUs. - - **CpuPercent** - An integer value containing the usable percentage of the available CPUs. (Windows daemon only) + - **CpuCount** - An integer value containing the number of usable CPUs. + Windows daemon only. On Windows Server containers, + the processor resource controls are mutually exclusive, the order of precedence + is CPUCount first, then CPUShares, and CPUPercent last. + - **CpuPercent** - An integer value containing the usable percentage of + the available CPUs. Windows daemon only. On Windows Server containers, + the processor resource controls are mutually exclusive, the order of precedence + is CPUCount first, then CPUShares, and CPUPercent last. - **CpuShares** - An integer value containing the container's CPU Shares (ie. the relative weight vs other containers). - **CpuPeriod** - The length of a CPU period in microseconds. @@ -623,6 +631,7 @@ Return low-level information on the container `id` "ContainerIDFile": "", "CpusetCpus": "", "CpusetMems": "", + "CpuCount": 4, "CpuPercent": 80, "CpuShares": 0, "CpuPeriod": 100000, diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index 23c8797686..034b9dd9d6 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -31,6 +31,9 @@ Options: --cap-drop value Drop Linux capabilities (default []) --cgroup-parent string Optional parent cgroup for the container --cidfile string Write the container ID to the file + --cpu-count int The number of CPUs available for execution by the container. + Windows daemon only. On Windows Server containers, this is + approximated as a percentage of total CPU usage. --cpu-percent int CPU percent (Windows only) --cpu-period int Limit CPU CFS (Completely Fair Scheduler) period --cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index 6ce6d29773..fc4a9e5a3a 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -29,7 +29,14 @@ Options: --cap-drop value Drop Linux capabilities (default []) --cgroup-parent string Optional parent cgroup for the container --cidfile string Write the container ID to the file - --cpu-percent int CPU percent (Windows only) + --cpu-count int The number of CPUs available for execution by the container. + Windows daemon only. On Windows Server containers, this is + approximated as a percentage of total CPU usage. + --cpu-percent int Limit percentage of CPU available for execution + by the container. Windows daemon only. + The processor resource controls are mutually + exclusive, the order of precedence is CPUCount + first, then CPUShares, and CPUPercent last. --cpu-period int Limit CPU CFS (Completely Fair Scheduler) period --cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota -c, --cpu-shares int CPU shares (relative weight) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index a91919da7d..370347400b 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -4766,3 +4766,67 @@ func (s *DockerSuite) TestRunMount(c *check.C) { } } } + +func (s *DockerSuite) TestRunWindowsWithCPUCount(c *check.C) { + testRequires(c, DaemonIsWindows) + + out, _ := dockerCmd(c, "run", "--cpu-count=1", "--name", "test", "busybox", "echo", "testing") + c.Assert(strings.TrimSpace(out), checker.Equals, "testing") + + out = inspectField(c, "test", "HostConfig.CPUCount") + c.Assert(out, check.Equals, "1") +} + +func (s *DockerSuite) TestRunWindowsWithCPUShares(c *check.C) { + testRequires(c, DaemonIsWindows) + + out, _ := dockerCmd(c, "run", "--cpu-shares=1000", "--name", "test", "busybox", "echo", "testing") + c.Assert(strings.TrimSpace(out), checker.Equals, "testing") + + out = inspectField(c, "test", "HostConfig.CPUShares") + c.Assert(out, check.Equals, "1000") +} + +func (s *DockerSuite) TestRunWindowsWithCPUPercent(c *check.C) { + testRequires(c, DaemonIsWindows) + + out, _ := dockerCmd(c, "run", "--cpu-percent=80", "--name", "test", "busybox", "echo", "testing") + c.Assert(strings.TrimSpace(out), checker.Equals, "testing") + + out = inspectField(c, "test", "HostConfig.CPUPercent") + c.Assert(out, check.Equals, "80") +} + +func (s *DockerSuite) TestRunProcessIsolationWithCPUCountCPUSharesAndCPUPercent(c *check.C) { + testRequires(c, DaemonIsWindows, IsolationIsProcess) + + out, _ := dockerCmd(c, "run", "--cpu-count=1", "--cpu-shares=1000", "--cpu-percent=80", "--name", "test", "busybox", "echo", "testing") + c.Assert(strings.TrimSpace(out), checker.Contains, "WARNING: Conflicting options: CPU count takes priority over CPU shares on Windows Server Containers. CPU shares discarded") + c.Assert(strings.TrimSpace(out), checker.Contains, "WARNING: Conflicting options: CPU count takes priority over CPU percent on Windows Server Containers. CPU percent discarded") + c.Assert(strings.TrimSpace(out), checker.Contains, "testing") + + out = inspectField(c, "test", "HostConfig.CPUCount") + c.Assert(out, check.Equals, "1") + + out = inspectField(c, "test", "HostConfig.CPUShares") + c.Assert(out, check.Equals, "0") + + out = inspectField(c, "test", "HostConfig.CPUPercent") + c.Assert(out, check.Equals, "0") +} + +func (s *DockerSuite) TestRunHypervIsolationWithCPUCountCPUSharesAndCPUPercent(c *check.C) { + testRequires(c, DaemonIsWindows, IsolationIsHyperv) + + out, _ := dockerCmd(c, "run", "--cpu-count=1", "--cpu-shares=1000", "--cpu-percent=80", "--name", "test", "busybox", "echo", "testing") + c.Assert(strings.TrimSpace(out), checker.Contains, "testing") + + out = inspectField(c, "test", "HostConfig.CPUCount") + c.Assert(out, check.Equals, "1") + + out = inspectField(c, "test", "HostConfig.CPUShares") + c.Assert(out, check.Equals, "1000") + + out = inspectField(c, "test", "HostConfig.CPUPercent") + c.Assert(out, check.Equals, "80") +} diff --git a/integration-cli/requirements.go b/integration-cli/requirements.go index 56312c1682..abddb9fe4f 100644 --- a/integration-cli/requirements.go +++ b/integration-cli/requirements.go @@ -218,6 +218,18 @@ var ( }, "Test requires containers are not pausable.", } + IsolationIsHyperv = testRequirement{ + func() bool { + return daemonPlatform == "windows" && isolation == "hyperv" + }, + "Test requires a Windows daemon running default isolation mode of hyperv.", + } + IsolationIsProcess = testRequirement{ + func() bool { + return daemonPlatform == "windows" && isolation == "process" + }, + "Test requires a Windows daemon running default isolation mode of process.", + } ) // testRequires checks if the environment satisfies the requirements diff --git a/libcontainerd/client_windows.go b/libcontainerd/client_windows.go index eca9b2d9fa..16abec3bca 100644 --- a/libcontainerd/client_windows.go +++ b/libcontainerd/client_windows.go @@ -110,6 +110,9 @@ func (clnt *client) Create(containerID string, checkpoint string, checkpointDir if spec.Windows.Resources != nil { if spec.Windows.Resources.CPU != nil { + if spec.Windows.Resources.CPU.Count != nil { + configuration.ProcessorCount = uint32(*spec.Windows.Resources.CPU.Count) + } if spec.Windows.Resources.CPU.Shares != nil { configuration.ProcessorWeight = uint64(*spec.Windows.Resources.CPU.Shares) } diff --git a/man/docker-create.1.md b/man/docker-create.1.md index ff378397ed..767a7fc307 100644 --- a/man/docker-create.1.md +++ b/man/docker-create.1.md @@ -15,6 +15,8 @@ docker-create - Create a new container [**--cap-drop**[=*[]*]] [**--cgroup-parent**[=*CGROUP-PATH*]] [**--cidfile**[=*CIDFILE*]] +[**--cpu-count**[=*0*]] +[**--cpu-percent**[=*0*]] [**--cpu-period**[=*0*]] [**--cpu-quota**[=*0*]] [**--cpu-rt-period**[=*0*]] @@ -124,6 +126,18 @@ The initial status of the container created with **docker create** is 'created'. **--cidfile**="" Write the container ID to the file +**--cpu-count**=*0* + Limit the number of CPUs available for execution by the container. + + On Windows Server containers, this is approximated as a percentage of total CPU usage. + + On Windows Server containers, the processor resource controls are mutually exclusive, the order of precedence is CPUCount first, then CPUShares, and CPUPercent last. + +**--cpu-percent**=*0* + Limit the percentage of CPU available for execution by a container running on a Windows daemon. + + On Windows Server containers, the processor resource controls are mutually exclusive, the order of precedence is CPUCount first, then CPUShares, and CPUPercent last. + **--cpu-period**=*0* Limit the CPU CFS (Completely Fair Scheduler) period diff --git a/man/docker-run.1.md b/man/docker-run.1.md index 5af64627be..ebede9a64f 100644 --- a/man/docker-run.1.md +++ b/man/docker-run.1.md @@ -15,6 +15,8 @@ docker-run - Run a command in a new container [**--cap-drop**[=*[]*]] [**--cgroup-parent**[=*CGROUP-PATH*]] [**--cidfile**[=*CIDFILE*]] +[**--cpu-count**[=*0*]] +[**--cpu-percent**[=*0*]] [**--cpu-period**[=*0*]] [**--cpu-quota**[=*0*]] [**--cpu-rt-period**[=*0*]] @@ -174,6 +176,18 @@ division of CPU shares: **--cidfile**="" Write the container ID to the file +**--cpu-count**=*0* + Limit the number of CPUs available for execution by the container. + + On Windows Server containers, this is approximated as a percentage of total CPU usage. + + On Windows Server containers, the processor resource controls are mutually exclusive, the order of precedence is CPUCount first, then CPUShares, and CPUPercent last. + +**--cpu-percent**=*0* + Limit the percentage of CPU available for execution by a container running on a Windows daemon. + + On Windows Server containers, the processor resource controls are mutually exclusive, the order of precedence is CPUCount first, then CPUShares, and CPUPercent last. + **--cpu-period**=*0* Limit the CPU CFS (Completely Fair Scheduler) period diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go index 18e43cdfaf..b1771112cc 100644 --- a/runconfig/opts/parse.go +++ b/runconfig/opts/parse.go @@ -73,6 +73,7 @@ type ContainerOptions struct { kernelMemory string user string workingDir string + cpuCount int64 cpuShares int64 cpuPercent int64 cpuPeriod int64 @@ -227,6 +228,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions { flags.StringVar(&copts.containerIDFile, "cidfile", "", "Write the container ID to the file") flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") + flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)") flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)") flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") @@ -529,6 +531,7 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c KernelMemory: kernelMemory, OomKillDisable: &copts.oomKillDisable, NanoCPUs: copts.cpus.Value(), + CPUCount: copts.cpuCount, CPUPercent: copts.cpuPercent, CPUShares: copts.cpuShares, CPUPeriod: copts.cpuPeriod,