Một tiến trình không được dùng tài nguyên vô hạn. Hệ điều hành đặt cho mỗi tiến trình một loạt giới hạn tài nguyên (resource limits, hay rlimit): số file mở tối đa, số tiến trình con, bộ nhớ, thời gian CPU. Khi chạm giới hạn, chương trình nhận một lỗi — và những lỗi đó thường bị chẩn đoán sai thành "hết tài nguyên". Tôi đo trong container để thấy rõ ranh giới, và dựng lại đúng lỗi "Too many open files" khét tiếng.
Giới hạn mềm và giới hạn cứng
Mỗi loại giới hạn có hai mức: giới hạn mềm (soft) là giá trị đang được ép lúc này, và giới hạn cứng (hard) là trần mà giới hạn mềm có thể được nâng tới. Quy tắc phân quyền rất rõ:
- Một tiến trình thường (không đặc quyền) có thể nâng giới hạn mềm lên tới bằng giới hạn cứng, và có thể hạ cả hai.
- Nhưng nó không bao giờ nâng được giới hạn cứng — chỉ tiến trình có quyền (
CAP_SYS_RESOURCE) mới làm được. - Hệ quả: giới hạn cứng là một chốt một chiều. Một khi bạn hạ nó xuống, bạn không nâng lại được.
Tôi đo bằng setrlimit/getrlimit để kiểm chứng từng quy tắc, tập trung vào giới hạn hay gây rắc rối nhất: RLIMIT_NOFILE, số file descriptor (fd) tối đa một tiến trình được mở.
Đo: EMFILE ở đúng ngưỡng, dù RAM còn thừa
Trong container, giới hạn mặc định rất rộng: soft = hard = 1.048.576. Tôi cố tình hạ giới hạn mềm xuống 20, rồi mở /dev/null liên tục cho tới khi thất bại:
Hạ soft NOFILE xuống 20 (hard giữ 1.048.576):
mở được 17 fd rồi thất bại: Too many open files (errno = 24)
Mở được đúng 17 fd rồi lỗi. Vì sao 17? Vì mỗi tiến trình khởi động đã có sẵn ba fd: 0 (stdin), 1 (stdout), 2 (stderr). Cộng 17 fd mới là 20 — chạm đúng giới hạn. open() thứ 20 trả về -1 với errno = 24 (EMFILE, "Too many open files"). strace xác nhận: các openat trả về fd 3, 4, 5... rồi cái vượt ngưỡng trả EMFILE.
Điểm mấu chốt: lúc này máy còn thừa gần 8GB RAM và đĩa chưa đầy. Lỗi "Too many open files" không liên quan gì tới bộ nhớ hay dung lượng đĩa. Nó là một giới hạn chính sách mỗi tiến trình, một cái van do hệ điều hành đặt để một tiến trình lỗi (rò rỉ fd) không thể mở vô hạn file làm sập cả hệ thống. Tôi đụng nó ở đúng con số cấu hình, giữa lúc tài nguyên vật lý còn dư dả.
Rồi tôi kiểm các quy tắc mềm/cứng:
Nâng soft lên = hard (1.048.576): OK (tiến trình thường làm được)
Nâng HARD lên gấp đôi : EPERM (cấm) (không có quyền)
Hạ hard xuống 100, rồi nâng lại 200: EPERM (chốt một chiều)
Đúng như lý thuyết: nâng mềm tới cứng thì được, nâng cứng thì bị từ chối (EPERM), và một khi hạ cứng thì không nâng lại được.
Một lần tôi đo hớ: "hết file" khi chưa hết gì cả
Cái bẫy ở đây là chẩn đoán. Lần đầu gặp "Too many open files" trong một hệ thống thật, phản xạ tự nhiên của nhiều người (và của tôi) là nghĩ tới cạn kiệt tài nguyên vật lý: hết bộ nhớ? đầy đĩa? máy quá tải? Nhưng phép đo cho thấy điều đó sai về bản chất: tôi đụng EMFILE với hàng gigabyte RAM còn trống. Con số "hết" trong khi tài nguyên còn dư chính là dấu hiệu đây là chính sách, không phải vật lý — một cái trần do cấu hình đặt ra, không phải giới hạn của phần cứng.
Đây là một dạng "số bất khả tố cáo": một lỗi "hết tài nguyên" trong khi tài nguyên rõ ràng còn thừa là điều vô lý nếu bạn tin lỗi đó nói về vật lý — và sự vô lý đó chỉ thẳng vào nguyên nhân thật: một giới hạn rlimit. Sửa nó không phải bằng cách thêm RAM hay dọn đĩa, mà bằng cách nâng giới hạn mềm (ulimit -n hay setrlimit) hoặc bịt chỗ rò rỉ fd trong code (quên close()). Bài học đo lường: đọc đúng nguyên nhân từ errno, đừng đoán "hết tài nguyên" khi một con số cụ thể đang chặn bạn ở một ngưỡng cố định.
Giới hạn CPU: một tín hiệu thay vì một lỗi
Không phải rlimit nào cũng báo bằng mã lỗi trả về. RLIMIT_CPU — số giây thời gian CPU một tiến trình được dùng — hoạt động khác: khi tiến trình vượt giới hạn mềm, nhân gửi tín hiệu SIGXCPU (chứ không phải trả lỗi từ một syscall), cho tiến trình một cơ hội tự dọn dẹp và thoát; nếu nó phớt lờ và tiếp tục chạy tới giới hạn cứng, nhân dùng SIGKILL giết hẳn. Đây là một cơ chế đẹp để chặn các tiến trình chạy vô hạn (một vòng lặp lỗi, một truy vấn quá nặng) mà không cần giám sát bên ngoài: đặt RLIMIT_CPU và bạn có một cái đồng hồ hẹn giờ tự động. Điểm đáng nhớ là cách giới hạn được thực thi khác nhau tùy loại — có cái trả errno ngay tại syscall (NOFILE→EMFILE, NPROC→EAGAIN), có cái trả NULL (bộ nhớ), có cái gửi tín hiệu (CPU) — nên khi một tiến trình "chết bí ẩn" hay "báo lỗi lạ", việc biết loại rlimit nào phát tín hiệu gì giúp lần ra nguyên nhân nhanh hơn nhiều.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên: biết các giới hạn hay chạm và mã lỗi của chúng. RLIMIT_NOFILE → EMFILE ("Too many open files") khi mở quá nhiều fd — cực phổ biến với server nhiều kết nối, vì mỗi socket là một fd. RLIMIT_NPROC → fork trả EAGAIN ("Resource temporarily unavailable") khi tạo quá nhiều tiến trình — đây là cái chặn một fork bomb. RLIMIT_AS/RLIMIT_DATA giới hạn bộ nhớ ảo → malloc trả NULL hay mmap trả ENOMEM. RLIMIT_CPU → nhân gửi SIGXCPU khi tiến trình dùng quá số giây CPU cho phép. RLIMIT_CORE → giới hạn cỡ file core dump (đặt 0 để tắt hẳn dump). Khi gặp một trong các lỗi này, nghĩ tới rlimit trước khi nghĩ tới cạn kiệt vật lý.
Hệ quả thứ hai: nâng giới hạn cho đúng chỗ. Một server web/CSDL thường cần RLIMIT_NOFILE cao hơn mặc định (mỗi kết nối một fd). Nâng nó phải làm ở đúng nơi: qua ulimit -n trong shell khởi động, hay LimitNOFILE= trong unit của systemd, hay setrlimit trong chính chương trình lúc khởi động — và chỉ nâng được giới hạn mềm tới giới hạn cứng; muốn vượt cứng thì cần chỉnh cấu hình hệ thống với quyền quản trị. Nhớ rằng đây là giới hạn mỗi tiến trình, không phải toàn hệ thống (giới hạn toàn hệ thống là /proc/sys/fs/file-max, một con số khác).
Hệ quả thứ ba là bài học đo lường: một lỗi "hết X" khi X còn dư nghĩa là bạn đang đụng một chính sách, không phải một bức tường vật lý. Con số mang theo: rlimit là giới hạn tài nguyên mỗi tiến trình do nhân ép, có giới hạn mềm (đang ép) và giới hạn cứng (trần, chỉ hạ được với tiến trình thường); vượt RLIMIT_NOFILE cho ra EMFILE "Too many open files" ngay cả khi RAM còn thừa — đó là chính sách, không phải cạn kiệt bộ nhớ. Chẩn đoán từ errno và /proc/<pid>/limits, đừng đổ lỗi cho phần cứng.
Thử ba mươi giây
Gõ ulimit -n để xem giới hạn mềm số fd của shell bạn, và ulimit -Hn để xem giới hạn cứng. Rồi xem toàn bộ giới hạn của một tiến trình đang chạy: cat /proc/<pid>/limits — một bảng đẹp liệt kê soft/hard của mọi loại rlimit. Thử tự chặn mình: bash -c 'ulimit -n 5; exec cat /etc/hostname' sẽ báo "Too many open files" vì cat không mở nổi file với chỉ 5 fd. Đó chính là lỗi bài này đo, tự tạo trong ba giây — và nó nhắc rằng khi thấy lỗi đó ở production, câu hỏi đầu tiên không phải "hết RAM chưa" mà là "ulimit -n của tiến trình đó là bao nhiêu, và nó có rò rỉ fd không".