Linux cho DE 6 — Tiến trình, job & giám sát tài nguyên

13 thg 7, 2026 3 lượt xem
#monitoring
#devops
#linux
#htop
#process

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 & redirectLinux 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ệuTênÝ nghĩa
RRunning / RunnableĐang chạy trên CPU hoặc sẵn sàng chạy (trong hàng đợi)
SInterruptible sleepĐang ngủ chờ sự kiện (I/O mạng, chờ input) — trạng thái phổ biến nhất
DUninterruptible sleepĐang chờ I/O đĩa/thiết bị, không thể bị interrupt hay kill
ZZombieĐã chết nhưng cha chưa "thu hoạch" mã thoát
TStoppedBị 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 -9 cũ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ọi wait() để đọ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ủa nohup/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.55 là 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. wa cao (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ên free thấp không đồng nghĩa hết RAM. Con số cần soi là available (xem free -h bê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ệuHành viBắt được?
15SIGTERMYêu cầu kết thúc lịch sự (mặc định của kill)Có — app có thể cleanup
9SIGKILLGiết cưỡng bức, kernel làm ngayKhông — không cleanup
2SIGINTNgắt (chính là Ctrl-C)
1SIGHUP"Hang up" — gửi khi terminal đóng
19SIGSTOPTạm dừngKhông
18SIGCONTTiế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: R chạy, S ngủ chờ sự kiện, D chờ I/O không kill được, Z zombie (giết cha, không giết zombie), T dừng.
  • Zombie = con chết cha chưa reap; orphan = cha chết trước con. nohup/disown/setsid cố 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; wa cao = 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 -9 giữa lúc ghi Delta/Parquet để lại file cụt.
  • pkill -f / killall giết theo tên — luôn pgrep -a xem trước trên server dùng chung.
  • Job control: & chạy nền, Ctrl-Z dừng, bg/fg/jobs. Job gắn với terminal, chết theo SIGHUP khi đóng — dùng nohup/disown/setsid (hoặc tốt hơn: tmux/systemd).
  • Tài nguyên: free -h xem cột available (không phải free); vmstat 2 phân loại nghẽn CPU vs I/O/swap; iostat -x, df -h, du -sh cho đĩ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.

13 thg 7, 2026 4

Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.

13 thg 7, 2026 3

Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.

13 thg 7, 2026 3

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á.

13 thg 7, 2026 3