Docker Deep Dive – Teil Eins: Cgroups

In dieser Serie von Artikeln wollen wir uns anschauen, wie Docker Container implementiert sind. In diesem ersten Artikel betrachten wir, wie wir über cgroups Ressourcenbeschränkungen für Container realisieren. Wir beschäftigen uns in den nächsten Artikeln damit, wie wir Container über Namespaces und chroot/Union Filesysteme vom Hostsystem isolieren. Nach dem Lesen dieser Artikel hast du ein tiefgreifendes Verständnis darüber, wie du Container über Linux Features implementierst.

Was sind cgroups?

Control groups, typischerweise als cgroups abgekürzt, sind ein Linux Kernel Feature, das es dir erlaubt, dass du Prozesse in hierarchische Gruppen gliederst und somit deren Ressourcenverbrauch beschränken und überwachen kannst. Für jede Ressource gibt es eine seperate cgroup Hierarchie, die von einem Ressource Controller verwaltet wird. Dabei beschränken sich cgroups nicht nur auf CPU und Memory, über die pids cgroup Hierarchy kann zum Beispiel die Anzahl der Prozesse innerhalb der cgroup kontrolliert werden, um sich gegen Angriffe wie fork bombs zu schützen.

Jeder Prozess befindet sich in genau einer cgroup und erbt die cgroup seines parents, wenn er erzeugt wird. Cgroups gibt es in der Version 1 und in der Version 2. Wir schauen uns Version 1 an, weil gegenwärtig nur Fedora Version 2 als Default aktiviert hat und der Support von Docker (bzw. runc) für Version 2 noch nicht komplett ist.

Erzeugen von cgroups

Ein Pseudofilesystem, das sich typischerweise unter /sys/fs/cgroup befindet, stellt Informationen über cgroups bereit. Innerhalb dieses Directories sehen wir verschiedene Subdirectories wie blkio, cpuset, memory etc., die jeweils die Root einer cgroup Hierarchie sind.

/sys/fs/cgroup$ ls
blkio cpu,cpuacct cpuset freezer memory net_cls,net_prio perf_event rdma unified
cpu cpuacct devices hugetlb net_cls net_prio pids systemd

Ein cgroup Directory enthält verschiedene Files, die es ermöglichen Informationen über die cgroup auszulesen und sie zu manipulieren. Um eine neue cgroup zu erzeugen, musst du einen neuen Folder innerhalb einer bestehenden cgroup anlegen. Der Folder wird dann automatisch mit Files, die Informationen über die cgroup enthalten, gefüllt.

/sys/fs/cgroup/cpu$ mkdir scratch
/sys/fs/cgroup/cpu/scratch$ ls
cgroup.clone_children cpu.shares cpuacct.stat cpuacct.usage_percpu_sys notify_on_release
cgroup.procs cpu.stat cpuacct.usage cpuacct.usage_percpu_user tasks
cpu.cfs_period_us cpu.uclamp.max cpuacct.usage_all cpuacct.usage_sys
cpu.cfs_quota_us cpu.uclamp.min cpuacct.usage_percpu cpuacct.usage_user

Verwendung in Docker

Docker legt in den relevanten cgroup Hierarchien einen eigenen Folder an, genauso wie wir es gerade gemacht haben.

/sys/fs/cgroup/cpu$ ls
cgroup.clone_children  cpu.shares         cpuacct.usage_percpu       docker                   user.slice
cgroup.procs           cpu.stat           cpuacct.usage_percpu_sys   notify_on_release
cgroup.sane_behavior   cpuacct.stat       cpuacct.usage_percpu_user  release_agent
cpu.cfs_period_us      cpuacct.usage      cpuacct.usage_sys          system.slice
cpu.cfs_quota_us       cpuacct.usage_all  cpuacct.usage_user         tasks

Wenn Docker einen Container startet, legt Docker für diesen Container einen Folder im docker Folder mit der ID des Containers an. Selbst wenn du über docker run –name einen Namen für den Container vergibst, wird immer die ID verwendet.

/$ docker run -d --cpus 0.5 --memory 200m nginx
8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
/$ ls /sys/fs/cgroup/cpu/docker
cgroup.clone_children  cpu.uclamp.min             cpuacct.usage_sys
cgroup.procs           cpuacct.stat               cpuacct.usage_user
cpu.cfs_period_us      cpuacct.usage              8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
cpu.cfs_quota_us       cpuacct.usage_all          notify_on_release
cpu.shares             cpuacct.usage_percpu       tasks
cpu.stat               cpuacct.usage_percpu_sys
cpu.uclamp.max         cpuacct.usage_percpu_user

Unsere Ressourcenbeschränkungen für CPU können wir über cpu.fs_quota_us und cpu.cfs_period_us einsehen und auch ändern.

/sys/fs/cgroup/cpu/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e$ ls
cgroup.clone_children cpu.shares cpuacct.stat cpuacct.usage_percpu_sys notify_on_release
cgroup.procs cpu.stat cpuacct.usage cpuacct.usage_percpu_user tasks
cpu.cfs_period_us cpu.uclamp.max cpuacct.usage_all cpuacct.usage_sys
cpu.cfs_quota_us cpu.uclamp.min cpuacct.usage_percpu cpuacct.usage_user
cat cpu.cfs_quota_us
50000
cat cpu.cfs_period_us
100000

Dies bedeutet, dass diese cgroup für jede 100 Millisekunden Periode bis zu 50 Millisekunden an CPU benutzen darf, was genau dem halben Core entspricht, den wir spezifiziert haben.
Memory Beschränkungen funktionieren auf ähnliche Weise.

/sys/fs/cgroup/memory/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e$ ls
cgroup.clone_children           memory.kmem.tcp.failcnt             memory.oom_control
cgroup.event_control            memory.kmem.tcp.limit_in_bytes      memory.pressure_level
cgroup.procs                    memory.kmem.tcp.max_usage_in_bytes  memory.soft_limit_in_bytes
memory.failcnt                  memory.kmem.tcp.usage_in_bytes      memory.stat
memory.force_empty              memory.kmem.usage_in_bytes          memory.swappiness
memory.kmem.failcnt             memory.limit_in_bytes               memory.usage_in_bytes
memory.kmem.limit_in_bytes      memory.max_usage_in_bytes           memory.use_hierarchy
memory.kmem.max_usage_in_bytes  memory.move_charge_at_immigrate     notify_on_release
memory.kmem.slabinfo            memory.numa_stat                    tasks
cat memory.limit_in_bytes
20971520000 # 200 Mebibytes

Docker delegiert die Erzeugung des Containers an runc. Die Implementierung für das Setzen von CPU- und Memorybeschränkungen kannst du hier und hier finden.
Die Zuweisung eines Prozesses zu einer cgroup erfolgt über das cgroups.procs File. Das überprüfst du, indem du die PID des Containers abfragst.

/$ docker inspect -f {{.State.Pid}} 8c0bb4dbb13766ee
18931
cat /sys/fs/cgroup/cpu/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e/cgroup.procs
18931

Willst du wissen, zu welchen cgroups ein Prozess gehört, kannst du in /proc/[pid]/cgroup nachschauen.

/$ cat /proc/18931/cgroup
12:freezer:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
11:pids:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
10:hugetlb:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
9:rdma:/
8:blkio:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
7:devices:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
6:cpu,cpuacct:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
5:memory:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
4:cpuset:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
3:perf_event:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
2:net_cls,net_prio:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
1:name=systemd:/docker/8c0bb4dbb13766ee96574c8645cf5f1e4726ae011c0bfeccac916e648040827e
0::/system.slice/containerd.service

Ausblick

In diesem Artikel haben wir uns damit beschäftigt, wie wir den Ressourcenverbrauch eines Containers beschränken können. Cgroups sind ein probates Mittel, um das Hostsystem vor Prozessen zu schützen, die exzessiv Ressourcen konsumieren. Einen Container, so wie wir es von Docker gewohnt sind, haben wir jedoch noch nicht, denn Prozesse in einer cgroup können weiterhin andere Prozesse auf dem System sehen, haben Zugriff auf das Filesystem des Hosts usw.

In den nächsten Artikeln beschäftigen wir uns daher damit, wie wir über Namespaces und chroot einen Prozess isolieren können. Am Ende können wir einen Container ohne Docker einfach über Linux Features und Shell Commands erstellen.

Thomas Schubart
Letzte Artikel von Thomas Schubart (Alle anzeigen)