I/O, Lưu trữ & Tài nguyên/Mini-challenge — máy chậm, nghẽn ở đâu?
25/26
Bài 25 / 26~20 phútTài nguyên & Đo lườngMiễn phí lượt xem

Mini-challenge — máy chậm, nghẽn ở đâu?

Bốn triệu chứng thật, một quy trình: dùng USE method cùng top, iostat và dmesg để quyết nghẽn CPU, RAM hay I/O, và nêu bằng chứng cho từng kết luận.

Năm bài vừa rồi cho bạn năm cái kính lúp: load average và header của top, RSS cạnh PSS, await cạnh aqu-sz, cpu.stat của cgroup, và khối log OOM. Bây giờ tới phần khó hơn hẳn việc biết từng cái kính: chọn đúng cái để cầm lên, theo đúng thứ tự.

Bốn ca dưới đây đều bắt đầu bằng đúng một câu mà bạn sẽ nghe suốt sự nghiệp: "máy chậm". Mỗi ca kèm một tập bằng chứng đã thu sẵn. Việc của bạn là kết luận nghẽn nằm ở đâu, và quan trọng hơn — chỉ ra dòng nào trong tập bằng chứng chứng minh điều đó, cùng một hành động sửa cụ thể.

🎯 Đề bài

Với mỗi ca, viết ra ba thứ trước khi đọc phần lời giải:

  1. Nút thắt nằm ở đâu — CPU, bộ nhớ, I/O, hay không phải cả ba.
  2. Bằng chứng — chỉ đúng dòng, đúng con số. Một kết luận không kèm dòng nào là một phỏng đoán.
  3. Hành động sửa — và một câu giải thích vì sao hành động phổ biến nhất mà người ta hay làm trong tình huống đó lại vô ích.

Ca A — máy build chậm dần từ trưa

Máy CI 8 lõi, ổ SATA, không container.

$ uptime
load average: 24.31, 23.88, 21.02

$ top   (header)
%Cpu(s):  4.1 us,  1.2 sy,  0.0 ni, 88.3 id,  6.1 wa,  0.0 hi,  0.2 si,  0.1 st

$ vmstat 1   (dong thu ba)
 r  b   swpd   free   buff  cache   si   so
 1 22      0 742118 102340 6112990    0    0

$ iostat -x 1   (bo dong dau)
Device   r/s    w/s  rMB/s  wMB/s  await  aqu-sz  %util
sda     18.0  240.0    0.4   28.6  145.2    37.4    92.0

Ca B — API p99 nhọn theo từng đợt

Container 4 lõi trên node 32 lõi, thread pool 32.

$ uptime   (trong container)
load average: 1.24, 1.31, 1.28

$ cat /sys/fs/cgroup/.../cpu.max
100000 100000

$ cat /sys/fs/cgroup/.../cpu.stat
nr_periods 1200
nr_throttled 318
throttled_usec 4238000

$ iostat -x 1   (nvme0n1)
r/s 210.0  w/s 88.0  await 0.4  aqu-sz 0.1  %util 41.0

Ca C — service biến mất lúc 3 giờ sáng

Container Java, memory.max là 2 GB, khởi động với -Xmx2g.

$ docker inspect ...   (trich)
"ExitCode": 137

Log ung dung: cut giua chung, khong exception, khong heap dump.

$ dmesg -T   (tren MAY CHU, trich)
oom-kill:constraint=CONSTRAINT_MEMCG,...,
    task_memcg=/system.slice/docker-9c1e....scope,task=java,pid=4471
Out of memory: Killed process 4471 (java) total-vm:9422160kB,
    anon-rss:2044880kB, file-rss:184kB

Ca D — máy ảo cloud chậm dần cả tuần

Dashboard đang bật hai cảnh báo đỏ: "RAM usage 190%" và "disk utilization 99%".

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            16Gi       8.1Gi       1.2Gi       0.3Gi       6.7Gi       6.2Gi
Swap:          2.0Gi          0B      2.0Gi

$ ps aux | awk '{s+=$6} END {print s/1024/1024, "GB tong RSS"}'
30.8 GB tong RSS

$ iostat -x 1   (nvme0n1)
r/s 940.0  w/s 512.0  await 0.3  aqu-sz 0.4  %util 99.0

$ top   (header)
%Cpu(s): 31.0 us,  4.2 sy,  0.0 ni, 40.1 id,  0.4 wa,  0.0 hi,  0.3 si, 24.0 st

🔍 Đầu vào — Xử lý — Đầu ra

Đầu vàoBốn tập bằng chứng ở trên; không cần máy Linux, nhưng có thì tự chạy lại được các lệnh
Xử lýVới mỗi ca, đi hết ba câu hỏi của USE method cho từng tài nguyên rồi loại dần
Đầu raBốn kết luận, mỗi kết luận kèm dòng bằng chứng và một hành động sửa

📦 Bạn sẽ dùng lại những gì

▶️ Công cụ — USE method

Brendan Gregg đặt tên cho một quy trình rất ngắn: với mỗi tài nguyên, hỏi đúng ba câu.

  • U — Utilization: tài nguyên bận bao nhiêu phần thời gian?
  • S — Saturation: có việc phải xếp hàng chờ nó không, và hàng dài bao nhiêu?
  • E — Errors: có lỗi nào được ghi lại không?

Giá trị của quy trình nằm ở chữ saturation. Mức bận một mình gây hiểu nhầm được ở cả hai chiều: bận 100% chưa chắc đã hết sức, mà bận 40% cũng chưa chắc đã rảnh rang. Hàng đợi mới là thứ người dùng cảm nhận được, nên câu S mới là câu phân xử.

Ba tai nguyen nhan ba cau hoi cua USE method, moi o mot lenh cu the

Bảng lệnh cho ba tài nguyên chính:

Tài nguyênUtilizationSaturationErrors
CPU%Cpu(s) trong top, mpstat -P ALL 1cột r của vmstat, nr_throttled trong cpu.statst cao, log máy chủ
Bộ nhớfree -h cột availablesi/so của vmstat, mức swap dùngdmesg tìm oom-kill
Lưu trữ%util trong iostat -xawait, aqu-sz, cột b của vmstatdmesg tìm lỗi thiết bị

Thứ tự chạy khi mới ssh vào một máy lạ: uptime để biết có xếp hàng không, vmstat 1 để chia CPU với I/O, free -h để biết còn bộ nhớ không, iostat -x 1 nếu nghi lưu trữ, dmesg -T | tail để xem kernel có ghi gì không. Năm lệnh, chưa tới một phút.

💡 Gợi ý

Đây là câu hỏi định hướng, không phải lời giải. Trả lời được chúng thì bạn tự ra kết luận.

Về ca A. Load 24 trên 8 lõi nghĩa là có xếp hàng — nhưng xếp hàng chờ ai? Cột r và cột b của vmstat được thiết kế đúng để trả lời câu đó, hãy đọc chúng trước khi nhìn bất cứ thứ gì khác. Sau đó, với await trên một ổ SATA, hãy tự hỏi thời gian phục vụ một thao tác của loại thiết bị này thường nằm ở thang nào, và phần chênh lệch với con số trong bảng thì đang là gì.

Về ca B. Container báo load 1,24 trong khi hạn ngạch là bao nhiêu lõi — hai con số đó có mâu thuẫn với triệu chứng p99 nhọn không? Trước khi kết luận, hãy tính hai đại lượng từ cpu.stat: tỉ lệ chu kỳ bị treo, và độ dài trung bình một lần treo. Rồi so đại lượng thứ hai với ngân sách độ trễ của một request API. Câu hỏi cuối: 32 thread chia nhau một hạn ngạch tính theo chu kỳ 100 mili-giây thì suất ấy hết trong bao lâu?

Về ca C. Có đúng một trường trong dòng oom-kill phân biệt "máy hết RAM" với "một nhóm chạm trần của nó" — tìm nó trước. Sau đó so anon-rss của nạn nhân với memory.max của container, và tự hỏi ngoài heap thì một tiến trình JVM còn tiêu bộ nhớ ở những chỗ nào. Câu hỏi phụ, để tự kiểm tra xem bạn đã hiểu đúng chưa: vì sao không có OutOfMemoryError nào trong log?

Về ca D. Hai cảnh báo đỏ đến từ hai phép tính; hãy kiểm tra từng phép tính trước khi tin kết luận của nó. Với cảnh báo RAM: cột available của free nói gì so với tổng RSS, và bạn đã học điều gì về việc cộng RSS của nhiều tiến trình? Với cảnh báo đĩa: await 0,3 mili-giây có nhất quán với "thiết bị quá tải" không? Và cuối cùng, trong header của top có đúng một trường nào bất thường mà không cảnh báo nào đang bật cho nó?

Công thức để tự thay số:

so chu ky bi treo / tong so chu ky      = ty le bi treo
tong thoi gian treo / so lan treo       = do dai trung binh moi lan treo
(r/s + w/s) x await                     ~= aqu-sz   (kiem tra cheo)
load average / so loi                   = muc xep hang tren moi loi

✅ Lời giải

Ca A — nghẽn ở lưu trữ

Bằng chứng. vmstat cho r bằng 1 và b bằng 22: chỉ một tiến trình cần CPU, còn hai mươi hai tiến trình đang kẹt trong chờ không ngắt được. Load 24 gần như hoàn toàn đến từ cột b, khớp với id 88,3% trong top. Trên sda, await 145 mili-giây trong khi một ổ SATA khoẻ mạnh phục vụ một thao tác trong khoảng vài tới trên chục mili-giây; phần còn lại là thời gian nằm chờ. Kiểm chéo bằng quan hệ ở bài 03: (18 + 240) × 0,1452 s ≈ 37, khớp cột aqu-sz 37,4. Nghĩa là trung bình có gần bốn mươi yêu cầu nằm trong hệ thống cùng lúc, trên một thiết bị chỉ phục vụ được một cái một lúc — hàng đợi dài đúng bằng chỗ 145 mili-giây kia sinh ra.

Sửa. Giảm lượng ghi ngẫu nhiên (gộp bước build, dùng cache artifact), hoặc đổi sang NVMe. Con số w/s 240 với chỉ 28,6 MB/s cho thấy đây là ghi nhỏ và rải rác, đúng thứ ổ quay ghét nhất.

Vì sao phản xạ thường gặp lại vô ích. Thấy load 24 trên 8 lõi, phản xạ là thêm CPU hoặc tăng số job chạy song song. Cả hai đều làm tệ hơn: CPU đang rảnh 88%, còn thêm job song song nghĩa là đẩy thêm yêu cầu vào một hàng đợi vốn đã dài.

Ca B — nghẽn ở hạn ngạch CPU của cgroup

Bằng chứng. cpu.max100000 100000, tức đúng một lõi cho cả nhóm, dù node có 32 lõi và ứng dụng dựng 32 thread. nr_throttled / nr_periods = 318 / 1200 ≈ 26,5% số chu kỳ bị treo. Độ dài trung bình mỗi lần treo là 4.238.000 / 318 ≈ 13.327 micro-giây, tức khoảng 13 mili-giây — với một API có ngân sách vài chục mili-giây thì đó là khoản cộng thêm thấy rõ trên p99. Lưu trữ sạch sẽ: await 0,4 mili-giây và aqu-sz 0,1 loại hẳn giả thuyết I/O.

Load 1,24 không hề mâu thuẫn: với hạn ngạch một lõi, load quanh 1 nghĩa là suất đã dùng hết, và 32 thread cùng chạy đốt hết suất của một chu kỳ chỉ trong khoảng ba mili-giây đầu.

Sửa. Hoặc nới hạn ngạch cho đúng nhu cầu đỉnh, hoặc giảm số thread cho khớp hạn ngạch để công việc trải đều trong chu kỳ thay vì dồn cục. Với dịch vụ nhạy độ trễ, không ít đội còn chọn bỏ hẳn trần CPU và chỉ giữ phần bảo đảm tối thiểu, đổi rủi ro hàng xóm ồn ào lấy đuôi trễ ổn định hơn.

Vì sao phản xạ thường gặp lại vô ích. Biểu đồ mức dùng CPU trung bình trông ổn, nên phản xạ là đi tìm truy vấn chậm trong ứng dụng. Nhưng chỗ chậm không nằm trong code: nó nằm ở những khoảng 13 mili-giây mà cả nhóm không được chạy.

Ca C — OOM ở cấp cgroup

Bằng chứng. constraint=CONSTRAINT_MEMCG cùng task_memcg trỏ vào một scope của Docker: container chạm memory.max của riêng nó, máy chủ có thể còn trống rất nhiều. anon-rss khoảng 2 GB, đúng bằng trần 2 GB. ExitCode 137128 + 9, chữ ký của SIGKILL.

Sửa. Hạ -Xmx xuống khoảng 1,4 tới 1,5 GB (chừng 70–75% trần), hoặc để JVM tự đọc giới hạn cgroup bằng tuỳ chọn đặt heap theo phần trăm bộ nhớ khả dụng. Nếu ứng dụng thật sự cần 2 GB heap thì phải nâng memory.max lên khoảng 2,7 GB, chứ không phải hạ mỗi heap.

Vì sao không có OutOfMemoryError. Trần bị chạm ở mức cgroup, không phải ở mức heap: JVM chưa bao giờ rơi vào tình huống hết heap để mà ném exception hay ghi heap dump. Ngoài heap, tiến trình còn tiêu bộ nhớ cho metaspace, ngăn xếp mỗi thread, vùng chứa mã đã biên dịch và bộ đệm ngoài heap — chính phần đó đẩy tổng vượt trần.

Ca D — cả hai cảnh báo đều là báo động giả; thủ phạm là st

Bằng chứng. Cảnh báo RAM dựng trên phép cộng RSS toàn máy, mà RSS tính trọn mọi trang chia sẻ cho từng tiến trình dùng nó, nên 30,8 GB là con số đếm trùng. Chỉ số đúng nằm ngay cạnh: available 6,2 GB, và swap dùng 0 byte. Máy không hề thiếu bộ nhớ.

Cảnh báo đĩa dựng trên %util 99%, nhưng await 0,3 mili-giây và aqu-sz 0,4 nói rằng trung bình còn chưa tới một yêu cầu nằm trong hệ thống. Thiết bị hiếm khi được nghỉ, và cũng chẳng ai phải chờ nó.

Thứ thật sự bất thường không có cảnh báo nào: st bằng 24 trong header của top. Gần một phần tư thời gian CPU mà máy ảo này đáng được dùng đã bị hypervisor cấp cho hàng xóm.

Sửa. Đây là vấn đề hạ tầng, không phải vấn đề của ứng dụng: đổi sang loại instance có CPU riêng, dời máy ảo sang máy chủ vật lý khác, hoặc làm việc với nhà cung cấp. Đồng thời sửa hai cảnh báo: đổi cảnh báo RAM sang available cùng mức swap, và đổi cảnh báo đĩa sang await cùng aqu-sz.

Vì sao phản xạ thường gặp lại vô ích. Với hai đèn đỏ đang nhấp nháy, phản xạ là nâng cấp RAM và đổi ổ đĩa. Tiền tiêu xong mà máy vẫn chậm y hệt, vì st không đổi. Một cảnh báo sai đắt hơn không có cảnh báo: nó dẫn cả nhóm đi sai hướng, mà lại rất tự tin.

🎓 Mở rộng

  • Tự dựng ca A trên máy mình. Chạy fio với --rw=randwrite --bs=4k --iodepth=32 trên một ổ chậm rồi mở vmstat 1 ở cửa sổ khác. Bạn sẽ thấy cột b phình lên trong khi cột r đứng yên, và load average leo dốc dù CPU rảnh.
  • Đo PSI trên máy của bạn. Đọc /proc/pressure/cpu/proc/pressure/io trong lúc chạy tải nặng. Đối chiếu chỉ số some với full — bạn sẽ thấy nó nói rõ hơn load average đúng ở chỗ nào.
  • Kiểm lại bộ cảnh báo đang chạy. Ca D không phải chuyện hiếm: rất nhiều hệ thống giám sát vẫn cảnh báo theo tổng RSS và theo %util. Rà lại xem đội bạn có đang đặt đèn đỏ trên hai chỉ số đó không.
  • Viết runbook năm lệnh. Ghi lại thứ tự uptime, vmstat 1, free -h, iostat -x 1, dmesg -T | tail kèm một dòng "nếu thấy X thì rẽ sang nhánh Y". Runbook viết lúc bình yên đáng giá gấp nhiều lần lúc 3 giờ sáng.

✨ Điều bạn vừa làm được

Bạn vừa phân xử bốn ca mà ba trong số đó có ít nhất một chỉ số chỉ sai hướng: load average cao ở ca A không phải chuyện CPU, biểu đồ CPU đẹp ở ca B che mất chuyện treo theo chu kỳ, và hai đèn đỏ ở ca D hoàn toàn là ảo. Kỹ năng thật sự không nằm ở việc thuộc tên các cột, mà ở phản xạ hỏi chỉ số này đang đo cái gì trước khi hành động theo nó.

Đó cũng là thứ khép lại cả khoá: bạn đã biết dữ liệu nằm ở đâu, đi đường nào, và bây giờ là cách đo xem nó đang tắc ở chỗ nào.

Bài tiếp theo: Tổng kết course & track — Máy tính cho Lập trình viên

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Tổng kết course & track — Máy tính cho Lập trình viên