Hiểu bộ lập lịch Linux để viết ứng dụng nặng về CPU hiệu quả
15 tháng 6, 2026
Khi một tiến trình chiếm trọn lõi CPU, nhiều lập trình viên nghĩ ngay đến tối ưu thuật toán hoặc thêm worker. Hai hướng đó đúng, nhưng còn một lớp quyết định nằm giữa mã nguồn và phần cứng: bộ lập lịch tiến trình của hệ điều hành. Trên Linux hiện đại, lớp này ảnh hưởng trực tiếp đến độ trễ, thông lượng, mức tiêu thụ điện và cả hành vi của container trong môi trường production.
Scheduler làm gì trong Linux
Trong Linux, scheduler không quyết định mã nào nhanh hơn, mà quyết định tiến trình hoặc luồng nào được đưa lên CPU, trong bao lâu, và khi nào bị tạm dừng để tiến trình khác chạy. Mỗi lần chuyển như vậy gọi là context switch. Nếu context switch quá nhiều, hệ thống mất thời gian lưu và phục hồi trạng thái, bộ nhớ cache bị dội, kết quả là ứng dụng nhìn có vẻ bận nhưng throughput không tăng.
Ngược lại, nếu một tác vụ chiếm CPU quá lâu, các tiến trình khác có thể bị trễ, nhất là tiến trình xử lý network, logging hoặc health check. Vì vậy scheduler là cơ chế điều hòa công bằng giữa nhiều mục tiêu: công bằng, độ trễ thấp, tận dụng nhân, tiết kiệm năng lượng và ưu tiên nghiệp vụ.
CFS không chia thời gian theo slot cố định
Linux hiện nay dùng Completely Fair Scheduler, thường gọi là CFS. Ý tưởng cốt lõi không phải chia CPU thành các lát thời gian cố định cho mọi tiến trình, mà theo dõi thời gian CPU đã nhận dưới dạng virtual runtime, hay vruntime. Tiến trình có vruntime nhỏ hơn thường được chọn chạy trước, nhằm bù lại những tiến trình chưa được phục vụ đủ.
Điều này giúp hệ thống linh hoạt hơn so với mô hình round-robin đơn giản. Một tiến trình xử lý I/O thường ngủ nhiều, khi tỉnh dậy sẽ được chạy nhanh để phản hồi người dùng. Một tiến trình tính toán liên tục vẫn được chia sẻ CPU, nhưng sẽ cạnh tranh dựa trên trọng số ưu tiên thay vì bị khóa cứng vào một khoảng thời gian.
Nice, priority và CPU affinity
Giá trị nice là cách phổ biến nhất để nói với scheduler rằng tiến trình này nên được đối xử hào phóng hơn hoặc ít hào phóng hơn. Nice càng cao, tiến trình càng dễ nhường CPU. Nice không phải là bảo đảm tuyệt đối, nhưng rất hữu ích cho job nền như nén file, tạo báo cáo, huấn mô hình nhỏ hoặc đồng bộ dữ liệu.
CPU affinity lại là ràng buộc vị trí. Khi bạn gắn một tiến trình vào lõi cụ thể, scheduler chỉ chọn các lõi đó. Kỹ thuật này có thể giảm dao động cache, tránh nhiễu từ tác vụ khác, hoặc phục vụ hệ thống thời gian thực mềm. Tuy nhiên, dùng affinity sai cách có thể tạo nút cổ chai: hai tác vụ nặng bị ép vào cùng lõi trong khi các lõi khác rảnh.
# Chạy tiến trình với độ ưu tiên thấp hơn
nice -n 5 python worker.py
# Gắn tiến trình vào lõi 2 và 3
taskset -c 2-3 python worker.py
# Theo dõi luồng bên trong tiến trình Python
top -H -p $(pgrep -n python)
Khi lập trình ứng dụng nặng về CPU
Với tác vụ CPU-bound, lỗi thường gặp là tạo quá nhiều worker. Trên máy có 8 lõi vật lý, việc khởi động 64 luồng tính toán không làm công việc nhanh hơn 8 lần. Nó chỉ tăng cạnh tranh, tăng context switch và làm cache CPU bẩn hơn. Với mã Python, Node.js worker, Java thread pool hoặc Go goroutine xử lý tính toán thuần túy, số worker nên bắt đầu gần số lõi khả dụng, sau đó được tinh chỉnh bằng đo đạc.
Chiến lược tốt hơn là chia bài toán thành batch vừa phải. Batch quá nhỏ khiến overhead điều phối lớn. Batch quá lớn khiến một worker chạy lâu, khó cân bằng tải và khó dừng khi cần scale down. Trong xử lý ảnh, mã hóa video, tính điểm rủi ro hoặc sinh dữ liệu giả lập, kích thước batch thường là tham số nên thử nghiệm thay vì chọn cố định.
import os
from concurrent.futures import ProcessPoolExecutor
def ham_nang_cpu(x):
return sum(i * i for i in range(x))
if __name__ == '__main__':
cores = os.cpu_count() or 1
workers = max(1, cores - 1)
jobs = range(1000)
with ProcessPoolExecutor(max_workers=workers) as pool:
results = list(pool.map(ham_nang_cpu, jobs))
Đoạn mã trên trừ đi một lõi để hệ điều hành còn chỗ xử lý network, logging hoặc các tiến trình nền. Đây không phải công thức vạn năng, nhưng là điểm bắt đầu hợp lý. Trên container, os.cpu_count() có thể trả về số lõi của node thay vì giới hạn của container nếu môi trường runtime hoặc base image chưa hỗ trợ cgroup đầy đủ. Vì vậy, trong production nên truyền số worker qua biến môi trường và kiểm tra bằng thực nghiệm.
Container, Kubernetes và giới hạn CPU
Trong container, scheduler Linux của host vẫn là người phân CPU thật, còn cgroup đóng vai trò giới hạn ngân sách CPU cho container. Kubernetes CPU limit thường được triển khai bằng CFS quota. Ví dụ limit 1 CPU có nghĩa container được dùng khoảng 100ms CPU trong mỗi chu kỳ 100ms. Nếu dùng hết quota, nó có thể bị throttle dù host còn lõi rảnh.
Hiện tượng này khiến nhiều người hiểu nhầm: pod có vẻ ít CPU nhưng latency vẫn tăng. Nguyên nhân không phải scheduler lười, mà là container bị giới hạn ngân sách. Với service CPU-bound nhạy độ trễ, bạn nên cân nhắc đặt request sát nhu cầu, tránh limit quá thấp, chạy nhiều replica, hoặc dùng node riêng nếu workload cần ổn định.
resources:
requests:
cpu: 500m
limits:
cpu: 1
Trong Kubernetes, nếu workload cần giảm nhiễu, có thể xem xét CPU Manager static policy, exclusive CPU assignment hoặc node affinity. Các lựa chọn này tăng độ phức tạp vận hành, nên chỉ dùng khi đo đạc chứng minh rằng jitter do chia sẻ CPU đang gây lỗi nghiệp vụ.
Tránh nhầm lẫn phổ biến
- Scheduler không thay thế profiling. Nếu hàm tính toán dùng thuật toán bậc cao, việc chỉnh nice hay affinity chỉ che triệu chứng.
- Thread không phải lúc nào cũng song song. Trên CPU-bound workload, nhiều thread có thể chỉ làm tăng cạnh tranh.
- Load average cao không luôn có nghĩa CPU hết. Nó còn phản ánh số tiến trình đang chạy hoặc đang chờ I/O.
- Container limit không tương đương tài nguyên vật lý. Nó là quyền được dùng CPU theo chính sách của host.
- Ưu tiên cao hơn không làm tác vụ nhanh vô hạn. Nó chỉ thay đổi thứ tự và tần suất được chọn.
Quy trình kiểm tra thực tế
Khi nghi ngờ scheduler ảnh hưởng đến ứng dụng, hãy bắt đầu bằng câu hỏi định lượng: workload có thật sự CPU-bound không, hay đang chờ ổ đĩa, network, lock hoặc garbage collection? Dùng pidstat, top -H, perf và profiler ngôn ngữ để tách hai trường hợp này. Nếu CPU user cao và CPU iowait thấp, scheduler mới là ứng viên đáng chú ý.
pidstat -u -p ALL 1
perf top -p <pid>
Sau đó thử ba thay đổi nhỏ: giảm số worker xuống gần số lõi, đặt nice phù hợp cho job nền, và so sánh kết quả với hoặc không có CPU affinity. Mỗi lần chỉ đổi một biến, chạy đủ lâu để tránh nhiễu, rồi ghi lại p50, p95, p99 latency cùng throughput. Nếu p99 giảm nhưng throughput không đổi, bạn có thể đã giảm jitter. Nếu throughput tăng nhưng p99 xấu đi, hệ thống có thể đang hy sinh độ ổn định để đổi lấy năng suất.
Kết luận
Hiểu scheduler Linux giúp lập trình viên đưa ra quyết định thực tế hơn khi mở rộng ứng dụng nặng về CPU. Nó không biến mã chậm thành mã nhanh, nhưng giúp bạn chọn số worker, cấu hình container, đặt ưu tiên và đo kết quả đúng cách. Lần tới khi thấy CPU tăng mà dịch vụ vẫn chậm, đừng chỉ nhìn biểu đồ. Hãy hỏi tiến trình nào đang chạy, trên lõi nào, bị giới hạn ra sao, và scheduler đang cố công bằng với ai.