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.
- Docker Deep Dive – Teil Eins: Cgroups - 19. Februar 2021