• Indonesian
  • English
  • Cara Mendeteksi & Membebaskan Disk Space dari File Terhapus

    📑 Daftar Isi

    1. Pendahuluan
    2. Gejala Disk Space Penuh Tapi File Tidak Terlihat
    3. Root Cause: Kenapa File Bisa Terhapus Tapi Space Tidak Kembali?
      1. 1. Konsep File Descriptor di Linux
      2. 2. Apa yang Terjadi Saat File Dihapus?
      3. 3. Kenapa Proses Masih Memegang File?
    4. Command #1: Temukan Semua File Terhapus yang Masih Dipegang Proses
      1. Penjelasan Tiap Bagian
      2. Contoh Output

        COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
        lsphp 8834 nobody 5w REG 253,1 12582944 0 456 /var/log/lsphp80/error.log-deleted (deleted)
        lsphp 8834 nobody 6w REG 253,1 8388608 0 457 /var/log/lsphp80/access.log-deleted (deleted)
        lsphp 9012 nobody 5w REG 253,1 6291456 0 458 /var/log/lsphp80/error.log-deleted (deleted)
        mysqld 1205 mysql 4w REG 253,1 5242880 0 612 /var/log/mysql/slow.log-deleted (deleted)
        lsphp 8834 nobody 7w REG 253,1 3145728 0 459 /tmp/session_phpxyz123-deleted (deleted)
        nginx 1102 root 5w REG 253,1 2097152 0 613 /var/log/nginx/access.log-deleted (deleted)

        Interpretasi Output

    5. Command #2: Identifikasi Proses yang Memegang File Terhapus
      1. Contoh Output
      2. Cara Membaca Output
    6. Command #3: Hitung Total Space yang Ditahan
      1. Contoh Output
    7. Cara Membebaskan Space dari File Terhapus
      1. ⚠️ PERINGATAN KEAMANAN: Backup & Verifikasi Sebelum Truncate
      2. ⚠️ PERINGATAN: Jangan Jalankan Perintah Truncate Tanpa Filter
      3. Alternatif: Restart Proses (Cara Paling Aman)
    8. Command Tambahan: Truncate Semua File Log PHP-FPM yang Terhapus
      1. Penjelasan Tiap Bagian
      2. ⚠️ PERINGATAN Keras untuk Perintah Ini
    9. Pro Tips: Pencegahan File Terhapus Menahan Space
    10. FAQ
      1. Apakah file terhapus yang menahan space akan hilang sendiri setelah beberapa waktu?
      2. Bagaimana cara mengetahui apakah file yang dipegang proses itu log atau file penting lainnya?
      3. Apakah aman menjalankan perintah gawk truncate di production?
      4. Kenapa saya harus pakai lsof +L1 dan bukan lsof | grep deleted?
    11. Related Issues
    12. Kesimpulan

    Kecepatan:

    Pendahuluan

    Pernahkah kamu mengalami situasi di mana disk space server penuh, kamu cek semua direktori pakai du -sh /*, tapi hasilnya tidak mencerminkan penggunaan disk yang sebenarnya? Misalnya, df -h menunjukkan disk sudah 95% terpakai, tapi kalau dijumlahkan dari semua direktori, hasilnya cuma 60%. Sisa 35% di mana?

    Pertama kali saya encounter masalah ini, saya sempat bingung selama hampir satu jam. Saya sudah cek log, sudah cek direktori temporary, bahkan sudah cek file cache — semuanya tidak ada yang cukup besar untuk menjelaskan selisih 35% tersebut. Barulah setelah saya pakai lsof +L1, saya menemukan rahasianya: ada ratusan file yang sudah dihapus dari filesystem, tapi masih tertahan oleh proses yang sedang berjalan. File-file ini sudah tidak terlihat di direktori manapun, tapi space-nya masih dihitung oleh sistem.

    Di artikel ini saya akan tunjukkan cara menemukan file-file “hantu” ini, memahami kenapa file bisa terhapus tapi space-nya tidak kembali, dan yang paling penting — cara membebaskan space tersebut dengan aman. Semua command di bawah sudah saya pakai berkali-kali di production server dan terbukti ampuh.

    Gejala Disk Space Penuh Tapi File Tidak Terlihat

    Sebelum masuk ke command troubleshooting, penting untuk tahu gejala-gejala yang menandakan ada file terhapus yang masih menahan disk space:

    • Selisih antara df -h dan du -sh — ini gejala paling klasik. df menunjukkan disk hampir penuh, tapi du di semua direktori tidak menjumlahkan ke angka yang sama.
    • Disk space berkurang sendiri tanpa ada file baru — kadang space berkurang perlahan-lahan meskipun kamu tidak mengupload atau membuat file baru. Ini bisa jadi karena log yang terus bertambah tapi proses lama masih menahan file log yang sudah di-rotate.
    • Backup gagal karena “No space left on device” — error ini muncul meskipun secara logika masih ada space yang cukup. Karena yang menahan space adalah file yang sudah tidak terlihat.
    • Proses tertentu menggunakan RAM berlebihan — beberapa proses yang menahan file descriptor bisa juga menggunakan RAM ekstra untuk buffer, terutama kalau file tersebut sangat besar.
    • Log rotation tidak efektif — kamu sudah rotate log, file log lama sudah dihapus dari direktori, tapi space tidak kembali. Ini karena proses logging masih menulis ke file descriptor yang sama.

    Root Cause: Kenapa File Bisa Terhapus Tapi Space Tidak Kembali?

    Untuk memahami masalah ini, kita perlu tahu bagaimana Linux menangani file yang sedang dibuka oleh suatu proses.

    1. Konsep File Descriptor di Linux

    Ketika suatu proses (misalnya Apache, PHP-FPM, atau MySQL) membuka file, sistem operasi membuat file descriptor — sebuah “link” antara proses tersebut dengan file di filesystem. File descriptor ini seperti kunci yang dipegang oleh proses selama file masih dibuka.

    2. Apa yang Terjadi Saat File Dihapus?

    Ketika kamu menjalankan rm /path/to/file, yang terjadi adalah:

    • Nama file dihapus dari direktori (inode link count berkurang)
    • Tapi kalau masih ada proses yang membuka file tersebut, file descriptor masih aktif
    • Filesystem menandai file sebagai “deleted” tapi tidak membebaskan space sampai semua file descriptor ke file tersebut ditutup

    Jadi file sudah tidak terlihat di ls, tapi space-nya masih dipakai oleh proses yang memegang file descriptor tersebut. Inilah kenapa kamu bisa melihat selisih antara df dan du.

    3. Kenapa Proses Masih Memegang File?

    Ada beberapa alasan kenapa proses masih memegang file descriptor meskipun file sudah dihapus:

    • Log rotation yang tidak sempurna — proses logging harus di-restart setelah log di-rotate. Kalau tidak di-restart, proses tetap menulis ke file descriptor yang sama (file lama yang sudah dihapus).
    • PHP-FPM worker yang stuck — worker PHP yang sudah selesai tapi belum di-restart masih bisa memegang file descriptor ke log atau temporary file.
    • Cron job yang lambat — script cron yang sedang memproses file besar dan belum selesai akan memegang file descriptor sampai selesai.
    • Proses batch yang lama — import/export database, backup, atau proses batch lain yang memakan waktu lama.

    Command #1: Temukan Semua File Terhapus yang Masih Dipegang Proses

    Langkah pertama adalah mencari tahu file mana yang sudah dihapus tapi masih menahan space. Kita pakai lsof dengan flag +L1 yang berarti “link count kurang dari 1” — yaitu file yang sudah tidak punya nama di direktori tapi masih dipegang oleh proses.

    lsof +L1 | grep deleted | sort -k 7 -rn | head -10

    Mari kita bedah command ini satu per satu:

    Penjelasan Tiap Bagian

    1. lsof +L1lsof (list open files) adalah command untuk melihat semua file yang sedang dibuka oleh proses di sistem. Flag +L1 memfilter hanya file yang link count-nya kurang dari 1 — yaitu file yang sudah dihapus dari direktori (link-nya sudah 0) tapi masih dipegang oleh proses. Tanpa flag ini, lsof akan menampilkan semua file yang terbuka, termasuk yang masih ada di direktori.

    2. | grep deleted — Filter hasil lsof untuk menampilkan hanya baris yang mengandung kata deleted. Di output lsof, file yang sudah dihapus ditandai dengan status (deleted) di akhir baris. Ini memastikan kita hanya melihat file yang sudah dihapus tapi masih menahan space.

    3. | sort -k 7 -rn — Urutkan hasil berdasarkan kolom ke-7 secara numerik terbalik (-rn berarti reverse numeric). Kolom ke-7 di output lsof adalah ukuran file dalam bytes. Jadi file terbesar akan muncul di paling atas — ini membantu kita memprioritaskan file mana yang paling banyak menahan space.

    4. | head -10 — Tampilkan hanya 10 baris pertama — yaitu 10 file terbesar yang masih menahan space. Biasanya 10 file terbesar ini sudah cukup untuk menjelaskan selisih space yang signifikan.

    Contoh Output

    COMMAND     PID   USER   FD   TYPE DEVICE SIZE/OFF   NLINK NODE NAME
    lsphp      8834  nobody    5w  REG  253,1  12582944      0  456 /var/log/lsphp80/error.log-deleted (deleted)
    lsphp      8834  nobody    6w  REG  253,1   8388608      0  457 /var/log/lsphp80/access.log-deleted (deleted)
    lsphp      9012  nobody    5w  REG  253,1   6291456      0  458 /var/log/lsphp80/error.log-deleted (deleted)
    mysqld     1205  mysql     4w  REG  253,1   5242880      0  612 /var/log/mysql/slow.log-deleted (deleted)
    lsphp      8834  nobody    7w  REG  253,1   3145728      0  459 /tmp/session_phpxyz123-deleted (deleted)
    nginx      1102  root      5w  REG  253,1   2097152      0  613 /var/log/nginx/access.log-deleted (deleted)

    Interpretasi Output

    Dari output di atas, kita bisa lihat beberapa hal penting:

    • File terbesar/var/log/lsphp80/error.log-deleted dengan ukuran 12 MB masih dipegang oleh proses lsphp (PHP-FPM) dengan PID 8834. File ini sudah dihapus dari direktori tapi masih menahan 12MB space.
    • Pattern yang sama — ada 3 file lsphp80/error.log-deleted dari PID yang berbeda (8834 dan 9012). Ini menandakan beberapa worker PHP-FPM masih memegang file descriptor ke log lama.
    • Total space yang ditahan — kalau dijumlahkan, 10 file di atas menahan sekitar 40MB space. Tapi ini baru 10 file terbesar — bisa jadi ada ratusan file kecil lain yang juga menahan space.

    Command #2: Identifikasi Proses yang Memegang File Terhapus

    Setelah tahu file mana yang menahan space, kita perlu tahu proses mana yang memegang file tersebut. Ini penting untuk menentukan tindakan selanjutnya — apakah perlu restart proses, atau cukup tutup file descriptor-nya.

    lsof +L1 | grep deleted | awk '{print $1, $2, $7, $9}' | sort -k 3 -rn | head -20

    Command ini menampilkan: nama proses, PID, ukuran file, dan path file yang dihapus. Hasilnya diurutkan berdasarkan ukuran file terbesar.

    Contoh Output

    lsphp 8834 12582944 /var/log/lsphp80/error.log-deleted
    lsphp 9012  8388608 /var/log/lsphp80/access.log-deleted
    mysqld 1205  5242880 /var/log/mysql/slow.log-deleted
    lsphp 8834  3145728 /tmp/session_phpxyz123-deleted
    nginx 1102  2097152 /var/log/nginx/access.log-deleted

    Cara Membaca Output

    Setiap baris menunjukkan:

    • Kolom 1 (lsphp) — nama proses yang memegang file descriptor. lsphp = LiteSpeed PHP (PHP-FPM), mysqld = MySQL/MariaDB, nginx = Nginx web server.
    • Kolom 2 (8834) — PID (Process ID) dari proses tersebut. Dari PID ini kita bisa tahu apakah perlu restart atau tidak.
    • Kolom 3 (12582944) — ukuran file dalam bytes. Untuk mengkonversi ke MB, bagi dengan 1.048.576.
    • Kolom 4 (/var/log/…) — path file yang sudah dihapus tapi masih dipegang.

    Command #3: Hitung Total Space yang Ditahan

    Untuk mengetahui total seberapa banyak space yang ditahan oleh file terhapus, gunakan command ini:

    lsof +L1 | grep deleted | awk '{sum += $7} END {printf "Total space held by deleted files: %.2f MBn", sum/1048576}'

    Atau versi yang lebih detail per proses:

    lsof +L1 | grep deleted | awk '{proc[$1]+=$7} END {for(p in proc) printf "%-15s %.2f MBn", p, proc[p]/1048576}' | sort -k2 -rn

    Contoh Output

    Total space held by deleted files: 847.32 MB

    Atau per proses:

    lsphp            612.45 MB
    mysqld           156.23 MB
    nginx             78.64 MB

    Dari output ini, sudah jelas: proses lsphp (PHP-FPM) menahan lebih dari 600MB space. Ini menjelaskan kenapa disk terasa penuh meskipun file sudah dihapus dari direktori.

    Cara Membebaskan Space dari File Terhapus

    ⚠️ PERINGATAN KEAMANAN: Backup & Verifikasi Sebelum Truncate

    Sebelum kita melakukan truncate pada file descriptor, WAJIB pahami bahwa perintah truncate akan mengosongkan isi file secara permanen. Jika file tersebut ternyata masih dibutuhkan (misalnya log yang belum di-backup), kamu akan kehilangan data tersebut selamanya.

    Backup dulu file yang masih bisa di-backup:

    # Backup file log yang masih ada di direktori (jika masih ada)
    cp /var/log/lsphp80/error.log /tmp/backup/error.log.bak-$(date +%Y%m%d-%H%M) 2>/dev/null
    
    # Backup seluruh daftar file terhapus untuk referensi
    lsof +L1 | grep deleted > /tmp/deleted-files-list-$(date +%Y%m%d-%H%M).txt

    Verifikasi target sebelum truncate:

    # Cek ulang file mana saja yang akan dituncate
    lsof +L1 | grep deleted | grep lsphp
    
    # Pastikan tidak ada file penting yang ikut tertangkap
    # (misalnya file database, file konfigurasi, atau file backup)
    lsof +L1 | grep deleted | grep -E "(mysql|postgres|config|backup)" 

    Perintah truncate via /proc/PID/fd/FD:

    Cara paling aman untuk membebaskan space dari file terhapus adalah dengan menulis ulang isi file descriptor tersebut. Kita bisa melakukannya melalui /proc/PID/fd/FD:

    # Truncate file descriptor tertentu (ganti PID dan FD dengan yang sesuai dari hasil lsof)
    echo -n "" > /proc/8834/fd/5
    echo -n "" > /proc/8834/fd/6

    Perintah ini akan mengosongkan isi file descriptor tanpa menutup koneksi atau menghentikan proses. Setelah di-truncate, proses akan tetap menulis ke file descriptor yang sama tapi ke area kosong — artinya space yang sebelumnya ditahan akan kembali ke sistem.

    ⚠️ PERINGATAN: Jangan Jalankan Perintah Truncate Tanpa Filter

    JANGAN jalankan perintah truncate untuk semua file descriptor sekaligus tanpa verifikasi. Perintah seperti ini sangat berbahaya:

    # ❌ JANGAN LAKUKAN INI — bisa mengosongkan file yang masih dibutuhkan
    lsof +L1 | grep deleted | awk '{print $2, $4}' | while read pid fd; do
      echo -n "" > /proc/$pid/fd/$fd
    done

    Perintah di atas akan mengosongkan SEMUA file descriptor yang dipegang oleh proses — termasuk file yang mungkin masih dibutuhkan (misalnya file socket, file pipe, atau file yang sedang aktif ditulis). Hasilnya bisa fatal: proses crash, koneksi terputus, atau data corrupt.

    Selalu: cek dulu file mana yang dituncate, pastikan hanya file log atau temporary file yang di-truncate, dan jangan pernah truncate file descriptor milik sistem kritis.

    Alternatif: Restart Proses (Cara Paling Aman)

    Jika kamu tidak yakin atau ragu-ragu, cara paling aman adalah restart proses yang menahan file. Begitu proses di-restart, semua file descriptor otomatis ditutup dan space akan kembali:

    # Restart PHP-FPM (akan menutup semua file descriptor lsphp)
    systemctl restart lsphp
    
    # Atau untuk versi PHP tertentu
    systemctl restart lsphp80
    
    # Restart MariaDB (untuk file log MySQL)
    systemctl restart mariadb
    
    # Restart Nginx
    systemctl restart nginx

    ⚠️ PENTING: Restart proses production harus dilakukan dengan hati-hati. Pastikan:

    • Tidak ada request aktif yang sedang diproses (atau tunggu sampai selesai)
    • Tidak ada deploy atau perubahan kode yang sedang berjalan
    • Client sudah diberi tahu jika ada kemungkinan downtime sesaat
    • Sudah ada backup konfigurasi sebelum restart

    Verifikasi setelah restart:

    # Cek apakah space sudah kembali
    df -h / | tail -1
    
    # Cek apakah file descriptor sudah bersih
    lsof +L1 | grep deleted | wc -l

    Command Tambahan: Truncate Semua File Log PHP-FPM yang Terhapus

    Kalau kamu yakin semua file terhapus yang dipegang oleh lsphp adalah file log yang sudah tidak dibutuhkan, ada command yang lebih efisien untuk truncate sekaligus:

    gawk 'match($0, /lsphp.* (([0-9]+).* ([0-9]+)[uw].*deleted)/, a) {print "true > /proc/"a[2]"/fd/"a[3]}' <(lsof +L1 | grep deleted) | sh

    Mari kita bedah command ini:

    Penjelasan Tiap Bagian

    1. gawk 'match($0, /lsphp.* (([0-9]+).* ([0-9]+)[uw].*deleted)/, a) — Gunakan gawk (GNU awk) dengan fungsi match untuk melakukan regex matching terhadap setiap baris output lsof. Regex ini mencari baris yang mengandung lsphp (proses PHP-FPM) dan mengekstrak: PID proses (a[2]) dan file descriptor number (a[3]). Flag [uw] menandakan file descriptor dalam mode write (w) atau read-write (u).

    2. {print "true > /proc/"a[2]"/fd/"a[3]} — Untuk setiap kecocokan, cetak perintah shell yang akan mengosongkan file descriptor tersebut. true > /proc/PID/fd/FD berarti menulis output kosong dari command true ke file descriptor — ini lebih ringan dari echo -n "" karena true tidak mengalokasikan buffer apapun.

    3. <(lsof +L1 | grep deleted) — Process substitution: jalankan lsof +L1 | grep deleted dan supply hasilnya sebagai input file untuk gawk.

    4. | sh — Pipe semua perintah truncate yang sudah dicetak ke shell untuk dieksekusi. Setiap baris akan menjalankan truncate untuk satu file descriptor.

    ⚠️ PERINGATAN Keras untuk Perintah Ini

    Perintah di atas hanya akan menangkap file yang dimulai dengan "lsphp" berkat filter regex lsphp. Ini lebih aman daripada truncate semua file, tapi tetap perlu diperhatikan:

    • Pastikan tidak ada file selain log yang dimulai dengan "lsphp" — misalnya kalau ada file temporary PHP yang penting, perintah ini juga akan mengosongkannya.
    • Verifikasi dulu output-nya — sebelum pipa ke sh, cek dulu apa yang akan dijalankan dengan menghapus | sh di akhir command.
    • Jangan jalankan di production tanpa testing dulu — coba di staging environment terlebih dahulu untuk memastikan tidak ada efek samping.

    Pro Tips: Pencegahan File Terhapus Menahan Space

    1. Restart PHP-FPM setelah log rotation — ini langkah paling penting. Kalau kamu rotate log PHP-FPM, pastikan restart proses PHP-FPM supaya file descriptor lama ditutup dan ditulis ke file log baru. Tambahkan di crontab: 0 0 * * * /usr/bin/systemctl restart lsphp80
    2. Monitor disk space secara berkala — buat cron job untuk alert kalau selisih antara df dan du lebih dari 10%: 0 */6 * * * DIFF=$(($(df / --output=pcent | tail -1 | tr -dc '0-9') - $(du -s / 2>/dev/null | awk '{print int($1/1024/1024/100*100)}'))); [ $DIFF -gt 10 ] && echo "ALERT: Disk space discrepancy - $DIFF%" | mail -s "Disk Alert" admin@domain.com
    3. Gunakan logrotate dengan copytruncate — di /etc/logrotate.d/lsphp, tambahkan opsi copytruncate. Opsi ini membuat salinan file log lama lalu mengosongkan file asli (bukan menghapus dan membuat baru). Dengan cara ini, proses logging tetap menulis ke file descriptor yang sama tanpa perlu restart.
    4. Setup logrotate untuk semua service log — pastikan semua log yang aktif ditulis (Apache, Nginx, PHP, MySQL, mail) sudah dikonfigurasi logrotate dengan copytruncate atau diikuti dengan restart service.
    5. Periksa file terhapus secara berkala — tambahkan command ini di monitoring script: lsof +L1 | grep deleted | wc -l | awk '{if ($1 > 50) print "ALERT: "$1" deleted files still held by processes"}'
    6. Gunakan tmpfs untuk temporary file — untuk file temporary yang sering dibuat dan dihapus (seperti session file PHP), gunakan tmpfs yang mounted di RAM. Space tidak akan mempengaruhi disk utama.
    7. Set ulimit untuk file descriptor — batasi jumlah file descriptor per proses untuk mencegah satu proses menahan terlalu banyak file. Tambahkan di /etc/security/limits.conf: nobody soft nofile 65535 dan nobody hard nofile 65535

    FAQ

    Apakah file terhapus yang menahan space akan hilang sendiri setelah beberapa waktu?

    Tidak. File terhapus yang masih dipegang oleh proses akan tetap menahan space sampai proses tersebut ditutup, di-restart, atau file descriptor-nya ditutup manual. Tidak ada timeout atau auto-cleanup dari sistem operasi. Kalau kamu diamkan saja, space tersebut akan terus ditahan selama proses masih berjalan — bahkan bisa berhari-hari atau berminggu-minggu.

    Bagaimana cara mengetahui apakah file yang dipegang proses itu log atau file penting lainnya?

    Lihat path file di output lsof. Kalau path-nya di /var/log/, kemungkinan besar itu file log. Kalau di /tmp/, itu temporary file. Kalau di /var/lib/mysql/, itu file database (hati-hati!). Kalau kamu ragu, jangan truncate — restart saja proses-nya. Restart lebih aman daripada truncate buta.

    Apakah aman menjalankan perintah gawk truncate di production?

    Relatif aman karena filter lsphp hanya menangkap file PHP-FPM. Tapi tetap ada risiko: kalau ada file temporary PHP yang penting (misalnya file upload yang sedang diproses), file tersebut juga akan terkena truncate. Sebaiknya cek dulu output-nya tanpa | sh, baru jalankan setelah yakin.

    Kenapa saya harus pakai lsof +L1 dan bukan lsof | grep deleted?

    lsof +L1 memfilter berdasarkan link count — hanya file yang sudah tidak punya link di direktori (link count < 1). Sedangkan lsof | grep deleted hanya mencari kata "deleted" di output. Keduanya bisa memberikan hasil yang berbeda. lsof +L1 lebih akurat untuk menemukan file yang benar-benar sudah dihapus dari direktori tapi masih menahan space. Kombinasi keduanya (lsof +L1 | grep deleted) memberikan hasil yang paling presisi.

    Related Issues

    Kesimpulan

    File terhapus yang masih menahan disk space adalah masalah yang sering terlihat sepele tapi bisa berakibat fatal jika tidak ditangani. Dengan lsof +L1, kamu bisa menemukan file-file "hantu" ini dengan cepat. Dua command utama yang harus kamu hafalkan adalah:

    • lsof +L1 | grep deleted | sort -k 7 -rn | head -10 — untuk menemukan file terhapus terbesar yang masih menahan space
    • echo -n "" > /proc/PID/fd/FD — untuk membebaskan space dari file descriptor tertentu (dengan verifikasi dulu!)

    Yang terpenting, pencegahan selalu lebih baik daripada treatment. Pastikan log rotation diikuti dengan restart service, monitor disk space secara berkala, dan selalu verifikasi sebelum truncate. Lebih baik cegah darip harus menjelaskan ke client kenapa disk server mereka penuh meskipun file log sudah dihapus — percaya deh, saya pernah ngalamin dan tidak mau ngalamin lagi.