Ở phần 31 tôi đo capabilities và seccomp — hai cổng lọc bớt sức mạnh của container trong khi daemon vẫn chạy bằng root. Phần này đi xa hơn: bỏ luôn cái root đó. Rootless Docker chạy daemon bằng một user thường, dùng user namespace để "root trong container" ánh xạ ra một uid không có quyền gì trên host. Nghe an toàn hơn hẳn — nhưng an toàn hơn thường đi kèm đánh đổi, và tôi muốn đo đánh đổi đó là gì thay vì đoán.
Cách dựng phép so sánh
Trên một máy chủ Linux thật, cài rootless Docker gồm ba bước: gói uidmap (cho newuidmap / newgidmap, bắt buộc để user thường tự ánh xạ subuid/subgid), gói docker-ce-rootless-extras, rồi chạy dockerd-rootless-setuptool.sh install bằng đúng user sẽ chạy daemon — không phải root. Nhưng cài trên hai máy vật lý khác nhau thì mọi con số đo được lẫn lộn giữa "khác vì rootless" và "khác vì phần cứng, kernel, tải nền". Tôi dùng cách khác: dựng cả hai daemon bằng Docker-in-Docker, docker:27-dind (rootful) và docker:27-dind-rootless (rootless), trên cùng một host, cùng thời điểm:
docker run -d --privileged --name dind-rootful -e DOCKER_TLS_CERTDIR= \
-p 12375:2375 docker:27-dind --host=tcp://0.0.0.0:2375
docker run -d --privileged --name dind-rootless -e DOCKER_TLS_CERTDIR= \
-p 12376:2375 docker:27-dind-rootless --host=tcp://0.0.0.0:2375
--privileged ở đây là cho container dind bên ngoài — bên trong nó, dockerd-rootless vẫn tự hạ quyền xuống user thường của riêng nó, ps aux bên trong xác nhận rõ:
$ docker exec -u 0 dind-rootless ps aux | grep -E "rootlesskit|vpnkit"
1 rootless rootlesskit --net=vpnkit --port-driver=builtin ... -- dockerd ...
60 rootless vpnkit --ethernet ... --host-ip 0.0.0.0
Cột đầu là user — rootless, không phải root. Đây chính là điểm khác biệt cốt lõi mà mọi số đo dưới đây xoay quanh.
Cgroup không được delegate — giới hạn tài nguyên im lặng biến mất
docker info của hai daemon lệch nhau ngay từ dòng cgroup:
rootful : Cgroup Driver: cgroupfs
rootless: Cgroup Driver: none
WARNING: Running in rootless-mode without cgroups.
Systemd is required to enable cgroups in rootless-mode.
Không có systemd quản lý phiên đăng nhập thì không có ai delegate cgroup cho user thường, và dockerd-rootless không tự tạo cgroup được. Tôi cấp --memory=64m cho một container trên cả hai daemon rồi đọc thẳng file cgroup:
$ docker -H tcp://localhost:12375 run --rm --memory=64m alpine:3.20 cat /sys/fs/cgroup/memory.max
67108864
$ docker -H tcp://localhost:12376 run --rm --memory=64m alpine:3.20 cat /sys/fs/cgroup/memory.max
max
67108864 đúng bằng 64MB — rootful ghi giới hạn thật vào cgroup. Rootless trả về max, tức là không có giới hạn nào cả, dù lệnh chạy y hệt và không báo lỗi. Để chắc đây không chỉ là khác biệt hiển thị, tôi cho container cấp phát thật 200MB, vượt xa mức 64MB đã khai:
$ docker -H tcp://localhost:12375 run --rm --memory=64m alpine:3.20 \
sh -c 'python3 -c "a=bytearray(200*1024*1024)"'; echo "exit=$?"
exit=137
$ docker -H tcp://localhost:12376 run --rm --memory=64m alpine:3.20 \
sh -c 'python3 -c "a=bytearray(200*1024*1024)"'; echo "exit=$?"
exit=0
Rootful bị OOM-kill (exit=137, đúng như cgroup báo trước). Rootless cấp phát trót lọt, exit=0 — container chạy như thể cờ --memory chưa từng được gõ. Đây không phải lỗi hiếm gặp, mà là hành vi mặc định của mọi bản cài rootless không có systemd cgroup delegation phía sau, và nó im lặng đến mức chỉ phát hiện được bằng cách cố tình vượt giới hạn rồi kiểm mã thoát — đọc docker inspect hay tài liệu cấu hình không cho biết điều này. Muốn có --memory/--cpus thật trên rootless, máy chủ phải chạy systemd, user phải có systemd --user hoạt động (loginctl enable-linger), và daemon phải khởi động qua unit systemd để nhận delegation — image dind dùng ở đây không có systemd nên đại diện đúng cho trường hợp "rootless không cgroup" mà nhiều bản cài đơn giản (script cài nhanh, container tự build) rơi vào.
Khởi động container: gần như không khác biệt
Nếu tưởng tượng rootless "chậm hơn ở mọi khâu" thì phép đo này không xác nhận điều đó. 30 lần chạy docker run --rm alpine:3.20 true liên tiếp trên mỗi daemon:
rootful : total=4.060s avg=135.3ms / lần
rootless: total=3.871s avg=129.0ms / lần
Lệch nhau trong biên độ nhiễu đo, không có xu hướng rõ ràng nghiêng về bên nào. Cả hai đều dùng storage driver overlay2 — không phải fuse-overlayfs như nhiều tài liệu cũ vẫn cảnh báo, vì kernel LinuxKit ở đây là 7.0.12, vượt xa ngưỡng 5.13 mà Docker yêu cầu để bật overlayfs không đặc quyền cho rootless. Trên kernel cũ hơn 5.13, rootless sẽ phải rơi về fuse-overlayfs, và khi đó khoảng cách khởi động chắc chắn khác — con số ở đây chỉ đúng với kernel đủ mới.
Build image: rootless chậm hơn nhất quán, khoảng 20%
Build cùng một Dockerfile (apk add curl bash git python3 py3-pip openssl jq, ghi thêm layer 64MB) hai lần trên mỗi daemon, --no-cache để loại cache layer:
rootful lần 1: 38.343s rootless lần 1: 40.595s
rootful lần 2: 29.536s rootless lần 2: 40.896s
Rootful dao động khá mạnh giữa hai lần (29.5s–38.3s, nhiều khả năng do biến thiên tốc độ tải gói apk qua mạng), nhưng rootless lại ổn định lạ thường ở quanh 40,6–40,9s cả hai lần — và chưa lần nào nhanh bằng lần nhanh nhất của rootful. Trung bình rootful ≈ 33,9s, rootless ≈ 40,7s, tức chậm hơn khoảng 20%. Tôi không có bằng chứng để chỉ đích danh nguyên nhân (vpnkit thêm một tầng mạng userspace cho mọi lượt tải gói khi build là nghi phạm hợp lý nhất, nhưng đo build ở đây không tách được phần mạng khỏi phần ghi layer) — nói thẳng là chưa giải thích được thay vì đoán.
Thông lượng qua cổng publish: kiến trúc khác hẳn, số đo lại gần giống nhau
Đây là chỗ bất ngờ nhất. Về kiến trúc, hai daemon publish cổng theo hai cách hoàn toàn khác nhau — ps aux bên trong mỗi dind xác nhận:
rootful : docker-proxy (root) + iptables DNAT cho IPv4 (kernel, gần như zero-copy)
rootless: rootlesskit-docker-proxy (user rootless) qua vpnkit — toàn bộ traffic là userspace,
kể cả IPv4
Rootful có đường tắt kernel (DNAT) cho IPv4, rootless thì mọi gói tin đều phải đi qua ngăn xếp mạng giả lập của vpnkit. Vậy mà tải file 512MB qua cổng đã publish, đo bằng curl -w "%{speed_download}", 5 lần mỗi bên:
rootful : 3103, 3236, 3283, 3165, 3141 MB/s → trung bình ≈ 3186 MB/s
rootless: 1911, 3152, 3175, 3164, 3162 MB/s → trung bình 4 lần sau ≈ 3163 MB/s
Trừ lần đầu của rootless (1911 MB/s, có thể là chi phí thiết lập kết nối lần đầu qua vpnkit — tôi đoán, không chắc), bốn lần còn lại gần như trùng khít với rootful. Đây là benchmark qua loopback trên cùng một host, không phải qua mạng thật có độ trễ và mất gói — kiến trúc userspace của rootless nhiều khả năng lộ rõ hơn khi CPU là nút thắt (nhiều kết nối đồng thời, packet nhỏ liên tục) chứ không phải khi tải một file lớn liên tục như ở đây. Đừng suy ra "rootless mạng nhanh ngang rootful" từ mỗi phép đo này.
Khi nào dùng, khi nào không
Dùng rootless khi mối lo chính là container thoát ra ngoài chiếm quyền root trên host — CI runner dùng chung, môi trường nhiều người thuê, hoặc máy mà bạn không tin tưởng mọi image sẽ chạy trên đó. User namespace khiến "chiếm được root trong container" không còn nghĩa là chiếm được gì trên host.
Đừng dùng rootless nếu bạn đang dựa vào --memory/--cpus để cách ly tải giữa các container trên cùng máy, trừ khi bạn đã bật systemd cgroup delegation và tự kiểm chứng lại, không tin theo mặc định — số đo ở trên cho thấy mặc định là không chặn gì cả, không có cảnh báo nào tại thời điểm docker run.
Thử ba mươi giây
Xem daemon Docker của bạn (hoặc một bản dựng thử) có thật sự enforce --memory hay không, không cần đọc tài liệu:
docker run --rm --memory=64m alpine:3.20 cat /sys/fs/cgroup/memory.max
Ra một con số byte cụ thể là giới hạn có tác dụng. Ra max là container của bạn đang chạy không giới hạn bộ nhớ dù cờ --memory đã được truyền vào.