Linux cho DE 6 — Tiến trình, job & giám sát tài nguyên
Vì sao DE ngân hàng phải hiểu tiến trình
Một job ETL nạp dữ liệu core banking chạy lúc 2 giờ sáng, đến 6 giờ vẫn chưa xong. Một Spark job đọc bảng transactions 200 triệu dòng bỗng bị kernel giết vì hết RAM. Một script Python reconcile.py treo và giữ khư khư 30 GB bộ nhớ trên server dùng chung với 5 người khác. Trong cả ba tình huống, thứ bạn cần không phải là log của ứng dụng — mà là khả năng nhìn thẳng vào tiến trình đang chạy trên máy: nó là PID nào, ngốn bao nhiêu CPU/RAM, ở trạng thái gì, và làm sao kết thúc nó mà không hỏng dữ liệu.
Bài này là bộ công cụ điều tra hiện trường của data engineer. Nó nối tiếp Linux 4 — Pipe & redirect và Linux 5 — Quyền & SSH, và chuẩn bị nền cho Linux 7 — Lập lịch & systemd nơi ta biến các job thủ công thành dịch vụ được quản lý.
Tiến trình là gì
Một tiến trình (process) là một chương trình đang chạy: một bản sao code được nạp vào bộ nhớ, có không gian địa chỉ riêng, file mô tả (file descriptor), biến môi trường và một định danh số gọi là PID (Process ID). Mỗi tiến trình (trừ init/systemd PID 1) có một tiến trình cha, tham chiếu bằng PPID (Parent PID).
Khi bạn gõ python etl.py trong shell, shell (bash) fork ra một tiến trình con rồi exec Python trong đó. Bash là cha, Python là con. Hiểu quan hệ cha–con này rất quan trọng: nó giải thích vì sao đóng terminal có thể giết job (con bị "mồ côi" hoặc nhận tín hiệu), và vì sao ta cần nohup/disown.
Trạng thái tiến trình
Kernel gắn cho mỗi tiến trình một trạng thái. Cột STAT/S trong ps cho biết:
| Ký hiệu | Tên | Ý nghĩa |
|---|---|---|
R | Running / Runnable | Đang chạy trên CPU hoặc sẵn sàng chạy (trong hàng đợi) |
S | Interruptible sleep | Đang ngủ chờ sự kiện (I/O mạng, chờ input) — trạng thái phổ biến nhất |
D | Uninterruptible sleep | Đang chờ I/O đĩa/thiết bị, không thể bị interrupt hay kill |
Z | Zombie | Đã chết nhưng cha chưa "thu hoạch" mã thoát |
T | Stopped | Bị dừng (do Ctrl-Z hoặc tín hiệu SIGSTOP) |
ps còn thêm ký tự phụ: s (session leader), + (foreground), l (multi-thread), < (ưu tiên cao)... Ví dụ Sl+ nghĩa là: đang ngủ, đa luồng, chạy foreground.
Hai trạng thái hay gây hiểu lầm với DE:
D(uninterruptible sleep): một tiến trình đang đọc/ghi đĩa hay NFS mà thiết bị chậm sẽ kẹt ởD.kill -9cũng không giết được cho tới khi I/O hoàn tất. Nếu Spark executor đứng im ởD, gần như chắc chắn là bottleneck I/O (đĩa đầy, NFS treo), không phải deadlock CPU.Z(zombie): tiến trình con đã kết thúc, nhưng cha chưa gọiwait()để đọc mã thoát. Zombie không ngốn CPU/RAM (chỉ giữ một entry trong bảng tiến trình). Bạn không thể kill zombie — phải giết hoặc sửa tiến trình cha. Nhiều zombie tích tụ = cha bị lỗi không reap con.
Zombie vs Orphan
Đừng nhầm hai khái niệm:
- Zombie: con chết trước, cha còn sống nhưng chưa reap. Con thành xác zombie chờ cha thu hoạch.
- Orphan (mồ côi): cha chết trước con. Con vẫn chạy nhưng mất cha; kernel "nhận nuôi" nó cho PID 1 (
systemd/init), lúc này PPID của nó đổi thành 1. Đây chính là cơ chế nền tảng củanohup/disown— ta cố tình biến job thành orphan để nó sống sót khi terminal đóng.
Xem tiến trình: ps, pgrep, pstree
ps — ảnh chụp tại một thời điểm
ps chụp một khoảnh khắc (snapshot), không cập nhật realtime. Hai "phương ngữ" hay dùng:
ps aux # kiểu BSD: mọi tiến trình, kèm %CPU %MEM
ps -ef # kiểu System V: đầy đủ, hiển thị PPID rõ ràng
Đọc ps aux: các cột USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND. Trong đó RSS (Resident Set Size, KB) là RAM vật lý thực đang dùng — con số bạn quan tâm khi soi OOM. %CPU trong ps aux là trung bình trên toàn vòng đời tiến trình, không phải "ngay lúc này" (khác với top).
Lọc theo nhu cầu điều tra:
# Top 10 tiến trình ngốn RAM nhất
ps aux --sort=-%mem | head -n 11
# Top 10 ngốn CPU nhất
ps aux --sort=-%cpu | head -n 11
# Chỉ tiến trình của user 'deuser', chọn cột gọn
ps -u deuser -o pid,ppid,stat,rss,etime,cmd
etime (elapsed time) cực hữu ích: cho biết job đã chạy bao lâu — một ETL etime ghi 04:12:33 mà bình thường xong trong 20 phút là dấu hiệu treo.
pgrep / pkill — tìm & giết theo tên
pgrep -a python # liệt kê PID + dòng lệnh mọi tiến trình python
pgrep -f "etl.py" # khớp toàn bộ dòng lệnh, không chỉ tên chương trình
pgrep -u deuser -f spark # spark của riêng user deuser
-f (full command line) là mấu chốt: nhiều job Python trùng tên python, chỉ phân biệt được qua tham số (etl_customers.py vs etl_txn.py). Không có -f, pgrep python không khớp vì tên tiến trình là python3, còn etl.py chỉ là đối số.
pstree — cây quan hệ cha–con
pstree -p deuser # cây tiến trình của user, kèm PID
pstree -p 4821 # cây con của PID 4821
Cực hợp với Spark: một spark-submit sinh ra JVM driver, JVM này lại fork executor và các thread. pstree cho thấy toàn bộ họ hàng, giúp bạn giết đúng gốc thay vì bỏ sót executor con.
Giám sát realtime: top & htop
ps là ảnh tĩnh; khi cần theo dõi liên tục dùng top (có sẵn mọi nơi) hoặc htop (đẹp hơn, cuộn được, cần cài).
Đọc dòng đầu của top
top - 02:14:07 up 12 days, 3:40, 2 users, load average: 8.42, 6.10, 3.55
Tasks: 312 total, 2 running, 309 sleeping, 0 stopped, 1 zombie
%Cpu(s): 71.2 us, 8.1 sy, 0.0 ni, 15.3 id, 4.9 wa, 0.0 hi, 0.5 si
MiB Mem : 64000.0 total, 1200.0 free, 58000.0 used, 4800.0 buff/cache
load average: 8.42, 6.10, 3.55— tải trung bình 1/5/15 phút. Đây là số tiến trình trung bình đang chạy hoặc chờ chạy/chờ I/O. Quy tắc ngón tay cái: so với số core. Máy 8 core mà load 8.42 nghĩa là gần bão hoà; load 16 trên 8 core = quá tải gấp đôi, job xếp hàng dài. Ba số cho thấy xu hướng:8.42, 6.10, 3.55là tải đang tăng dần (1 phút cao hơn 15 phút).%Cpu(s):us= user space,sy= kernel,id= idle,wa= I/O wait.wacao (ví dụ 40%) nghĩa CPU rảnh nhưng đang chờ đĩa — bottleneck là I/O chứ không phải tính toán. Rất hay gặp khi Spark shuffle/spill xuống đĩa.Mem: chú ýbuff/cache— Linux dùng RAM rảnh làm cache đĩa, nênfreethấp không đồng nghĩa hết RAM. Con số cần soi là available (xemfree -hbên dưới).
Cột từng tiến trình & phân biệt RES/VIRT
Trong top/htop, hai cột bộ nhớ hay gây nhầm:
- VIRT (Virtual): tổng không gian địa chỉ ảo tiến trình đăng ký — gồm cả code, thư viện chia sẻ, vùng mmap chưa dùng. JVM Spark thường báo VIRT hàng chục GB dù chưa thực sự chiếm. Đừng hoảng vì VIRT.
- RES (Resident): RAM vật lý thực sự đang chiếm (tương đương RSS của
ps). Đây mới là con số quyết định OOM.
Trong top: nhấn M sắp theo bộ nhớ, P theo CPU, k để kill (nhập PID), 1 để hiện từng core riêng, q để thoát. htop cho phép dùng chuột, mũi tên chọn tiến trình, F5 xem cây (tree view), F9 gửi tín hiệu — thân thiện hơn nhiều khi điều tra trực quan.
Gửi tín hiệu & kết thúc tiến trình
Bạn không "tắt" tiến trình — bạn gửi tín hiệu (signal) cho nó. Các tín hiệu quan trọng:
| Số | Tín hiệu | Hành vi | Bắt được? |
|---|---|---|---|
| 15 | SIGTERM | Yêu cầu kết thúc lịch sự (mặc định của kill) | Có — app có thể cleanup |
| 9 | SIGKILL | Giết cưỡng bức, kernel làm ngay | Không — không cleanup |
| 2 | SIGINT | Ngắt (chính là Ctrl-C) | Có |
| 1 | SIGHUP | "Hang up" — gửi khi terminal đóng | Có |
| 19 | SIGSTOP | Tạm dừng | Không |
| 18 | SIGCONT | Tiếp tục sau khi dừng | — |
SIGTERM trước, SIGKILL sau
Nguyên tắc vàng: luôn thử SIGTERM (15) trước, SIGKILL (9) là biện pháp cuối.
kill 4821 # gửi SIGTERM (mặc định) — cho job cơ hội đóng file, commit, flush
kill -TERM 4821 # tường minh, tương đương trên
sleep 10
kill -9 4821 # SIGKILL — chỉ khi nó không chịu chết
Vì sao quan trọng với DE: một job đang ghi Parquet/Delta hay commit transaction, nếu nhận SIGTERM sẽ chạy handler dọn dẹp — đóng file, xoá temp, rollback. Bị kill -9 giết ngang, nó không kịp cleanup: để lại file .parquet cụt, temp rác, hoặc transaction treo. Với Spark ghi vào bảng Delta (xem Delta Lakehouse), kill -9 giữa lúc commit có thể để lại file dữ liệu mồ côi chưa nằm trong transaction log. Xử lý ưu tiên SIGTERM để hệ ghi kịp hoàn tất giao dịch.
Lưu ý: tiến trình ở trạng thái D (uninterruptible) không chết kể cả kill -9 cho tới khi I/O xong — kiên nhẫn hoặc xử lý gốc I/O.
pkill & killall — giết theo tên
pkill -f "etl_customers.py" # giết mọi tiến trình khớp dòng lệnh (SIGTERM)
pkill -9 -u deuser -f spark # SIGKILL mọi spark của deuser
killall python3 # giết mọi tiến trình đúng TÊN 'python3'
Khác biệt tinh tế: pkill/pgrep khớp theo pattern (mạnh với -f); killall khớp đúng tên chương trình. Cẩn trọng trên server dùng chung — pkill -f python có thể giết cả job của đồng nghiệp. Luôn pgrep -a xem trước khi pkill.
Job control trong shell
Job control cho phép chạy nhiều việc trong một terminal: đẩy job xuống nền, kéo lên trước, tạm dừng.
Các thao tác cơ bản
python long_etl.py & # chạy nền ngay từ đầu, in "[1] 4821" (job 1, PID 4821)
python long_etl.py # chạy trước, rồi:
# Ctrl-Z -> dừng (Stopped), shell in "[1]+ Stopped"
bg # -> cho job vừa dừng chạy tiếp ở nền
jobs # liệt kê job của shell hiện tại
jobs -l # kèm PID
fg %1 # kéo job số 1 lên foreground
kill %1 # gửi tín hiệu bằng job spec thay vì PID
jobs chỉ thấy job của shell hiện tại — đây là điểm mấu chốt: chúng gắn với phiên terminal đó, và mặc định sẽ nhận SIGHUP khi terminal đóng.
Giữ job sống sau khi logout: nohup, disown, setsid
Vấn đề kinh điển: bạn SSH vào server, chạy python etl.py &, đóng laptop — job chết vì shell nhận SIGHUP và forward xuống các job con. Ba cách giải quyết:
# 1) nohup: chặn SIGHUP + tự chuyển output ra file
nohup python etl.py > etl.log 2>&1 &
# stdout/stderr mặc định đổ vào nohup.out nếu không redirect
# 2) disown: gỡ job đang chạy ra khỏi bảng job của shell
python etl.py &
disown -h %1 # -h: chỉ chặn SIGHUP, vẫn giữ trong 'jobs'
disown %1 # gỡ hẳn khỏi 'jobs' -> shell không HUP nó nữa
# 3) setsid: tạo session mới, tách hẳn khỏi terminal (trở thành orphan của PID 1)
setsid python etl.py > etl.log 2>&1
Khác biệt thực dụng:
nohup— quyết định trước khi chạy, tiện nhất cho ETL thủ công dài. Nhớ redirect output.disown— cứu cánh khi đã lỡ chạy job không nohup và mới nhận ra sắp phải logout.setsid— tách triệt để (session mới, PPID về 1), gần với cách một daemon thật khởi động.
Với job production nên dùng tmux/screen (session bền vững để quay lại xem) hoặc tốt hơn nữa là biến thành systemd service (xem Linux 7) — chuyên nghiệp hơn nohup & rời rạc.
Giám sát tài nguyên hệ thống
Ngoài từng tiến trình, cần bức tranh toàn máy.
free -h — bộ nhớ
free -h
# total used free shared buff/cache available
# Mem: 62Gi 56Gi 1.2Gi 0.5Gi 4.8Gi 3.1Gi
# Swap: 8.0Gi 6.2Gi 1.8Gi
Cột quan trọng nhất là available — RAM thực sự còn có thể cấp cho ứng dụng mới (đã tính phần buff/cache có thể thu hồi). Đừng nhìn free. Nếu available gần 0 và swap đang bị dùng nhiều (như trên: 6.2 GB swap), máy đang chịu áp lực bộ nhớ — nguy cơ OOM killer ra tay, và job sẽ chậm khủng khiếp vì swap thrashing.
vmstat — nhịp đập hệ thống
vmstat 2 5 # in mỗi 2 giây, 5 lần
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 9 1 6300000 120000 ... ... 40 120 8200 6400 5000 9000 70 9 6 15 0
Cột đáng soi: r (tiến trình chờ CPU — cao = thiếu CPU), b (chờ I/O), si/so (swap in/out — khác 0 liên tục = đang swap, xấu), wa (I/O wait). vmstat 2 là cách nhanh nhất để phân loại: nghẽn CPU (r cao, wa thấp) hay nghẽn I/O/RAM (wa/b/so cao).
iostat & đĩa
iostat -x 2 # thống kê I/O chi tiết từng thiết bị (gói sysstat)
# %util gần 100% = đĩa bão hoà; await cao = latency đọc/ghi lớn
df -h # dung lượng đĩa còn trống theo mount point
du -sh /data/* # kích thước từng thư mục con (tìm thủ phạm ngốn đĩa)
du -sh /data/* | sort -rh | head # sắp lớn -> nhỏ
df -h cứu bạn khỏi lỗi kinh điển "No space left on device" giữa lúc Spark spill — đĩa /tmp hoặc thư mục shuffle đầy làm job chết mà log ứng dụng không nói rõ. du truy ra thư mục ngốn chỗ. uptime cho load average nhanh gọn không cần mở top.
Đọc /proc sơ lược
Mọi công cụ trên thực chất đọc từ /proc — filesystem ảo phản chiếu trạng thái kernel:
cat /proc/loadavg # load average thô
cat /proc/meminfo # chi tiết bộ nhớ
cat /proc/4821/status # trạng thái, VmRSS, threads của PID 4821
ls -l /proc/4821/fd # file descriptor tiến trình đang mở (soi rò rỉ FD)
cat /proc/4821/cmdline | tr '\0' ' ' # dòng lệnh đầy đủ đã khởi động PID
/proc/<pid>/status cho VmRSS (RAM thực) và số threads; /proc/<pid>/fd giúp phát hiện job mở quá nhiều file (lỗi "Too many open files").
Use case thực tế
Bối cảnh. 02:20 sáng, cảnh báo Airflow báo task load_transactions_daily (một spark-submit đọc bảng transactions rồi ghi Delta) chạy quá 3 giờ (bình thường 25 phút). Server dùng chung 16 core, 64 GB RAM. Bạn SSH vào điều tra.
Bước 1 — Nhìn tổng thể.
uptime # load average: 22.5, 20.1, 15.0 -> quá tải (>16 core)
free -h # available 0.9Gi, Swap used 7.5Gi -> áp lực RAM nặng, đang swap
Load 22.5 trên 16 core và swap 7.5 GB: máy đang thrashing.
Bước 2 — Ai ngốn RAM.
ps aux --sort=-%mem | head -n 6
Thấy một java ... org.apache.spark ... (Spark driver) RES = 41 GB và một python3 adhoc_join.py của user khác RES = 18 GB. Tổng vượt 64 GB → đây là nguyên nhân swap.
Bước 3 — Xác nhận trạng thái Spark.
pgrep -af spark # lấy PID driver = 30412
pstree -p 30412 # thấy driver + nhiều executor thread
ps -o pid,stat,etime,rss -p 30412 # STAT = Sl, etime = 03:05:11
Spark không treo (S chứ không D), chỉ chạy chậm vì thiếu RAM do bị job adhoc_join.py cạnh tranh.
Bước 4 — Xử lý đúng thủ phạm. Job adhoc_join.py là truy vấn ad-hoc không lịch, chiếm 18 GB làm nghẽn pipeline production. Liên hệ chủ job; nếu không phản hồi, giết lịch sự để nó cleanup:
kill -TERM 5567 # PID của adhoc_join.py
sleep 15
ps -p 5567 || echo "đã thoát" # xác nhận đã chết
Sau khi nó thoát, free -h cho thấy available bật lên 22 GB, swap giảm dần, Spark tăng tốc và hoàn tất trong 12 phút. Không đụng tới Spark driver — đúng thủ phạm mới xử lý, và ưu tiên SIGTERM để không để lại file Delta mồ côi.
Bài học rút ra. Server dùng chung cần hàng rào tài nguyên: đặt cgroups/quota, hoặc tách môi trường ad-hoc khỏi production. Trong lúc chưa có, quy trình điều tra uptime → free → ps --sort=-%mem → pstree → SIGTERM là bộ phản xạ chuẩn.
Ghi nhớ
- PID/PPID định danh tiến trình và cha nó; PID 1 (
systemd) nhận nuôi mọi orphan. - Trạng thái:
Rchạy,Sngủ chờ sự kiện,Dchờ I/O không kill được,Zzombie (giết cha, không giết zombie),Tdừng. - Zombie = con chết cha chưa reap; orphan = cha chết trước con.
nohup/disown/setsidcố tình tạo orphan để job sống qua logout. - Xem tiến trình:
ps aux --sort=-%mem,ps -ef,pgrep -af,pstree -p. Dùng-fđể khớp toàn dòng lệnh. - Realtime:
top/htop. Load average so với số core;wacao = nghẽn I/O; RES = RAM thực (quyết định OOM), VIRT chỉ là địa chỉ ảo — đừng hoảng. - Tín hiệu:
SIGTERM(15) trước để job cleanup,SIGKILL(9) là biện pháp cuối.kill -9giữa lúc ghi Delta/Parquet để lại file cụt. pkill -f/killallgiết theo tên — luônpgrep -axem trước trên server dùng chung.- Job control:
&chạy nền,Ctrl-Zdừng,bg/fg/jobs. Job gắn với terminal, chết theoSIGHUPkhi đóng — dùngnohup/disown/setsid(hoặc tốt hơn:tmux/systemd). - Tài nguyên:
free -hxem cột available (không phải free);vmstat 2phân loại nghẽn CPU vs I/O/swap;iostat -x,df -h,du -shcho đĩa;/proc/<pid>/là nguồn thô của mọi công cụ. - Phản xạ điều tra OOM/treo:
uptime → free -h → ps --sort=-%mem → pstree → SIGTERM đúng thủ phạm.
Bài viết liên quan
Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.
Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.
Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.
Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.