Container Không Phải Máy Ảo — Giải Mã Docker Từ First Principles

8 phút đọc

Vấn đề gốc — Tại sao cần Container?

Nếu bạn đang dùng Docker hàng ngày nhưng chưa thực sự hiểu container là gì ở mức kernel — bài viết này dành cho bạn. Chúng ta sẽ đi từ vấn đề gốc, phân tích từ first principles, và hiểu tại sao container không phải máy ảo.

Trước khi hiểu Docker, cần hiểu vấn đề mà nó giải quyết.

Dependency Hell

Một ứng dụng cần: OS cụ thể, runtime version, libraries, config files, environment variables. Khi deploy lên server khác → "Works on my machine" syndrome.

Giải pháp truyền thống: Virtual Machine

Đóng gói cả OS + app vào một VM → nặng, chậm, tốn tài nguyên.

Giải pháp hiện đại: Container

Đóng gói chỉ app + dependencies → chia sẻ kernel host → nhẹ, nhanh, portable.

Câu hỏi cốt lõi: Làm sao chạy nhiều ứng dụng trên cùng một máy mà chúng không thấy nhau, không ảnh hưởng nhau, nhưng không cần OS riêng?


First Principle — Container chỉ là một Process

Khi bạn chạy docker run nginx, thực chất Docker yêu cầu Linux kernel:

  1. Tạo một process mới (nginx)
  2. Cô lập process đó khỏi phần còn lại của hệ thống (Namespaces)
  3. Giới hạn tài nguyên process đó được dùng (Cgroups)
  4. Cung cấp filesystem riêng cho process đó (Union Filesystem)

Bạn có thể verify điều này: chạy docker run -d nginx, sau đó ps aux | grep nginx trên host → bạn sẽ thấy process nginx chạy trực tiếp trên host, không phải trong một VM nào.


3 trụ cột Linux Kernel tạo nên Container

Trụ cột 1: Namespaces — Tạo "ảo giác" cô lập

Namespaces là kernel feature tạo ra các không gian riêng biệt cho process. Mỗi process trong namespace chỉ nhìn thấy tài nguyên thuộc namespace đó.

Linux cung cấp 7 loại namespace:

Namespace Cô lập cái gì Ý nghĩa thực tế
PID Process IDs Container thấy PID 1 là app của nó, không thấy process của host
NET Network stack Container có IP, port, routing table riêng
MNT Mount points Container có filesystem tree riêng
UTS Hostname Container có hostname riêng (vì sao docker exec vào thấy hostname khác)
IPC Inter-process communication Shared memory, message queues riêng biệt
USER User/Group IDs Root trong container ≠ Root trên host (nếu cấu hình đúng)
CGROUP Cgroup hierarchy Container chỉ thấy cgroup của mình

Mental model: Namespaces = bức tường của căn hộ. Mỗi căn hộ (container) có không gian riêng, nhưng thực ra đều nằm trong cùng một tòa nhà (kernel).

Ví dụ thực tế — PID Namespace

# Trên host: nginx process có PID 28374
ps aux | grep nginx
# => root 28374 ... nginx: master process

# Trong container: cùng process đó có PID 1
docker exec my-nginx ps aux
# => root 1 ... nginx: master process

Cùng một process, nhưng nhìn từ hai namespace khác nhau → PID khác nhau. Container "tưởng" nó là process duy nhất.

Trụ cột 2: Cgroups (Control Groups) — Giới hạn tài nguyên

Nếu Namespaces tạo ra cô lập (không thấy nhau), thì Cgroups tạo ra giới hạn (không chiếm hết tài nguyên).

Cgroups cho phép kernel:

  • Giới hạn CPU: Container A chỉ được dùng 50% CPU
  • Giới hạn Memory: Container B tối đa 512MB RAM, vượt → bị kill (OOM)
  • Giới hạn I/O: Container C tối đa 100MB/s disk read
  • Giới hạn Network bandwidth: Container D tối đa 10Mbps

Mental model: Cgroups = ban quản lý tòa nhà phân bổ điện nước. Mỗi căn hộ có quota riêng, dùng quá → bị cắt.

Ví dụ thực tế

# Chạy container với giới hạn: 256MB RAM, 0.5 CPU
docker run -d --memory=256m --cpus=0.5 nginx

# Kiểm tra cgroup của container
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes
# => 268435456 (256MB in bytes)

Trụ cột 3: Union Filesystem (OverlayFS) — Hệ thống file phân lớp

Container cần một filesystem riêng nhưng không thể copy cả OS cho mỗi container. Giải pháp: Union Filesystem — xếp chồng nhiều layer lên nhau.

┌─────────────────────────────────┐
│   Writable Layer (Container)    │  ← Chỉ layer này ghi được
├─────────────────────────────────┤
│   Layer: COPY . /app            │  ← Read-only
├─────────────────────────────────┤
│   Layer: RUN apt-get install    │  ← Read-only
├─────────────────────────────────┤
│   Layer: Ubuntu base image      │  ← Read-only
└─────────────────────────────────┘

Cơ chế Copy-on-Write (CoW)

Khi container cần sửa một file nằm ở read-only layer, nó không sửa trực tiếp. Thay vào đó:

  1. Copy file đó lên writable layer
  2. Sửa bản copy trên writable layer
  3. Bản gốc ở read-only layer không bị thay đổi

Điều này giải thích tại sao nhiều container có thể share cùng base image mà không conflict — mỗi container chỉ tốn thêm dung lượng cho phần nó thay đổi.

Ý nghĩa: 100 container cùng dùng image Ubuntu → chỉ cần 1 bản Ubuntu trên disk. Tiết kiệm disk space đáng kể trong production với hàng trăm container.


Tổng hợp: Container = Namespaces + Cgroups + Union FS

┌─────────────────────────────────────────────────┐
│                 CONTAINER                        │
│                                                  │
│  ┌──────────┐  ┌──────────┐  ┌───────────────┐  │
│  │Namespaces│  │  Cgroups  │  │  Union FS     │  │
│  │(cô lập)  │  │(giới hạn) │  │(filesystem)   │  │
│  └──────────┘  └──────────┘  └───────────────┘  │
│                                                  │
│         Tất cả chạy trên CÙNG KERNEL             │
└─────────────────────────────────────────────────┘

Docker không phát minh ra container. Docker đóng gói 3 kernel features trên thành công cụ dễ dùng với CLI, image registry, và ecosystem.


Docker Engine — Kiến trúc bên trong

Khi bạn chạy docker run nginx, lệnh đi qua 3 tầng:

┌──────────────────────────────────────────┐
│  docker CLI (client)                     │
│         │ REST API                       │
│         ▼                                │
│  dockerd (Docker Daemon)                 │
│         │ gRPC                           │
│         ▼                                │
│  containerd (high-level runtime)         │
│         │                                │
│         ▼                                │
│  runc (low-level runtime)                │
│         │                                │
│         ▼                                │
│  Linux Kernel (namespaces + cgroups)     │
└──────────────────────────────────────────┘
  • dockerd — daemon xử lý Docker API requests, quản lý images, containers, networks, volumes
  • containerd — high-level container runtime, quản lý lifecycle (create, start, stop, delete)
  • runc — low-level runtime, thực sự gọi Linux kernel APIs (clone + unshare syscalls) để tạo namespaces và cgroups

Sau khi runc tạo xong container process, nó exit. Container process chạy độc lập — không có long-running parent process nào giữ nó.


Key Takeaways


Tham khảo

Bài viết liên quan

Đang cập nhật...