Hiểu Đơn Giản Về Namespaces, cgroups, và UnionFS trong Docker Roadmap

Chào mừng trở lại với series “Roadmap Docker” của chúng ta! Sau khi đã cùng nhau khám phá Container Là Gì, so sánh Container, Máy ảo (VM) và Bare Metal, tìm hiểu về Docker và tiêu chuẩn OCI, và lướt qua những kiến thức Linux cơ bản như các kỹ năng cốt lõi, Package Managers, Người Dùng, Nhóm và Quyền Hạn, cũng như các lệnh ShellShell Script, đã đến lúc chúng ta lặn sâu hơn vào những công nghệ nền tảng giúp container hoạt động hiệu quả trên Linux.

Docker, về bản chất, không phải là một công nghệ ảo hóa hoàn chỉnh như máy ảo. Thay vào đó, nó tận dụng các tính năng có sẵn trong nhân Linux để tạo ra môi trường “gần như riêng biệt” cho các ứng dụng. Ba trụ cột chính tạo nên sự kỳ diệu này là Namespaces, cgroups (Control Groups), và Union Filesystems (phổ biến nhất là OverlayFS).

Hiểu rõ ba khái niệm này là chìa khóa để thực sự nắm vững cách Docker hoạt động, gỡ lỗi khi cần thiết, và thậm chí tối ưu hóa hiệu suất container của bạn. Đối với các bạn DevOps, đặc biệt là những bạn mới bắt đầu, đây là những kiến thức không thể thiếu.

Namespaces: Tạo Ra Thế Giới Riêng Cho Container

Hãy tưởng tượng bạn đang sống trong một tòa nhà chung cư lớn. Mỗi căn hộ có số phòng riêng, mạng internet riêng, danh sách người thuê riêng. Mặc dù tất cả đều nằm trong cùng một tòa nhà (hệ điều hành Host), nhưng bên trong mỗi căn hộ lại là một không gian được phân chia rõ ràng. Namespaces trong Linux đóng vai trò như “ranh giới” cho các căn hộ này.

Namespaces là một tính năng của nhân Linux cung cấp sự *cô lập* (isolation) cho các tài nguyên hệ thống. Khi một tiến trình được tạo ra trong một namespace mới, nó sẽ chỉ nhìn thấy và tương tác với các tài nguyên trong namespace đó, chứ không phải tài nguyên của hệ thống Host hoặc các namespace khác (trừ khi được cấu hình đặc biệt).

Có nhiều loại namespace khác nhau, mỗi loại chịu trách nhiệm cô lập một khía cạnh cụ thể của hệ thống:

  • PID namespace (Process ID): Cô lập danh sách các tiến trình. Một tiến trình chạy trong namespace PID mới sẽ có cây tiến trình (process tree) riêng biệt, với PID 1 là tiến trình “init” của riêng namespace đó (thường là tiến trình chính của container). Nó không thể nhìn thấy các tiến trình chạy bên ngoài namespace của nó. Lệnh `ps aux` bên trong container sẽ cho kết quả khác so với lệnh tương tự trên Host.
  • Net namespace (Network): Cô lập các tài nguyên mạng như giao diện mạng (network interfaces), bảng định tuyến (routing tables), cổng (ports), bảng tường lửa (firewall rules), v.v. Mỗi container có thể có một stack mạng riêng, với địa chỉ IP riêng. Đây là lý do tại sao các container có thể chạy trên cùng một port (ví dụ: port 80) mà không xung đột trên máy Host.
  • Mount namespace (mnt): Cô lập hệ thống tập tin (filesystem) mount points. Điều này có nghĩa là các hoạt động mount/unmount bên trong một container sẽ không ảnh hưởng đến hệ thống tập tin của Host hoặc các container khác. Mỗi container có hệ thống tập tin gốc (root filesystem) riêng biệt dựa trên image của nó.
  • User namespace (user): Cô lập user và group IDs. Tính năng này cho phép một user không có quyền root trên Host có thể có quyền root bên trong container. Điều này tăng cường bảo mật, vì quyền root bên trong user namespace mới không có quyền root tương ứng trên Host.
  • IPC namespace (Interprocess Communication): Cô lập các tài nguyên IPC như System V IPC (message queues, semaphores, shared memory) và POSIX message queues. Điều này ngăn các tiến trình trong một container truy cập vào các tài nguyên IPC của container khác hoặc Host.
  • UTS namespace (UNIX Timesharing System): Cô lập hostname và NIS domain name. Mỗi container có thể có hostname riêng của nó, độc lập với hostname của máy Host.

Khi bạn chạy lệnh `docker run`, Docker daemon sử dụng lời gọi hệ thống (system call) như `clone()` hoặc `unshare()` với các cờ (flags) tương ứng để tạo ra một tập hợp các namespaces mới cho tiến trình container. Tiến trình ứng dụng chính của container sẽ chạy trong các namespaces này.

Để cảm nhận rõ hơn, bạn có thể thử chạy một container đơn giản và so sánh các thông tin bên trong và bên ngoài:

# Mở terminal 1 (trên máy Host)
ps aux | head -n 5
lsns
ip addr show

# Mở terminal 2 (chạy một container)
docker run -it --rm ubuntu:latest bash

# Bên trong container (trong terminal 2)
ps aux
lsns
ip addr show

# So sánh kết quả giữa terminal 1 và terminal 2

Bạn sẽ thấy danh sách tiến trình, namespaces ID và cấu hình mạng khác nhau hoàn toàn giữa Host và bên trong container. Đây chính là sự cô lập mà Namespaces mang lại.

cgroups (Control Groups): Quản Lý Tài Nguyên Cho Container

Namespaces cung cấp sự cô lập, nhưng bản thân nó không giới hạn lượng tài nguyên mà một tiến trình có thể sử dụng. Một container có thể “ngốn” hết CPU, bộ nhớ, hoặc băng thông mạng, ảnh hưởng tiêu cực đến các container khác hoặc chính máy Host. Đây là lúc cgroups phát huy tác dụng.

cgroups là một tính năng khác của nhân Linux cho phép *quản lý*, *giới hạn* và *kiểm soát* việc sử dụng tài nguyên hệ thống của các nhóm tiến trình. Nó hoạt động như người quản lý tòa nhà, đặt ra các quy định về lượng điện (CPU), nước (Network I/O), diện tích sử dụng (Memory) cho mỗi căn hộ (nhóm tiến trình).

cgroups được tổ chức theo một cây phân cấp, nơi các nhóm con (child cgroups) kế thừa một số thuộc tính từ nhóm cha (parent cgroup). Mỗi nhóm cgroup có một tập hợp các “controllers” (bộ điều khiển) quản lý một loại tài nguyên cụ thể. Các controllers phổ biến bao gồm:

  • CPU controller: Quản lý quyền truy cập CPU, bao gồm giới hạn thời gian CPU (CPU shares, CPU quotas) hoặc giới hạn số lượng core CPU được sử dụng.
  • Memory controller: Giới hạn lượng bộ nhớ (RAM) mà một nhóm tiến trình có thể sử dụng, bao gồm cả bộ nhớ swap.
  • Block I/O controller (blkio): Giới hạn hoặc ưu tiên quyền truy cập vào các thiết bị I/O khối (như ổ cứng).
  • Network controller (net_cls, net_prio): Gắn nhãn cho các gói tin mạng (net_cls) để có thể kiểm soát lưu lượng (traffic shaping) hoặc đặt ưu tiên (net_prio).

Khi bạn chạy Docker container với các tùy chọn giới hạn tài nguyên như `–cpus`, `–memory`, `–blkio-weight`, Docker daemon sẽ tạo một cgroup mới cho container đó và cấu hình các controllers tương ứng trong cây cgroup của Host. Tiến trình chính của container và tất cả tiến trình con của nó sẽ được đặt vào cgroup này.

Bạn có thể khám phá cgroups trên hệ thống Linux của mình, thường nằm dưới `/sys/fs/cgroup/`:

# Trên máy Host Linux
# Cấu trúc có thể khác nhau tùy thuộc vào hệ thống (systemd vs sysvinit)
# systemd sử dụng cgroup v2 thường nằm dưới /sys/fs/cgroup/system.slice/
# cgroup v1 thường có các thư mục con theo từng controller như /sys/fs/cgroup/cpu, /sys/fs/cgroup/memory, ...

# Liệt kê các thư mục cgroup (ví dụ cho systemd/cgroup v2)
ls /sys/fs/cgroup/user.slice/
ls /sys/fs/cgroup/system.slice/docker-<container_id>.scope/ # Thay <container_id> bằng ID container đang chạy

# Xem thông tin CPU limit của một container (ví dụ cho cgroup v2)
# Tìm thư mục cgroup của container đó (ví dụ: /sys/fs/cgroup/system.slice/docker-<container_id>.scope/)
# Đọc file cpu.max hoặc cpu.shares/cpu.cfs_quota_us/cpu.cfs_period_us (tùy cgroup v1/v2)
# cat /sys/fs/cgroup/system.slice/docker-<container_id>.scope/cpu.max # Ví dụ cho cgroup v2

# Bên trong container, bạn không thể trực tiếp thao tác với cgroup của chính nó theo cách này

Lệnh `docker stats ` mà bạn thường dùng để xem mức độ sử dụng CPU, bộ nhớ, I/O của container chính là Docker daemon đang đọc thông tin từ các file trong thư mục cgroup của container đó trên máy Host.

cgroups là nền tảng đảm bảo rằng container không chỉ cô lập về môi trường mà còn được kiểm soát về tài nguyên, giúp hệ thống Host ổn định và các container có thể chạy cạnh nhau một cách công bằng.

Union Filesystems (UnionFS) & OverlayFS: Hệ Thống Tệp Tin Hiệu Quả Cho Container

Một trong những yếu tố khiến Docker image trở nên nhỏ gọn và việc khởi tạo container trở nên nhanh chóng là nhờ vào Union Filesystems, đặc biệt là OverlayFS (công nghệ lưu trữ mặc định cho Docker trên nhiều hệ thống Linux hiện đại).

UnionFS là một loại hệ thống tập tin cho phép bạn *kết hợp* nhiều thư mục (gọi là branches hoặc layers) thành một hệ thống tập tin duy nhất, thống nhất. Nó hoạt động giống như việc xếp chồng nhiều tấm phim trong suốt lên nhau: bạn nhìn xuyên qua các tấm trên để thấy nội dung của các tấm dưới.

Trong ngữ cảnh của Docker, các layer của image (như layer cho hệ điều hành cơ bản, layer cài đặt thư viện, layer thêm ứng dụng của bạn) là các layer *chỉ đọc* (read-only). Khi bạn chạy một container từ image đó, Docker thêm một layer *có thể ghi* (writable layer) lên trên cùng. UnionFS/OverlayFS sẽ “gom” tất cả các layer này lại để container thấy một hệ thống tập tin hoàn chỉnh duy nhất.

Mục đích chính của UnionFS trong Docker là:

  • Tái sử dụng layer: Các image khác nhau có thể chia sẻ các layer cơ bản giống nhau. Ví dụ: nhiều image Ubuntu khác nhau sẽ dùng chung layer OS Ubuntu. Điều này tiết kiệm đáng kể dung lượng đĩa.
  • Lưu trữ hiệu quả: Mỗi layer chỉ lưu trữ những thay đổi so với layer bên dưới nó.
  • Cơ chế Copy-on-Write (CoW): Đây là cơ chế quan trọng nhất. Khi một tiến trình trong container muốn *sửa đổi* một tập tin nằm trong một layer chỉ đọc bên dưới, hệ thống tập tin UnionFS/OverlayFS sẽ không sửa trực tiếp trên layer gốc. Thay vào đó, nó sẽ *copy* tập tin đó từ layer chỉ đọc lên layer có thể ghi của container. Sau đó, tiến trình sẽ sửa đổi bản copy này trên layer có thể ghi. Bản gốc trên layer chỉ đọc vẫn được giữ nguyên. Khi container đọc tập tin đó, nó sẽ đọc bản trên layer có thể ghi (vì nó “che” bản gốc).

Cơ chế Copy-on-Write giải thích tại sao:

  • Các thay đổi bên trong một container không ảnh hưởng đến image gốc hoặc các container khác chạy từ cùng image.
  • Việc khởi động container rất nhanh vì không phải sao chép toàn bộ hệ thống tập tin của image.
  • Dung lượng đĩa chỉ tăng lên khi có dữ liệu mới được ghi hoặc tập tin được sửa đổi (tạo bản copy trên layer ghi).

Bạn có thể kiểm tra loại Storage Driver mà Docker đang sử dụng trên hệ thống của bạn bằng lệnh `docker info`. Output sẽ hiển thị dòng `Storage Driver: overlay2` (hoặc aufs, devicemapper, v.v.). Overlay2 là phiên bản cải tiến của OverlayFS và là khuyến nghị hiện tại.

Cấu trúc thư mục của các layer này thường nằm ẩn sâu bên trong thư mục gốc của Docker (thường là `/var/lib/docker/`). Bạn sẽ thấy các thư mục con tương ứng với các layer của image và các layer có thể ghi của container.

# Trên máy Host Linux
# Chỉ mang tính chất tham khảo, không nên chỉnh sửa trực tiếp các file này
sudo ls /var/lib/docker/overlay2/
sudo ls /var/lib/docker/image/overlay2/layerdb/sha256/

Hiểu về UnionFS và Copy-on-Write giúp bạn lý giải kích thước của image, cách thức hoạt động của `docker commit`, và tại sao việc ghi/sửa đổi nhiều file lớn trong container có thể tốn kém I/O và dung lượng hơn bạn nghĩ.

Tổng Kết: Ba Mảnh Ghép Hoàn Hảo

Namespaces, cgroups, và UnionFS (hoặc OverlayFS) là ba công nghệ nền tảng, khi kết hợp lại, tạo nên sức mạnh và hiệu quả của Docker container trên Linux.

Công nghệ Mục đích chính Chức năng chính Ví dụ liên hệ Docker
Namespaces Cô lập tài nguyên hệ thống Tạo ra không gian riêng cho các tiến trình về PID, Mạng, Filesystem Mount, User, IPC, Hostname. Mỗi container có PID 1 riêng, IP riêng, hệ thống file gốc riêng, hostname riêng.
cgroups (Control Groups) Quản lý và giới hạn tài nguyên Kiểm soát mức độ sử dụng CPU, Bộ nhớ, I/O, Mạng của nhóm tiến trình. Giới hạn CPU/Memory khi chạy container (`–cpus`, `–memory`). Lệnh `docker stats`.
Union Filesystems (phổ biến là OverlayFS) Quản lý hệ thống tập tin hiệu quả Kết hợp nhiều layer file chỉ đọc và một layer ghi thành một view duy nhất. Áp dụng cơ chế Copy-on-Write. Docker Image được xây dựng từ các layer. Container có layer ghi riêng. Sao chép file khi sửa đổi.

Khi bạn chạy một lệnh `docker run `, Docker daemon thực hiện các bước sau (đơn giản hóa):

  1. Tải image nếu chưa có, kiểm tra các layer.
  2. Sử dụng UnionFS (OverlayFS) để kết hợp các layer chỉ đọc của image với một layer có thể ghi mới tạo cho container.
  3. Tạo các Namespaces mới (PID, Net, Mount, User, IPC, UTS) cho tiến trình container.
  4. Tạo một cgroup mới cho container và cấu hình các giới hạn tài nguyên theo yêu cầu (nếu có).
  5. Khởi động tiến trình chính của container trong môi trường đã được thiết lập (Namespaces, cgroup, hệ thống file UnionFS).

Hiểu được bức tranh này sẽ giúp bạn rất nhiều khi đối mặt với các vấn đề trong môi trường container. Ví dụ, nếu container bị chậm do “ngốn” tài nguyên, bạn biết rằng cần kiểm tra cgroups. Nếu có vấn đề về networking, bạn sẽ nghĩ đến Net namespace. Nếu cần phân tích nội dung file trong container, bạn sẽ hiểu cấu trúc layer của UnionFS.

Chúng ta đã đi qua một chặng đường khá dài trong việc tìm hiểu các kiến thức nền tảng cần thiết để làm việc với Docker, từ những khái niệm cơ bản nhất đến các thành phần cốt lõi dưới lớp vỏ bọc của nó. Việc nắm vững Linux là cực kỳ quan trọng, như chúng ta đã nhấn mạnh trong bài viết về các kỹ năng Linux cốt lõi.

Ở các bài viết tiếp theo trong series “Roadmap Docker”, chúng ta sẽ bắt đầu thực hành nhiều hơn, khám phá nền tảng phát triển web liên quan đến container hóa, cách chọn ngôn ngữ lập trình phù hợp, và cách kiến trúc ứng dụng ảnh hưởng đến thiết kế container. Chúng ta sẽ dần tiến tới việc xây dựng và quản lý các ứng dụng container hóa một cách chuyên nghiệp.

Hãy tiếp tục theo dõi series này để nâng cao kiến thức và kỹ năng của mình nhé! Nếu có bất kỳ câu hỏi nào về Namespaces, cgroups, hay UnionFS, đừng ngần ngại để lại bình luận. Hẹn gặp lại trong bài viết tiếp theo!

Chỉ mục