Xử lý sự cố

Đã xóa file nhưng ổ cứng không giảm dung lượng: cách xử lý bằng lsof

Hướng dẫn xử lý lỗi đã gõ rm xóa file nhưng df -h không giảm dung lượng: nguyên nhân file descriptor chưa đóng, cách dùng lsof +L1 và kỹ thuật truncate an toàn.

Xử lý file đã xóa bằng rm nhưng dung lượng đĩa không giảm
MỤC LỤC

Khi phân vùng ổ cứng trên Linux chạm ngưỡng đầy, thao tác thường gặp là xóa bớt các file log lớn hoặc file sao lưu cũ để giải phóng dung lượng. Tuy nhiên, trong nhiều trường hợp, lệnh rm đã chạy xong nhưng kiểm tra lại bằng df -h thì dung lượng sử dụng vẫn giữ nguyên. Hệ thống vẫn tiếp tục báo lỗi thiếu dung lượng lưu trữ.

Nguyên nhân không nằm ở bộ đệm hiển thị hay sự cố ổ đĩa, mà xuất phát từ cơ chế quản lý tệp của nhân Linux: lệnh xóa chỉ gỡ tên file khỏi thư mục, trong khi dung lượng thực tế chỉ được giải phóng khi không còn tiến trình nào đang mở file đó.

Sơ đồ giải phóng dung lượng đĩa khi xóa file: Cơ chế liên kết cứng và File Descriptor

Phân biệt giữa xóa mục nhập thư mục (dentry) và đóng file descriptor của tiến trình trên Linux.

Vì sao lệnh rm không làm giảm dung lượng đĩa?

Trên các hệ thống tệp phổ biến như ext4 hay xfs, một tệp tin được cấu thành từ ba thành phần:

  • Dentry (Directory Entry): Tên file hiển thị trong cấu trúc thư mục để trỏ đến inode tương ứng.
  • Inode: Nơi lưu trữ siêu dữ liệu (metadata) của file, bao gồm quyền truy cập, kích thước, số lượng liên kết cứng (hard link count) và danh sách con trỏ trỏ tới các khối dữ liệu.
  • Data blocks: Các khối đĩa thực tế chứa nội dung tệp tin.

Khi bạn chạy lệnh rm, hệ điều hành thực hiện system call unlink(). Lệnh này chỉ làm hai việc: gỡ tên file khỏi thư mục và giảm bộ đếm liên kết cứng của inode đi 1.

Tuy nhiên, nhân Linux chỉ đánh dấu các data block là vùng trống khi thỏa mãn đồng thời hai điều kiện:

  1. Số liên kết cứng trỏ tới inode bằng 0.
  2. Không còn tiến trình nào đang giữ file descriptor mở tới inode đó.

Nếu một dịch vụ (như Nginx, MySQL hoặc tiến trình ứng dụng) đang mở file để ghi nhật ký, tiến trình này vẫn giữ file descriptor trỏ thẳng tới inode. Vì vậy, dù file không còn hiển thị khi chạy lệnh ls, hệ điều hành vẫn giữ nguyên các khối dữ liệu trên đĩa để đảm bảo tiến trình hoạt động bình thường.

Tình trạng này dẫn đến việc dung lượng đĩa không hề thay đổi cho đến khi file descriptor được đóng lại.

Bước 1: Nhận diện hiện tượng chênh lệch dung lượng giữa df và du

Dấu hiệu điển hình của việc tiến trình chưa đóng file descriptor là sự chênh lệch lớn giữa kết quả của hai lệnh df và du:

bash
# 1. Kiểm tra dung lượng từ góc nhìn phân vùng hệ thống tệp
df -h /

# 2. Kiểm tra tổng dung lượng các file thực tế đang nằm trong thư mục
sudo du -sh --exclude=/proc --exclude=/sys --exclude=/dev / 2>/dev/null

Cách hai công cụ tính toán dung lượng:

  • df (Disk Free): Đọc dữ liệu trực tiếp từ superblock của phân vùng. Lệnh này tính toàn bộ các block đã cấp phát trên đĩa, bao gồm cả những file đã bị gỡ khỏi thư mục nhưng tiến trình vẫn đang mở.
  • du (Disk Usage): Quét đệ quy qua cây thư mục và cộng kích thước của từng file đang hiển thị. Do file đã bị lệnh rm xóa khỏi thư mục, du sẽ không tính dung lượng của file đó nữa.

Bước 2: Dùng lệnh lsof để tìm tiến trình đang giữ file

Lệnh lsof (List Open Files - tham khảo chi tiết tại Linux man7.org lsof manual) giúp liệt kê các file đang mở trong hệ thống, bao gồm cả những file đã bị xóa khỏi cây thư mục nhưng tiến trình vẫn đang giữ file descriptor.

Nếu máy chủ chưa có sẵn công cụ này, bạn có thể cài đặt:

bash
# Trên Ubuntu / Debian
sudo apt update && sudo apt install -y lsof

# Trên RHEL / AlmaLinux / Rocky Linux
sudo dnf install -y lsof

Lệnh lọc các file đã xóa nhưng vẫn mở

Chạy lệnh lọc theo liên kết cứng:

bash
sudo lsof +L1

Nếu muốn tìm nhanh theo từ khóa, bạn cũng có thể dùng:

bash
sudo lsof | grep '(deleted)'

Kết quả trả về thường có cấu trúc như sau:

text
COMMAND    PID USER   FD   TYPE DEVICE   SIZE/OFF NODE NAME
nginx    12450  www    7w   REG  252,1 32212254720 131075 /var/log/nginx/access.log (deleted)
nginx    12451  www    7w   REG  252,1 32212254720 131075 /var/log/nginx/access.log (deleted)
mysqld    1890 mysql    5u   REG  252,1  8589934592 131090 /tmp/ibzT3k (deleted)

Các thông số cần lưu ý:

  • COMMAND: Tên dịch vụ đang mở file (ở ví dụ trên là nginx và mysqld).
  • PID: Mã định danh tiến trình (Process ID), ví dụ 12450.
  • FD: Số file descriptor kèm chế độ mở (ví dụ 7w là descriptor số 7 ở chế độ ghi).
  • SIZE/OFF: Kích thước thực tế của file tính theo byte (ví dụ 32212254720 byte xấp xỉ 30GB).
  • NAME: Đường dẫn ban đầu của file kèm nhãn (deleted).

Sắp xếp theo dung lượng chiếm dụng

Khi danh sách trả về nhiều dòng, bạn có thể sắp xếp để xác định ngay tiến trình đang giữ file có dung lượng lớn nhất:

bash
sudo lsof +L1 | sort -k 7 -n -r | head -n 10

Bước 3: Giải phóng dung lượng đĩa an toàn không cần dừng dịch vụ

Sau khi đã xác định được PID và số file descriptor (FD), bạn có hai cách xử lý tùy thuộc vào tính chất của dịch vụ trên môi trường production:

Cách 1: Làm rỗng nội dung qua hệ thống tệp ảo /proc

Mỗi file descriptor đang mở đều có đường dẫn đại diện trong thư mục ảo /proc/<PID>/fd/<FD>.

Dù file log ban đầu đã bị xóa khỏi thư mục, bạn vẫn có thể giải phóng dung lượng bằng cách đưa kích thước file về 0 byte thông qua đường dẫn descriptor này:

bash
# Cú pháp bằng lệnh truncate
sudo truncate -s 0 /proc/<PID>/fd/<FD>

# Hoặc dùng toán tử chuyển hướng trong shell
sudo sh -c '> /proc/<PID>/fd/<FD>'

Ví dụ với tiến trình Nginx có PID 12450 và file descriptor số 7:

bash
sudo truncate -s 0 /proc/12450/fd/7

Khi lệnh thực thi:

  • Nhân Linux thu hồi toàn bộ các data block của file và đưa về vùng nhớ trống.
  • Lệnh df -h / sẽ hiển thị dung lượng đĩa được giải phóng ngay lập tức.
  • Tiến trình ứng dụng không bị ngắt kết nối và tiếp tục ghi dữ liệu bình thường.

Cách 2: Yêu cầu dịch vụ mở lại file log hoặc reload tiến trình

Nếu không muốn thao tác trực tiếp vào /proc, bạn có thể yêu cầu tiến trình đóng file descriptor cũ và tạo file log mới.

Với Nginx: Dùng lệnh yêu cầu Nginx mở lại file log mà không làm gián đoạn request của người dùng (theo cơ chế quản lý log chính thức tại Nginx Documentation - Controlling Nginx Logs):

bash
# Lệnh mở lại log chính thức của Nginx
sudo nginx -s reopen

# Hoặc gửi tín hiệu USR1 trực tiếp đến tiến trình cha
sudo kill -USR1 $(cat /run/nginx.pid)

Với các dịch vụ hỗ trợ tải lại cấu hình (reload):

bash
sudo systemctl reload nginx
sudo systemctl reload php8.2-fpm

Với các ứng dụng không hỗ trợ reload (như một số daemon Java, Node.js hoặc script nền): Nếu ứng dụng không có cơ chế reopen log, bạn cần lên kế hoạch khởi động lại dịch vụ vào khung giờ ít người truy cập:

bash
sudo systemctl restart my-app.service

Khi tiến trình dừng hẳn, nhân Linux sẽ tự động đóng các file descriptor liên quan và hoàn trả dung lượng đĩa.

Bước 4: Thiết lập giải pháp phòng ngừa tái phát

Để không phải can thiệp thủ công khi ổ cứng đầy, bạn nên áp dụng các giải pháp chuẩn hóa sau trong vận hành hệ thống:

1. Dùng lệnh truncate thay cho rm khi cần làm rỗng file log

Khi cần giải phóng nhanh một file log dung lượng lớn mà không muốn gián đoạn dịch vụ, bạn nên làm rỗng file tại chỗ:

bash
# Đưa kích thước file về 0 byte nhưng giữ nguyên inode và file descriptor
sudo truncate -s 0 /var/log/nginx/access.log

# Hoặc dùng ký tự chuyển hướng
sudo true > /var/log/nginx/access.log

Thao tác này giữ nguyên đường dẫn và inode, giúp ứng dụng tiếp tục ghi vào file mà dung lượng đĩa vẫn được giải phóng ngay.

2. Cấu hình xoay vòng log định kỳ bằng logrotate

Mọi tiến trình ghi log trên máy chủ cần được cấu hình xoay vòng tự động qua logrotate.

Với ứng dụng tự triển khai ghi log riêng, tạo file cấu hình tại /etc/logrotate.d/my-app:

text
/var/log/my-app/*.log {
    daily
    missingok
    rotate 7
    compress
    delaycompress
    notifempty
    copytruncate
}

Tùy chọn copytruncate sao chép nội dung sang file nén lưu trữ, sau đó làm rỗng file gốc tại chỗ. Tùy chọn này phù hợp với các ứng dụng không hỗ trợ tín hiệu reload để mở lại log.

Với Nginx hoặc các dịch vụ hỗ trợ reload, bạn nên dùng chỉ thị postrotate để gửi tín hiệu mở lại file sau khi xoay vòng:

text
/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 $(cat /var/run/nginx.pid)
        fi
    endscript
}

3. Kiểm soát log của container Docker

File log dạng JSON của Docker (/var/lib/docker/containers/) nếu bị xóa bằng rm trong khi container đang chạy cũng sẽ dẫn đến tình trạng tương tự.

Cách xử lý an toàn:

bash
  sudo truncate -s 0 $(docker inspect --format='{{.LogPath}}' <CONTAINER_NAME>)

Tóm tắt quy trình kiểm tra và xử lý

Khi gặp tình trạng đã xóa file nhưng df -h không giảm dung lượng, quy trình xử lý gồm 4 bước:

  1. So sánh df và du: Xác định lượng dung lượng chênh lệch đang bị giữ bởi các tiến trình.
  2. Tìm tiến trình bằng lsof: Dùng sudo lsof +L1 để lấy PID, số FD và dung lượng file tương ứng.
  3. Giải phóng dung lượng: Chạy sudo truncate -s 0 /proc/<PID>/fd/<FD> hoặc yêu cầu dịch vụ mở lại file log.
  4. Chuẩn hóa cấu hình: Dùng truncate thay cho rm khi dọn file thủ công và thiết lập logrotate định kỳ.

Để đối chiếu với trường hợp ổ cứng còn trống dung lượng khối nhưng cạn kiệt số lượng tệp tin, bạn có thể tham khảo thêm bài viết Xử lý sự cố VPS đầy 100% inode dù dung lượng đĩa còn trống. Nếu ổ đĩa đầy do nguyên nhân khác, hãy xem quy trình tổng quát tại VPS đầy ổ đĩa: nguyên nhân thường gặp và quy trình xử lý an toàn.

Nội dung hướng dẫn này có hữu ích với hệ thống của bạn không?

Chia sẻ bài viết hữu ích

Gửi bài viết này cho đồng nghiệp

Đọc tiếp

Bài nên đọc tiếp

Xem tất cả bài về Xử lý sự cố