Container & cgroups nhìn từ Linux
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 đề
--cpus 1không có nghĩa "một core riêng". Nó là quota thời gian: tối đa 100ms CPU trong mỗi chu kỳ 100ms, tính tổng mọi thread.- JVM có nhiều thread (GC, JIT, Tomcat). Nếu 8 thread cùng chạy trên 8 core thật, quota 100ms hết sau ~12ms. 88ms còn lại toàn bộ container bị throttle, đứng chờ chu kỳ sau.
- Dashboard trung bình theo phút nên chỉ thấy 40%, không thấy các khoảng bị đóng băng.
3. Giải pháp: đọc cgroups v2
- cgroups v2: một cây thư mục duy nhất ở
/sys/fs/cgroup. Mỗi container/service là một thư mục, file trong đó là giới hạn và số liệu. - CPU:
cpu.max= "quota period" (vd100000 100000= 1 CPU).cpu.weight= tỉ lệ chia khi tranh chấp (giốngCPUWeight, khác quota).cpu.statcónr_throttled,throttled_usec. - Memory:
memory.max= trần cứng, vượt thì OOM kill trong cgroup.memory.high= ngưỡng mềm, vượt thì kernel ép thu hồi, chậm lại.memory.eventsđếmoom_kill. Lưu ýmemory.currenttính cả page cache. - pids.max: giới hạn số task (process + thread).
- PSI (
cpu.pressure,memory.pressure,io.pressure): % thời gian task bị chặn vì thiếu tài nguyên. - JVM từ 10 (và 8u191):
UseContainerSupportbật mặc định, đọccpu.maxvàmemory.maxđể tính số CPU và heap mặc định.
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:
- Đặt CPU limit cao hơn (vd 2), hoặc chỉ đặt
cpu.weight/request và bỏ limit nếu cluster cho phép. - Nếu giữ limit thấp:
-XX:ActiveProcessorCountkhớp với limit để JVM không tạo quá nhiều GC thread cùng chạy. - Memory:
-XX:MaxRAMPercentage=75, chừa phần còn lại cho native (bài 10.2).
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ên | File cgroup v2 | Vượt thì | Biểu hiện ở app |
|---|---|---|---|
| CPU quota | cpu.max | Throttle tới chu kỳ sau | Latency tăng theo bậc, CPU trung bình thấp |
| CPU weight | cpu.weight | Chỉ chia lại khi tranh chấp | Không throttle khi máy rảnh |
| Memory cứng | memory.max | OOM kill trong cgroup | Chết, exit 137, log không có lỗi |
| Memory mềm | memory.high | Bị ép thu hồi | Chậm, không chết |
| Số task | pids.max | clone() 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
- Luôn đặt memory limit cho container Java; đặt heap theo phần trăm.
- CPU limit thấp (dưới 1–2) cho API Java dễ gây throttling; theo dõi
nr_throttledtrước khi chọn số. - systemd trên VM cũng dùng cùng cơ chế:
MemoryMax=,CPUQuota=,TasksMax=.
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
- Con số đáng theo dõi là working set ≈
memory.current − inactive_file. Đây cũng là số mà Kubernetes/cAdvisor dùng (container_memory_working_set_bytes). - Dấu hiệu thật sự sắp chết:
anontăng dần,memory.eventscómaxtăng,memory.pressurecao.
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
- CPU vượt → throttle (chậm). Memory vượt → kill (137).
cpu.stat: nr_throttled.memory.events: oom_kill.- Working set = current − inactive_file.
- systemd trên VM cũng là cgroups.
Tóm tắt chương 10
| Bài | Ý chính | Lệnh/file cần nhớ |
|---|---|---|
| 10.1 Process lifecycle | fork + exec; R/S/D/Z; zombie chờ cha wait; chạy app bằng systemd | ps -eo pid,ppid,stat, systemctl, journalctl -u |
| 10.2 Virtual memory | VSZ 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 Scheduler | Load = R + D theo thread; context switch có giá; CPU-bound vs IO-bound | vmstat 1, pidstat -w, nice |
| 10.4 FD, signal, IPC | Socket là fd; Too many open files; 15/9/3 | /proc/PID/fd, lsof, LimitNOFILE, kill -3 |
| 10.5 JVM ↔ OS thread | Platform thread 1:1; nid = TID hex; -Xss; virtual thread chỉ thấy carrier | top -H, printf %x, jcmd Thread.print |
| 10.6 cgroups | Quota CPU → throttle; memory.max → kill; JVM đọc cgroup | cpu.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.