Container là process: namespace, cgroups và JVM

3 câu hỏi~25 phútTừ khóa: namespace, cgroups, VM, shared kernel, OOMKilled, MaxRAMPercentage, container support, CPU limit, ActiveProcessorCount

Tiếp đi: container thực chất là gì bên dưới? Có phải một máy ảo nhỏ không?

Không phải. Container là một process Linux bình thường, được kernel "bịt mắt" bằng namespace và "giới hạn khẩu phần" bằng cgroups. Hiểu chỗ này thì mới hiểu được vì sao JVM trong container hay bị kill.

1. Bài toán

Chạy order-service với giới hạn 512MB:

docker run -d --name order-1 -m 512m --cpus 1 -p 8080:8080 order-service:1.0

Chạy được vài phút, tải tăng lên thì container biến mất:

docker ps -a
STATUS: Exited (137)

docker inspect order-1 --format '{{.State.OOMKilled}}'
true

2. Vấn đề

3. Giải pháp: hiểu namespace + cgroups

       Virtual Machine                      Container
┌──────────────────────────┐     ┌──────────────────────────┐
│ App                      │     │ App (java)               │
│ Guest OS + Guest Kernel  │     │ (userland của image)     │
├──────────────────────────┤     ├──────────────────────────┤
│ Hypervisor               │     │ Host kernel (dùng chung) │
├──────────────────────────┤     │ namespace + cgroups      │
│ Hardware                 │     ├──────────────────────────┤
└──────────────────────────┘     │ Hardware                 │
                                 └──────────────────────────┘

Namespace = process thấy gì:

NamespaceCô lậpHệ quả
PIDDanh sách processjava là PID 1 trong container
NETNetwork interface, port, routinglocalhost trong container là chính container
MNTFilesystemThấy filesystem của image, không thấy của host
UTSHostnameHostname = container ID
IPCShared memory, semaphoreKhông đụng IPC của process khác
USERUID/GIDroot trong container có thể map thành user thường trên host

cgroups = process được dùng bao nhiêu: memory, CPU, số process, I/O. Vượt memory limit thì kernel OOM-kill; vượt CPU quota thì bị throttle (chậm lại), không bị kill.

4. Code từng bước

Bước 1: Thấy container là process trên host (Linux host; Docker Desktop thì nó nằm trong VM Linux của Docker Desktop):

docker top order-1          # process bên trong container
ps aux | grep app.jar       # cùng process đó nhìn từ host, PID khác
docker exec order-1 cat /proc/1/cmdline   # trong container: PID 1 là java

Bước 2: Đặt limit

docker run -d --name order-1 \
  -m 768m --cpus 1.5 \
  -p 8080:8080 order-service:1.0

docker stats order-1        # xem CPU %, MEM USAGE / LIMIT

Bước 3: Cho JVM tính heap theo limit của container. Từ Java 10 (và 8u191), JVM có container support (bật mặc định): đọc limit từ cgroup thay vì RAM của cả host. Mặc định heap tối đa chỉ = 25% limit, khá phí. Dùng phần trăm thay vì -Xmx cứng:

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/order-service-1.0.0.jar app.jar
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Bước 4: Kiểm tra JVM thấy gì

docker run --rm -m 768m --cpus 1.5 eclipse-temurin:17-jre \
  java -XX:MaxRAMPercentage=75.0 -XshowSettings:vm -version
# Max. Heap Size (Estimated): ~576M

docker run --rm -m 768m --cpus 1.5 eclipse-temurin:17-jre \
  java -XX:+PrintFlagsFinal -version | grep -E "ActiveProcessorCount|UseContainerSupport"

Với CPU: JVM dùng số CPU từ cgroup để tính số GC thread, kích thước ForkJoinPool.commonPool... --cpus 1.5 thì JVM thấy 2 CPU. Nếu cần ép: -XX:ActiveProcessorCount=2.

5. Cách hoạt động

docker run -m 768m --cpus 1.5 order-service:1.0
        │
        ▼
containerd/runc tạo process java
        │
        ├── namespaces: PID, NET, MNT, UTS, IPC (cô lập "thấy gì")
        └── cgroup:     memory.max = 768M, cpu.max = 150000/100000
                │
                ▼
JVM khởi động → đọc cgroup → heap max = 75% × 768M ≈ 576M
                │
   tổng RSS (heap + metaspace + stacks + direct) > 768M ?
        │ có                         │ không
        ▼                            ▼
  kernel OOM-kill (exit 137)     chạy bình thường

6. So sánh

Virtual MachineContainer
KernelMỗi VM một kernel riêngDùng chung kernel host
Khởi độngHàng chục giây đến phútBằng thời gian start process
Kích thướcGBMB
Cô lậpMạnh (hypervisor)Yếu hơn (cùng kernel)
Chạy OS khácĐược (Windows trên Linux)Phải cùng loại kernel
Cách đặt heapƯuNhược
-Xmx cứngRõ ràngĐổi limit phải sửa cả Xmx, dễ quên → OOMKilled
Mặc định (25%)Không cần cấu hìnhLãng phí 75% RAM được cấp
MaxRAMPercentage 70–80%Tự co giãn theo limitVẫn phải để dư cho non-heap

7. Khi nào nên dùng / không nên dùng

Mình đặt -Xmx512m và limit 512MB rồi mà vẫn bị kill, log cũng không có OutOfMemoryError. Tại sao vậy?

Đúng như bạn quan sát, và đó chính là manh mối: có hai loại "hết bộ nhớ" khác nhau.

Memory limit cgroup = 512M
┌───────────────────────────────────────┐
│ Heap (-Xmx512m)                 512M  │ ← riêng heap đã chiếm hết
├───────────────────────────────────────┤
│ Metaspace                       ~80M  │
│ Thread stacks (200 thread × 1M) ~200M │ ← tràn ra ngoài limit
│ Code cache, GC, direct buffer   ~60M  │
└───────────────────────────────────────┘

Sửa: bỏ -Xmx512m, dùng -XX:MaxRAMPercentage=75.0 và tăng limit nếu số đo thực cần. Muốn biết non-heap dùng bao nhiêu: bật -XX:NativeMemoryTracking=summary rồi docker exec order-1 jcmd 1 VM.native_memory summary (cần image có jcmd, tức JDK).

Phỏng vấn hỏi container khác VM thế nào, chạy JVM trong container cần chú ý gì thì trả lời sao?

Q1. Container khác VM thế nào?

🇻🇳 Tiếng Việt:

Container là một process Linux dùng chung kernel với host, được cô lập bằng namespace (PID, network, mount...) và giới hạn tài nguyên bằng cgroups. VM có kernel riêng chạy trên hypervisor. Vì vậy container nhẹ, khởi động nhanh, nhưng cô lập yếu hơn VM.

🇬🇧 English:

A container is a Linux process that shares the host kernel. It is isolated with namespaces such as PID, network and mount, and its resources are limited with cgroups. A VM runs its own kernel on a hypervisor. So containers are lighter and start faster, but their isolation is weaker than a VM.

Q2. Container Java bị exit 137 thì bạn điều tra thế nào?

🇻🇳 Tiếng Việt:

137 là SIGKILL, thường do vượt memory limit của cgroup. Em kiểm tra OOMKilled trong docker inspect hoặc trạng thái pod. Nguyên nhân hay gặp là heap đặt quá sát limit mà quên non-heap. Em dùng MaxRAMPercentage khoảng 75%, đo bằng Native Memory Tracking rồi chỉnh limit.

🇬🇧 English:

Exit code 137 means SIGKILL, usually because the container exceeded its cgroup memory limit. I check OOMKilled in docker inspect or the pod status. A common cause is a heap sized too close to the limit, ignoring non-heap memory. I use MaxRAMPercentage around 75%, measure with Native Memory Tracking and then adjust the limit.

Q3. CPU limit ảnh hưởng JVM thế nào?

🇻🇳 Tiếng Việt:

Vượt CPU quota thì container bị throttle, không bị kill, nên biểu hiện là latency tăng. JVM đọc số CPU từ cgroup để chọn số GC thread và kích thước common pool. Limit quá thấp làm GC và JIT chậm; có thể chỉnh bằng ActiveProcessorCount.

🇬🇧 English:

When a container exceeds its CPU quota it is throttled, not killed, so the symptom is higher latency. The JVM reads the CPU count from the cgroup to size GC threads and the common pool. Very low limits slow down GC and JIT; the count can be overridden with ActiveProcessorCount.

Cách nhớ ngắn gọn


Tự kiểm tra

Namespace và cgroups khác nhau thế nào?

Namespace cô lập những gì process nhìn thấy (PID, network, filesystem). cgroups giới hạn tài nguyên process được dùng (memory, CPU).

Exit code 137 nghĩa là gì?

128 + 9: process bị SIGKILL, thường do OOM killer khi vượt memory limit.

Mặc định JVM trong container lấy heap tối đa bao nhiêu?

Khoảng 25% memory limit của container (MaxRAMPercentage mặc định = 25).

Vì sao không nên đặt -Xmx bằng memory limit?

JVM còn dùng metaspace, thread stack, code cache, direct memory ngoài heap; tổng sẽ vượt limit và bị kernel kill.

Container có chạy được image Windows trên host Linux không?

Không, vì container dùng chung kernel của host. Cần VM nếu muốn kernel khác.