LXC vs VM для докера и не только

А теперь разоблачение года

Миф №0: Матрешка вида PVE → LXC → Docker дает потерю производительности и виртуализацию внутри виртуализации

Нет смысла делать контейнер в контейнере в контейнере… и запускать докер внутри LXC т.к. это увеличивает накладные расходы и дублирует функциональность.

Разоблачение: Что LXC, что Docker под linux используют механизм cgroups/cgroups2, который позволяет изолировать процессы на на уровне ядра операционной системы

Для примера возьмем PVE 9 с LXC контейнером на основе alpine и 3 Docker сервисами


Не знаю, зачем я включал там fuse, возможно для overlayfs, но есть nesting, который означает, что используется вложенность

Заходим в контейнер

vaultwarden:~# docker compose ls
NAME                STATUS              CONFIG FILES
komodo              running(1)          /srv/komodo/compose.yaml
vw                  running(2)          /srv/vw/compose.yaml

vaultwarden:~# docker ps
CONTAINER ID   IMAGE                                      COMMAND                  CREATED       STATUS                PORTS                                                                                  NAMES
5e733ff0019c   ghcr.io/moghtech/komodo-periphery:latest   "periphery"              2 weeks ago   Up 6 days             0.0.0.0:8120->8120/tcp, [::]:8120->8120/tcp                                            komodo-periphery-1
488bebf5e6bc   vaultwarden/server:latest-alpine           "/start.sh"              5 weeks ago   Up 6 days (healthy)   0.0.0.0:3012->3012/tcp, [::]:3012->3012/tcp, 0.0.0.0:8884->80/tcp, [::]:8884->80/tcp   vaultwarden
1a41d9d1b566   ghcr.io/openbao/openbao:latest             "docker-entrypoint.s…"   5 weeks ago   Up 6 days             8200/tcp                                                                               vw-vault-agent-1
vaultwarden:~# 

и видим, что там запущено 2 стека и docker контейнера

Теперь переходим на хост Proxmox и вводим команду systemd-cgls -c /lxc/150

root@pve-01:~# systemd-cgls -c /lxc/150
CGroup /lxc/150:
└─ns (#7570)
  ├─   1825 /sbin/init
  ├─   3511 /sbin/getty 38400 console
  ├─   3513 /bin/login -- кщще
  ├─   3514 /sbin/getty 38400 tty2
  ├─3831564 -sh
  ├─openrc.syslog (#8396)
  │ └─3414 /sbin/syslogd -t -n
  ├─openrc.dropbear (#8414)
  │ └─3470 /usr/sbin/dropbear
  ├─docker (#8655)
  │ ├─5e733ff0019c24245e762694f4cdd446da476905e3eb98657b0639892b2693cd (#8691)
  │ │ └─4775 periphery
  │ ├─488bebf5e6bc9acd213be890ee2f0efe7c18ccd96cf658d0b12297f6e791fe82 (#8673)
  │ │ └─4771 /vaultwarden
  │ └─1a41d9d1b566ad4e8ce50a44fe740e944ae23e1ed6c0d479603339222ec4a854 (#8709)
  │   ├─4773 /usr/bin/dumb-init /bin/sh /usr/local/bin/docker-entrypoint.sh agent -config=/etc/vault/agent-config.hcl
  │   └─4980 bao agent -config=/etc/vault/agent-config.hcl
  ├─openrc.docker (#8378)
  │ ├─3351 supervise-daemon docker --start --retry TERM/60/KILL/10 --stdout-logger log_proxy -m /var/log/docker.log --stderr-logger log_proxy -m /var/log/docker.log --respawn-delay 2 --respawn-max 5 --respawn-period 180>
  │ ├─3352 /usr/bin/dockerd
  │ ├─3373 log_proxy -m /var/log/docker.log
  │ ├─3374 log_proxy -m /var/log/docker.log
  │ ├─3647 containerd --config /var/run/docker/containerd/containerd.toml
  │ ├─4659 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 1a41d9d1b566ad4e8ce50a44fe740e944ae23e1ed6c0d479603339222ec4a854 -address /var/run/docker/containerd/containerd.sock
  │ ├─4660 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 5e733ff0019c24245e762694f4cdd446da476905e3eb98657b0639892b2693cd -address /var/run/docker/containerd/containerd.sock
  │ ├─4662 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 488bebf5e6bc9acd213be890ee2f0efe7c18ccd96cf658d0b12297f6e791fe82 -address /var/run/docker/containerd/containerd.sock
  │ ├─4899 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8884 -container-ip 172.18.0.2 -container-port 80 -use-listen-fd
  │ ├─4916 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8884 -container-ip 172.18.0.2 -container-port 80 -use-listen-fd
  │ ├─4922 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 3012 -container-ip 172.18.0.2 -container-port 3012 -use-listen-fd
  │ ├─4927 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 3012 -container-ip 172.18.0.2 -container-port 3012 -use-listen-fd
  │ ├─4956 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8120 -container-ip 172.19.0.2 -container-port 8120 -use-listen-fd
  │ └─4963 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8120 -container-ip 172.19.0.2 -container-port 8120 -use-listen-fd
  ├─openrc.crond (#8432)
  │ └─3504 /usr/sbin/crond -c /etc/crontabs -f
  └─openrc.networking (#7922)
    └─2372 /sbin/udhcpc -b -R -p /var/run/udhcpc.eth0.pid -i eth0 -x hostname:vaultwarden

Что мы тут видим?

  1. PVE создал группу /lxc для контейнеров
  2. В нее положил группу 150 с номером CT (и неймспейс 7570)
  3. Там запущен init в качестве родителя всех процессов (в linux должен быть родительский процесс)
  4. Там же запущен docker через openrc
  5. Docker создал 3 группы для каждого контейнера и внутри запустил по init процессу docker (#8655), тоже имеет свой неймспейс но не обображается утилитой

Если кратко, то мы имеем следующую иерархию

Host Kernel
└── cgroup / namespaces (LXC)
    ├── init, getty, openrc...
    └── cgroup / namespaces (Docker)
        ├── vaultwarden
        ├── periphery
        └── bao agent

Ну и выполним еще одну команду на хосте

root@pve-01:~# ps -p 881,3470,4771 -o pid,ppid,user,cmd
    PID    PPID USER     CMD
    881       1 root     sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
   3470    1825 100000   /usr/sbin/dropbear
   4771    4662 100000   /vaultwarden

Мы видим тут следующее

  1. ssh от рута на хосте (через который я сейчас и зпускаю данную команду)
  2. ssh сервер dropbear внутри LXC контейнера. Он запущен от рута в контейнере, на хосте для безопасности к ID пользователя прибавляется 100000, таким образом, процесс, запущенный от root внутри контейнера на хосте является обычным пользователем и не обладает расширенными правами
  3. vaultvarden внутри docker контейнера, который запущен тоже под рутом, поэтому и рекомендуется указывать user: UID:GID в докере, чтобы ограничить права процесса

Что мы из этого поняли?

LXC → Docker не порождает вложенную контейнеризацию и тем более, виртуализацию, не добавляет накладных расходов, Это только способ упорядочивания процессов и ресурсов, точно также как мы создаем каталоги в файловой системе или пользовуем docker-compose в место docker run

Единственное, что мы тут имеем из накладного это вложенность файловых систем и независимые слои docker образов. Это значит, что docker pull в отдельных контейнерах будет скачивать образы каждый раз, а docker compose pull будет использовать общий набор слоев для всех стеков и условная ubuntu, использованная в качестве базового образа будет скачана 1 раз для всех контейнеров, которые запускаются на внутри текущего LXC контейнера

На уровне ядра операционной системы эти процессы будут работать как обычные процессы, просто им будут доступны различные ресурсы и возможности

Ну и напоследок, чтобы совсем взорвать мозг зуммерам

Если в Docker в качестве entrypoint указать /sbin/init, предварительно сложив нужные файлы внутрь docker образа, то мы получим практическу полноценную “операционную систему” внутри докера, с ssh, и кучей сервисов, т.е. вместо кучи контейнеров можно в одном запустить и postgresql и redis и nginx и crond и свои сервисы, которые будут работать со всем этим.

12 лайков