Container & cgroups nhìn từ Linux

4 câu hỏi~25 phútTừ khóa: cgroups v2, /sys/fs/cgroup, cpu.max, cpu.stat, nr_throttled, CFS quota, period, memory.max, memory.high, memory.events, oom_kill, pids.max, PSI, UseContainerSupport, availableProcessors

Bài cuối chương Linux nhé. order-service chạy container limit 1 CPU. Dashboard báo CPU trung bình chỉ 40%, nhưng p99 latency thỉnh thoảng nhảy lên 300ms đúng từng nhịp. Có liên quan gì tới cgroups không? Mình muốn xem tận file trong Linux.

Có, rất hay gặp. Bài 16.3 đã giải thích container = namespace + cgroups và chuyện -Xmx với memory limit. Bài này mở tận file trong /sys/fs/cgroup để thấy kernel đo và chặn thế nào.

1. Bài toán

docker run -d --name order-1 --cpus 1 --memory 1g order-service:1.4.0

CPU trung bình 40%. Nhưng mỗi khi GC chạy hoặc có đợt request, latency tăng thành từng bậc khoảng 100ms.

2. Vấn đề

3. Giải pháp: đọc cgroups v2

4. Code từng bước

Bước 1: Tìm cgroup của container

stat -fc %T /sys/fs/cgroup             # cgroup2fs = v2
PID=$(docker inspect -f '{{.State.Pid}}' order-1)
cat /proc/$PID/cgroup
0::/system.slice/docker-3f9a....scope
CG=/sys/fs/cgroup/system.slice/docker-3f9a....scope
# bên trong container thì đơn giản là /sys/fs/cgroup

Bước 2: Đọc CPU và throttling

cat $CG/cpu.max
100000 100000
cat $CG/cpu.stat
usage_usec 81234567
nr_periods 36000
nr_throttled 9100               ← 25% chu kỳ bị throttle
throttled_usec 912000000
cat $CG/cpu.pressure
some avg10=18.20 avg60=15.02 ...

Tỉ lệ nr_throttled / nr_periods trên vài % là đáng lo với API cần latency thấp.

Bước 3: Đọc memory

cat $CG/memory.max          # 1073741824
cat $CG/memory.current
cat $CG/memory.events
low 0
high 0
max 312
oom 2
oom_kill 2                  ← đã bị kernel kill 2 lần
grep -E '^(anon|file) ' $CG/memory.stat   # anon = JVM thật sự dùng, file = page cache
cat $CG/pids.current $CG/pids.max

Bước 4: Xem JVM thấy gì và chỉnh

docker exec order-1 java -XshowSettings:system -version
Operating System Metrics:
    Provider: cgroupv2
    Effective CPU Count: 1
    Memory Limit: 1.00G

docker exec order-1 jcmd 1 VM.flags | tr ' ' '\n' | grep -E 'MaxHeapSize|ParallelGCThreads'
// Kiểm tra trong code (log lúc khởi động)
@Component
class ContainerInfo implements ApplicationRunner {
    private static final Logger log = LoggerFactory.getLogger(ContainerInfo.class);
    @Override
    public void run(ApplicationArguments args) {
        Runtime rt = Runtime.getRuntime();
        log.info("cpus={} maxHeap={}MB", rt.availableProcessors(), rt.maxMemory() / 1024 / 1024);
    }
}

Cách giảm throttling cho API Java:

5. Cách hoạt động

cpu.max = 100000 100000  (100ms mỗi 100ms)

chu kỳ 1 (100ms)
 0ms        12ms                                  100ms
 |██████████|.....................................|
  8 thread chạy song song trên 8 core thật
  → dùng hết 8×12 ≈ 100ms quota trong 12ms
             └─ THROTTLED 88ms: mọi thread đứng yên,
                request đang xử lý chờ → p99 +88ms
chu kỳ 2: quota được nạp lại → chạy tiếp

memory.max = 1G
 anon (heap + native) + file cache ──► gần 1G
   1. kernel thu hồi page cache trong cgroup
   2. vẫn vượt → OOM killer CHỈ trong cgroup → SIGKILL java
   3. memory.events: oom_kill +1, docker: OOMKilled=true, exit 137

6. So sánh

Tài nguyênFile cgroup v2Vượt thìBiểu hiện ở app
CPU quotacpu.maxThrottle tới chu kỳ sauLatency tăng theo bậc, CPU trung bình thấp
CPU weightcpu.weightChỉ chia lại khi tranh chấpKhông throttle khi máy rảnh
Memory cứngmemory.maxOOM kill trong cgroupChết, exit 137, log không có lỗi
Memory mềmmemory.highBị ép thu hồiChậm, không chết
Số taskpids.maxclone() thất bại"unable to create native thread" (10.5)

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

Container báo memory.current gần chạm 1GB làm mình tưởng sắp OOM, nhưng app vẫn chạy cả tuần. Con số này có đáng tin không?

Bạn nhận ra đúng điểm dễ nhầm. memory.current gồm cả page cache (file mà app đọc/ghi: log, jar, file tạm). Page cache có thể thu hồi: khi gần chạm memory.max, kernel đẩy cache ra trước khi phải kill ai.

grep -E '^(anon|file|inactive_file) ' $CG/memory.stat
anon 690000000            ← JVM thật sự dùng (heap + native)
file 320000000            ← page cache, phần lớn thu hồi được
inactive_file 280000000

Còn VM không có Docker thì sao? order-service chạy systemd trên VM có dùng cgroups không?

Có. Trên Ubuntu hiện tại, systemd đặt mọi service vào cgroup riêng, kể cả khi bạn không đặt limit. Vì vậy cùng các file đó dùng được:

systemd-cgls -u order-service.service
cat /sys/fs/cgroup/system.slice/order-service.service/memory.current
systemd-cgtop                         # như top nhưng theo cgroup
[Service]
MemoryHigh=1400M
MemoryMax=1600M
CPUQuota=200%          # 2 CPU
TasksMax=2048

Đặt limit cho service trên VM có lợi: order-service quá tay chỉ tự chết trong cgroup của nó, Postgres hay sshd trên cùng máy không bị OOM killer chọn nhầm (bài 10.2). JVM chạy dưới systemd cũng đọc được limit này như trong container.

Phỏng vấn hỏi cgroups và JVM trong container thì trả lời sao? Và tóm tắt giúp mình cả chương Linux.

Q1. CPU throttling là gì, vì sao CPU trung bình thấp mà app vẫn chậm?

🇻🇳 Tiếng Việt:

CPU limit là quota thời gian trong mỗi chu kỳ, thường 100ms, tính tổng mọi thread. JVM nhiều thread có thể dùng hết quota rất sớm trong chu kỳ, sau đó cả container bị dừng tới chu kỳ sau, nên latency tăng theo bậc dù CPU trung bình thấp. Em xem nr_throttled trong cpu.stat, rồi tăng limit, bỏ limit chỉ giữ request, hoặc chỉnh ActiveProcessorCount.

🇬🇧 English:

A CPU limit is a time quota per period, usually 100 milliseconds, summed across all threads. A JVM with many threads can burn the quota early in the period, and then the whole container is paused until the next one, so latency jumps in steps while average CPU looks low. I check nr_throttled in cpu.stat, then raise the limit, drop the limit and keep only requests, or tune ActiveProcessorCount.

Q2. JVM biết limit của container bằng cách nào?

🇻🇳 Tiếng Việt:

Với UseContainerSupport, bật mặc định từ Java 10, JVM đọc cgroup như cpu.max và memory.max để tính availableProcessors, số GC thread và heap mặc định. Em đặt heap bằng MaxRAMPercentage khoảng 75% và kiểm tra bằng java -XshowSettings:system.

🇬🇧 English:

With UseContainerSupport, enabled by default since Java 10, the JVM reads cgroup files such as cpu.max and memory.max to compute availableProcessors, GC thread counts and the default heap. I size the heap with MaxRAMPercentage around 75% and verify it with java -XshowSettings:system.

Q3. memory.max và memory.high khác nhau thế nào?

🇻🇳 Tiếng Việt:

memory.max là trần cứng: vượt thì kernel OOM kill process trong cgroup, exit 137. memory.high là ngưỡng mềm: vượt thì kernel ép thu hồi bộ nhớ và làm chậm process, không kill. Khi đọc memory.current phải trừ page cache, nên em theo dõi working set.

🇬🇧 English:

memory.max is a hard limit: going over it makes the kernel OOM kill a process in the cgroup, exit code 137. memory.high is a soft limit: going over it triggers reclaim and throttles the process, without killing it. memory.current includes page cache, so I monitor the working set instead.

Cách nhớ ngắn gọn

Tóm tắt chương 10

BàiÝ chínhLệnh/file cần nhớ
10.1 Process lifecyclefork + exec; R/S/D/Z; zombie chờ cha wait; chạy app bằng systemdps -eo pid,ppid,stat, systemctl, journalctl -u
10.2 Virtual memoryVSZ là reserve, RSS là RAM thật; RSS JVM > Xmx; swap hại GC; OOM killer/proc/PID/status, jcmd VM.native_memory, dmesg
10.3 SchedulerLoad = R + D theo thread; context switch có giá; CPU-bound vs IO-boundvmstat 1, pidstat -w, nice
10.4 FD, signal, IPCSocket là fd; Too many open files; 15/9/3/proc/PID/fd, lsof, LimitNOFILE, kill -3
10.5 JVM ↔ OS threadPlatform thread 1:1; nid = TID hex; -Xss; virtual thread chỉ thấy carriertop -H, printf %x, jcmd Thread.print
10.6 cgroupsQuota CPU → throttle; memory.max → kill; JVM đọc cgroupcpu.stat, memory.events, -XshowSettings:system

Tự kiểm tra

cpu.max = 50000 100000 nghĩa là gì?

Tối đa 50ms CPU mỗi chu kỳ 100ms = 0.5 CPU, tính tổng mọi thread.

Xem container có bị throttle không ở đâu?

cpu.stat: nr_throttled, throttled_usec; hoặc cpu.pressure.

Xem container đã bị OOM kill bao nhiêu lần ở đâu?

memory.events, dòng oom_kill.

Vì sao memory.current cao chưa chắc sắp OOM?

Nó gồm page cache thu hồi được. Xem anon/working set.

Lệnh để xem JVM nhận limit nào?

java -XshowSettings:system -version.