TL;DR — Sửa Nhanh
Tiến trình của bạn đã mở quá nhiều file (hoặc socket) mà hệ điều hành cho phép và không bao giờ đóng chúng lại. Bạn đã chạm đến giới hạn file descriptor.
Cách sửa nhanh nhất trên production: tăng giới hạn cho user đang chạy:
ulimit -n 65536
Điều đó giúp bạn có thêm không gian. Sau đó hãy sửa nguyên nhân gốc rễ trước lần deploy tiếp theo — chỉ dùng ulimit thôi sẽ không cứu được bạn mãi mãi.
Lỗi Này Trông Như Thế Nào
Traceback (most recent call last):
File "worker.py", line 47, in process
f = open(path, 'r')
OSError: [Errno 24] Too many open files
Socket cũng báo lỗi tương tự:
OSError: [Errno 24] Too many open files
File "server.py", line 112, in accept
conn, addr = self.sock.accept()
Dù trường hợp nào thì nguyên nhân gốc rễ cũng giống nhau. File descriptor (FD) được dùng chung cho file, socket, pipe và một số cơ chế IPC. Bạn đơn giản là đã dùng hết.
Nguyên Nhân Gốc Rễ
Mỗi file hoặc socket đang mở tiêu thụ một file descriptor. Linux và macOS áp đặt giới hạn mềm theo từng tiến trình — thường là 1024 trên Linux. Kiểm tra giới hạn của bạn:
ulimit -n
# hoặc
cat /proc/sys/fs/file-max # giới hạn cứng toàn hệ thống
Hai thủ phạm chiếm phần lớn các trường hợp:
- Rò rỉ FD: code mở file hoặc socket nhưng không bao giờ đóng lại. Chúng tích lũy âm thầm cho đến khi bạn chạm giới hạn.
- Cạn kiệt thực sự: một trình crawler, bộ xử lý log, hoặc connection pool thực sự cần nhiều FD hơn mức mặc định cho phép.
Kiểm tra xem tiến trình của bạn đang mở bao nhiêu FD ngay lúc này:
# thay PID bằng ID tiến trình của bạn
ls /proc/PID/fd | wc -l
# hoặc dùng lsof
lsof -p PID | wc -l
Con số tăng dần đến giới hạn mà không bao giờ giảm? Đó là rò rỉ.
Cách Sửa 1 — Ngăn Rò Rỉ (Cách Sửa Phổ Biến Nhất)
Dùng câu lệnh with. Context manager của Python sẽ gọi close() ngay cả khi có exception xảy ra giữa chừng — không cần dọn dẹp thủ công.
Pattern sai:
f = open('data.csv', 'r')
content = f.read()
# quên f.close() — nếu exception xảy ra ở trên, FD sẽ bị rò rỉ
Đã sửa:
with open('data.csv', 'r') as f:
content = f.read()
# FD được giải phóng ở đây, dù có chuyện gì xảy ra
Socket cũng hoạt động tương tự:
import socket
with socket.create_connection(('example.com', 80)) as sock:
sock.sendall(b'GET / HTTP/1.0\r\n\r\n')
response = sock.recv(4096)
# socket tự động đóng
Cách Sửa 2 — Đóng Tường Minh Trong Code Chạy Lâu
Đôi khi bạn không thể dùng with — chẳng hạn với thuộc tính của object hoặc handle ở cấp class. Hãy dùng try/finally thay thế:
f = open('output.log', 'a')
try:
f.write(line)
finally:
f.close()
HTTP session cũng có bẫy tương tự. Hãy dùng chúng như context manager:
import requests
with requests.Session() as session:
resp = session.get('https://api.example.com/data')
print(resp.json())
Cách Sửa 3 — Tăng Giới Hạn OS
Các server có độ đồng thời cao và các bộ xử lý file hàng loạt có thể thực sự cần hàng nghìn FD. Mặc định 1024 đơn giản là quá thấp.
Tạm thời (chỉ cho phiên shell hiện tại):
ulimit -n 65536
Vĩnh viễn cho một user — chỉnh sửa /etc/security/limits.conf:
# /etc/security/limits.conf
www-data soft nofile 65536
www-data hard nofile 65536
Đăng xuất và đăng nhập lại để thay đổi có hiệu lực. Xác nhận bằng ulimit -n.
Cho một systemd service — thêm vào phần [Service]:
[Service]
LimitNOFILE=65536
Sau đó áp dụng: sudo systemctl daemon-reload && sudo systemctl restart yourservice
Cách Sửa 4 — Dùng Module resource Trong Python
Cần tăng giới hạn từ bên trong script — không có systemd, không có quyền truy cập shell? Module resource xử lý điều đó khi khởi động:
import resource
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
print(f'Giới hạn hiện tại: soft={soft}, hard={hard}')
# Tăng giới hạn mềm lên đến giới hạn cứng
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
Một lưu ý: nếu không có quyền root, bạn chỉ có thể tăng giới hạn mềm lên đến giới hạn cứng. Bất kỳ điều gì vượt quá đó đều cần dùng ulimit hoặc limits.conf.
Cách Sửa 5 — Giới Hạn Kích Thước Connection Pool (requests, aiohttp, databases)
Dưới tải nặng, các HTTP client và DB driver có thể mở kết nối nhanh hơn tốc độ chúng được trả về pool. Hãy cố định kích thước pool một cách tường minh:
import requests
from requests.adapters import HTTPAdapter
session = requests.Session()
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=20)
session.mount('https://', adapter)
session.mount('http://', adapter)
Với asyncio, semaphore giúp kiểm soát số lượng file mở đồng thời:
import asyncio
async def process_files(paths):
sem = asyncio.Semaphore(100) # giới hạn tối đa 100 FD đồng thời
async def open_with_limit(path):
async with sem:
# thao tác file hoặc mạng ở đây
pass
await asyncio.gather(*[open_with_limit(p) for p in paths])
Điều chỉnh con số 100 đó dựa trên ulimit của bạn. Quy tắc an toàn: giữ dưới 50% giới hạn mềm của bạn.
Xác Nhận Bản Sửa
Theo dõi số FD theo thời gian thực trong khi workload đang chạy:
# Số FD trực tiếp cho PID
watch -n1 'ls /proc/PID/fd | wc -l'
# Kiểm tra giới hạn mà tiến trình thực sự thấy
cat /proc/PID/limits | grep 'open files'
Con số ổn định nghĩa là rò rỉ đã được vá. Vẫn còn tăng? Dùng lsof để tìm thủ phạm:
lsof -p PID | sort -k9 | head -40
Các entry lặp đi lặp lại của cùng một đường dẫn file chính là chỗ rò rỉ — đó chính xác là nơi cần xem xét.
Danh Sách Kiểm Tra Nhanh
- Chạy
ulimit -n— mặc định có còn là 1024 không? Hãy tăng lên. - Kiểm tra
ls /proc/PID/fd | wc -l— bạn đang gần giới hạn đến mức nào? - Tìm trong code các lệnh gọi
open(không nằm trong blockwith. - Tìm các
socketchưa đóng, các lệnh gọirequests.get()trực tiếp, hoặc DB cursor. - Dùng thread hoặc async? Đảm bảo số lượng file mở song song có giới hạn — không dùng
gather()không giới hạn hoặcThreadPoolExecutorvới số worker không giới hạn.

