Docker: Placing limits on cpu usage in containers

Containers themselves are light, but by default a container has access to all the CPU resources the Docker host kernel scheduler will allow.

Internally Docker uses cgroups to limit CPU resources, and this is exposed as the flag “–cpus” when bringing up a docker container:

sudo docker run -it --cpus=1.0 alpine:latest /bin/sh

This will limit the CPU abilities of this container to the equivalent of a single CPU core on the Docker host system, balanced among the Docker host processors.

Monitoring host CPU with htop

A nice way to watch CPU utilization is using htop on the host system.  This is typically installed already, but if you need to install it.

sudo apt-get install htop -y

Here is a screenshot showing the 4 CPU Docker host system at baseline, with no discernible load.

Go ahead and keep htop running in one console to prepare for the next section.

Running container and stress-ng

stress-ng is a utility that can stress a system in multiple ways, but we will be using it only for CPU here.

Instead of having you install stress-ng on the alpine image every time you run a test, I created an alpine-stressng-cpu Dockerfile in github.  To use this project:

# get project from github
git clone https://github.com/fabianlee/alpine-stressng-cpu.git
cd alpine-stressng-cpu

# make utility is a prereq
sudo apt-get install make -f

# build docker image
make docker-build

# run quick test (50% load on all host CPU for 20 seconds)
make docker-run

We did not place any limits on container CPU, so notice that while the docker-run was executing, htop should show all your host CPU at ~50% utilization.

Limiting container CPU

Now we need to start exploring the “–cpu” flag that will limit a container’s processing power relative to the processing power of a single host CPU.

For example, if a single host CPU can execute 100 instructions/second, then –cpus=0.15 means the container will be capable of 15 instructions/second, and –cpus=0.75 means the container will be capable of 75 instructions/second.

EXAMPLE 1 – 1 CPU at 100% load

sudo docker run -it --cpus=1.0 -e cpuload=100 -e timeout=20 fabianlee/alpine-stressng-cpu:1.0.0

We requested that the equivalent power of 1 host CPU be put at 100% load.  Since we have 4 host CPU, that placed 25% load on each as shown below.

EXAMPLE 2 – 2 CPU at 50% load

sudo docker run -it --cpus=2.0 -e cpuload=50 -e timeout=20 fabianlee/alpine-stressng-cpu:1.0.0

We requested that the equivalent power of 2 host CPU be put at 50% load.  Since we have 4 host CPU, that placed 25% load on each as shown below.

Notice that in both examples so far the host CPU load on all processors has been ~25%.  This is because 1 CPU at 100% matches the host CPU processing power of 2 CPU at 50%.

Pinning to host CPU

In the previous section we had container CPU load spread across all host CPU.  This is the recommended way of sharing processing load.

But if you do need to pin processing to a set of host CPU, that is possible using the flag “–cpuset-cpus”.

The example below requests the equivalent of 2 host CPU at 100% load,  pinned to CPU 0 and 1.

sudo docker run -it --cpus=2.0 --cpuset-cpus=0,1 -e cpuload=100 -e timeout=20 fabianlee/alpine-stressng-cpu:1.0.0

As you can see CPU 0 and 1 are ~100%, while 3 and 4 are almost unused.

 

REFERENCES

docker, runtime constraints on resources

docker, container has access to all host resources

kernel.org, cgroups reference

redhat, cgroups reference

linuxhint, cgroups and cpu.cfs_period_us and cpu.cfs_quota_us

stackoverflow, definitions of cpu.cfs_period_us and cpu.fs_quota_us

stackoverflow, flags cpus and cpuset-cpus

tecmint, describing usage of stress-ng

ubuntu man page for stress-ng

man page stress-ng

github, stress-ng source code

github, ctop utility.  Good for memory/network, doesn’t handle multiple core well

 

NOTES

Looking at cgroup controls for CPU

$ sudo docker run -it --cpus=0.25 alpine-stressng /bin/sh
# cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us
100000
# cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
25000
# cat /sys/fs/cgroup/cpuset/cpuset.cpus
0-3
# exit

You would see the cfs_period_us=100000, while cfs_quota_us=25000.  Which shows the fractional amount of host CPU requested.  If you had used “–cpus=1.0”, both numbers would be 100000.