Vấn đềMicroservice viết bằng Go của bạn đang chạy mượt mà—cho đến khi một chiến dịch marketing hoặc một bài kiểm tra tải (load test) diễn ra. Đột nhiên, nhật ký (logs) của bạn bùng nổ với một lỗi khó chịu: accept4: too many open files. Tại thời điểm này, server ngừng chấp nhận các kết nối mới, khiến người dùng không thể truy cập được.
Điều này xảy ra vì tiến trình Go của bạn đã chạm đến số lượng File Descriptors (FDs) tối đa mà hệ điều hành cho phép. Mặc dù Go có thể xử lý hàng nghìn goroutine một cách dễ dàng, nó vẫn bị ràng buộc bởi các giới hạn tài nguyên của nhân Linux.
Nguyên nhân gốc rễLinux coi hầu hết mọi thứ là một file. Điều này bao gồm các file vật lý trên ổ cứng NVMe, nhưng cũng bao gồm cả network sockets, pipes và database handles. Mỗi khi người dùng kết nối đến server của bạn qua TCP, hệ điều hành sẽ gán một File Descriptor mới cho kết nối cụ thể đó.
Hầu hết các bản phân phối Linux đặt giới hạn mềm (soft limit) mặc định là 1024 file mở cho mỗi tiến trình. Đây là một mức trần rất thấp đối với các ứng dụng web hiện đại. Nếu bạn có 500 người dùng hoạt động và mỗi yêu cầu kích hoạt một truy vấn cơ sở dữ liệu cùng một lệnh gọi API bên ngoài, bạn sẽ chạm giới hạn 1024 đó chỉ trong vài giây.
Bước 1: Kiểm tra giới hạn hiện tạiBắt đầu bằng việc kiểm tra giới hạn của phiên làm việc (shell session) hiện tại. Bạn có thể thực hiện việc này bằng lệnh ulimit:
ulimit -Sn # Kiểm tra giới hạn mềm (Soft limit)
ulimit -Hn # Kiểm tra giới hạn cứng (Hard limit)
Nếu kết quả trả về là 1024, bạn đã tìm thấy điểm nghẽn. Để xem tiến trình Go đang chạy thực sự mở bao nhiêu file, hãy tìm PID của nó và kiểm tra thư mục /proc:
# Tìm PID của ứng dụng trước
ls /proc/$(pgrep my-app-name)/fd | wc -l
Bước 2: Tăng giới hạn### Tùy chọn A: Thay đổi tạm thời (Cấp độ Session)Nếu bạn cần một giải pháp tức thời trong khi gỡ lỗi, hãy tăng giới hạn trong terminal hiện tại trước khi chạy file thực thi. Thay đổi này chỉ có hiệu lực trong phiên làm việc đó.
ulimit -n 65535
./my-go-server
Tùy chọn B: Thay đổi vĩnh viễn (Toàn hệ thống)Để đảm bảo các giới hạn vẫn tồn tại sau khi khởi động lại, hãy chỉnh sửa file /etc/security/limits.conf. Đây là cách tiếp cận tiêu chuẩn cho các hệ thống cũ hoặc triển khai thủ công.
sudo nano /etc/security/limits.conf
Thêm các dòng này vào cuối file. Sử dụng dấu * sẽ áp dụng cho tất cả người dùng, nhưng bạn có thể thay thế bằng tên người dùng cụ thể của dịch vụ để bảo mật chặt chẽ hơn:
* soft nofile 65535
* hard nofile 65535
Tùy chọn C: Dịch vụ Systemd (Tiêu chuẩn môi trường Production)Các bản phân phối Linux hiện đại sử dụng systemd, công cụ này thường bỏ qua limits.conf. Nếu bạn chạy ứng dụng Go dưới dạng một dịch vụ (service), bạn phải định nghĩa giới hạn ngay trong file .service.
# /etc/systemd/system/my-app.service
[Service]
ExecStart=/usr/local/bin/my-app
LimitNOFILE=65535
Áp dụng các thay đổi bằng cách tải lại daemon và khởi động lại dịch vụ của bạn:
sudo systemctl daemon-reload
sudo systemctl restart my-app
Bước 3: Tìm kiếm rò rỉ tài nguyên trong mã nguồnViệc tăng giới hạn lên 65.535 thường giúp bạn có đủ khoảng trống. Tuy nhiên, nếu số lượng FD của bạn tiếp tục tăng đều đặn mà không bao giờ giảm, bạn đang gặp lỗi rò rỉ tài nguyên. Không có giới hạn nào đủ cao để sửa lỗi mã nguồn bị hỏng.
Rò rỉ HTTP BodyLỗi phổ biến nhất trong Go là không đóng body của response từ một yêu cầu gửi đi. Nếu bạn không đóng nó, kết nối TCP sẽ duy trì ở trạng thái WAIT, chiếm giữ một FD vô thời hạn.
resp, err := http.Get("https://api.example.com")
if err != nil {
return err
}
// Thực hiện việc này ngay lập tức!
defer resp.Body.Close()
Bẫy kết nối cơ sở dữ liệuĐừng bao giờ gọi sql.Open bên trong một hàm xử lý yêu cầu (request handler). Hàm này tạo ra một pool kết nối, chứ không phải một kết nối đơn lẻ. Nếu bạn gọi nó trong mỗi yêu cầu, bạn sẽ tạo ra hàng nghìn pool và làm cạn kiệt FD gần như ngay lập tức. Thay vào đó, hãy khởi tạo đối tượng *sql.DB một lần khi khởi động và dùng chung trong các trình xử lý.
Xác minhXác nhận các giới hạn mới đã có hiệu lực bằng cách kiểm tra góc nhìn của kernel về tiến trình đang chạy. Đây là nguồn thông tin chính xác nhất:
cat /proc/$(pgrep my-app-name)/limits | grep "Max open files"
Nếu cột "Soft Limit" hiển thị 65535, cấu hình hệ điều hành của bạn đã chính xác. Bây giờ, hãy theo dõi số lượng FD bằng lệnh lsof -p <PID> | wc -l. Nếu con số này ổn định dưới mức tải cao, lỗi rò rỉ của bạn đã được khắc phục.

