А теперь разоблачение года
Миф №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
Что мы тут видим?
- PVE создал группу
/lxcдля контейнеров - В нее положил группу
150с номером CT (и неймспейс 7570) - Там запущен
initв качестве родителя всех процессов (в linux должен быть родительский процесс) - Там же запущен docker через openrc
- 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
Мы видим тут следующее
- ssh от рута на хосте (через который я сейчас и зпускаю данную команду)
- ssh сервер dropbear внутри LXC контейнера. Он запущен от рута в контейнере, на хосте для безопасности к ID пользователя прибавляется 100000, таким образом, процесс, запущенный от root внутри контейнера на хосте является обычным пользователем и не обладает расширенными правами
- 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 и свои сервисы, которые будут работать со всем этим.
