df 顯示已滿但 du 找不到空間怎麼辦?
VPS 顯示磁碟已滿,但 df 與 du 數值不一致?了解 deleted file 仍被程序開啟時的原因,並用 lsof 找出檔案,不需重新開機即可釋放空間。
df 顯示已滿,但 du 顯示不是
df 回報磁碟已滿,但 du 找不到佔用的空間,原因是某個程序仍持有已刪除的檔案。刪除檔案只會移除檔案在目錄中的名稱。只有指向該 inode 的最後一個開啟檔案描述元關閉後,資料區塊才會釋放。du 會走訪檔案名稱,因此不會計入該檔案。df 會向檔案系統查詢已配置的區塊數,因此仍會計入已沒有名稱的檔案。
本指南會在已安裝所需工具的標準 Ubuntu VPS 上重現此問題,透過 /proc 找出持有檔案的程序,並在不重新開機的情況下釋放空間。後續也會說明其他會造成相同症狀的原因:inode table 沒有可用項目、檔案位於 mount point 下方,以及保留給 root 的區塊。
請逐一執行每個命令,並查看自己的輸出。數值取決於你的磁碟,因此請在自己的電腦上比較操作前後的結果,不要拿來與指南中列出的數值比較。
df 計算的內容與 du 計算的內容
df(disk free)會向每個已掛載的檔案系統查詢自身的統計資料:共有多少區塊、已配置多少區塊,以及剩餘多少區塊。它不會開啟任何目錄。統計結果包含所有已配置的區塊,包括屬於某個檔案、但沒有任何目錄項目指向該檔案的區塊。
du(disk usage)則相反。它從指定的路徑開始讀取目錄,對找到的每個項目取得 stat 資訊,再加總區塊數。沒有檔名的檔案不會被計入。無權讀取的目錄也一樣不會被計入,因此一般使用者取得的總數會小於 root。比較結果前,請在 sudo 下執行 du。
每次比較兩者時,都必須注意以下兩個選項。
-x讓du僅在同一個檔案系統內運作。未使用此選項時,du /會進入掛載於/下方的所有檔案系統,產生df /從未測量的總數。-s每個參數只輸出一行摘要,而不是每個目錄輸出一行。
因此,請在目標檔案系統上並列執行以下兩個命令。
df -h /
sudo du -xhs / 2>/dev/nulldf 會立即回應。du 在大型檔案系統上可能需要數分鐘,因為它會逐一取得沿途每個檔案的 stat 資訊。當兩個總數差異很大,且 du 以 root 身分搭配 -x 執行時,遺失的空間就是配置給沒有檔名的項目。
刻意重現不一致
請在測試用 VPS 上執行。以下所有內容都使用 bash 和 coreutils,因此不會安裝任何套件。
記錄存放 /var/tmp 的檔案系統目前狀態。
cd /var/tmp
df -h .
df --output=used -B1 .第二個命令會在不四捨五入的情況下輸出已使用的位元組數,因此最後的檢查結果會精確相符。
現在建立檔案。檔案大小取自電腦回報的可用空間,因此無論使用哪種磁碟,都能完成這項示範。
free=$(df --output=avail -B1 . | tail -n 1)
fallocate -l $((free / 10)) ghost.bin
ls -l ghost.bin
df -h .$(...) 是命令替換語法:shell 會執行其中的命令,並將輸出設為 free 的值。如果你不熟悉這項語法,bash 中的命令替換有完整說明。fallocate 不寫入資料,只保留實際的區塊,因此能立即完成。不支援此功能的檔案系統會使該命令失敗,而 head -c $((free / 10)) /dev/zero > ghost.bin 會透過寫入位元組執行相同工作。
將這個 df -h . 與先前記錄的結果比較。已使用欄位增加,可用欄位減少。
接著由另一個程序保持檔案開啟,再刪除檔案。
sleep infinity < ghost.bin &
holder=$!
rm ghost.bin
ls -l ghost.bin
df -h .
sudo du -xhs . 2>/dev/null重導向就是整個關鍵。sleep infinity < ghost.bin & 會啟動背景程序,並將該檔案設為其標準輸入,因此 shell 會開啟檔案,將檔案描述元交給 sleep,讓它保持開啟。$! 會保存該背景工作的程序 ID。rm 接著移除檔案名稱,但檔案描述元仍保持開啟。
查看輸出。ls 找不到檔案,因為檔案名稱已不存在。du 回到接近起始值,因為它會逐一走訪檔案名稱。df 沒有變動,因為區塊仍已配置。此時檔案系統的配置狀態與目錄樹不一致,兩者之間的差異就是剛才刪除的檔案。
找出持有已刪除檔案的程序
每個開啟的檔案描述元都會出現在 /proc/<pid>/fd/ 下,並以符號連結指向其參照的檔案。檔案解除連結後,核心會將該連結的目標標記為已刪除。因此,找出持有者就是找出目標帶有該標記的連結。
sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null-lname 比對的是符號連結的目標,而不是連結名稱;%p 顯示描述元路徑,%l 顯示連結指向的內容。它輸出的路徑中,第二個元素就是程序 ID。請搭配 sudo 執行,否則只能讀取自己程序的 /proc/<pid>/fd。stderr 重新導向會丟棄 find 掃描期間因程序結束而產生的雜訊。
忙碌的伺服器隨時可能持有數個已刪除檔案,其中大多數很小且沒有影響。依大小排序,讓有問題的檔案留在清單頂端。
sudo bash -c 'for fd in /proc/[0-9]*/fd/*; do
target=$(readlink "$fd" 2>/dev/null) || continue
case "$target" in
*"(deleted)") echo "$(stat -Lc %s "$fd" 2>/dev/null) $fd $target" ;;
esac
done' | sort -rn | headstat -L 會沿著連結追蹤至 inode 本身,因此 %s 回報的是已不再具有檔名之檔案的大小。依該數值排序後,最大的檔案會排在最前面。
接著找出持有該描述元的程序。清單頂端的路徑包含所需的兩個數字,因此先將它們存入變數,並以清單中實際輸出的內容取代 PID 和 N。
pid=PID
n=N
ps -o pid,user,etime,args -p "$pid"
sudo stat -L "/proc/$pid/fd/$n"ps 顯示程式名稱及其執行時間。stat -L 顯示已刪除 inode 的大小與已配置區塊數。兩者合併後即可回答關鍵問題:是哪個服務仍在持續使用這個檔案。
如果機器已安裝 lsof,sudo lsof +L1 會列出連結計數已降至 0 的開啟檔案,並在同一個表格中顯示大小。最精簡的 Ubuntu 映像檔中沒有此工具,而在沒有可用磁碟空間的檔案系統上安裝套件本身也可能失敗,因此 /proc 掃描是始終可用的做法。
釋放空間,無須重新開機
重新開機確實能解決問題,但不應是第一個處置方式:它會讓服務中斷,也會破壞證據。較溫和的處理方式有4種,請依序嘗試。
如果仍需要資料,先將資料複製出去。讀取 descriptor 路徑,就能讀取仍在使用中的 inode。
sudo cp /proc/<pid>/fd/<n> /root/recovered.log這是刪除檔案後仍容易取回的少數情況之一。因此,復原使用 rm -rf 刪除的檔案時,第一步會先確認是否仍有程序保持該檔案開啟。最後一個 descriptor 關閉後,就無法再透過這個方式取回。
第二,透過 descriptor 清空檔案。/proc 路徑會指向同一個 inode,因此截斷檔案即可釋放區塊,同時讓程序繼續執行。
sudo truncate -s 0 "/proc/$pid/fd/$n"
df -h /寫入程序以 append 模式開啟檔案時,這種方式可以正常運作,因為之後的每次寫入都會位於檔案目前的結尾。若不是以此模式開啟,程序會保留原本的寫入偏移量,因此下一次寫入會落在檔案很後面,並在檔案開頭重新形成空洞。空洞不會配置磁碟空間,因此區塊仍保持釋放狀態,而 df 會保留剛釋放的空間。恢復的只有檔案大小:程序寫入後再次執行 sudo stat -L "/proc/$pid/fd/$n",會看到原本的檔案大小,但區塊數量已不再與其相符。若也要讓檔案大小從0開始,請重新啟動程序。
第三,要求服務重新開啟其日誌。程序在日誌檔案遭到刪除後仍持續使用該檔案,是這個問題最常見的實際情況。許多 daemon 會在收到 signal 時重新開啟日誌檔案:nginx 使用 SIGUSR1,rsyslog 使用 SIGHUP。請查閱目前 daemon 的文件,不要自行猜測,因為對錯誤的 daemon 傳送錯誤的 signal 可能會使其停止。
sudo systemctl kill -s USR1 nginx第四,重新啟動 unit。sudo systemctl restart <unit> 會關閉舊程序持有的所有 descriptor,因此區塊一定會釋放。以上示範中的持有者是由你自行啟動的 sleep,因此結束該程序即可。
kill $holder
df -h .
df --output=used -B1 .
sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null將目前使用的位元組數與建立檔案前記錄的值比較。兩者會再次一致,而 find 也不再回報你的 descriptor。使用發現問題的相同指令進行驗證,是值得養成的習慣。
持續觀察該數值的變化,比一再手動執行 df 更容易。watch 會以固定間隔重複執行指令並在原處重新輸出結果,因此 watch df -h / 會顯示使用量欄位隨空間釋放而變化。
當總量一致但磁碟仍然已滿
如果 df 與 root 的 du -x 彼此一致,表示沒有涉及已刪除的檔案。剩下的原因類型不同,各自也有對應的檢查方式。
inode 用盡,而不是區塊用盡
inode 會儲存一個檔案的中繼資料。ext4 建立檔案系統時會配置固定數量的 inode,因此檔案系統可能在仍有可用區塊時耗盡 inode。此時即使 df -h 顯示仍有空間,建立新檔案仍會失敗。
df -h /
df -i /第一個命令會計算區塊,第二個命令會計算 inode。比較兩者的 use 欄位。如果區塊使用率很低,而 inode 使用率已達上限,表示問題是檔案數量非常多,但每個檔案都很小。
df 不接受在同一次呼叫中同時使用 -i 和 --output。因此,若要取得可讀取或傳給其他命令的原始計數,請依名稱選取 inode 欄位,並省略 -i。
df --output=itotal,iused,iavail,ipcent /這些欄位包含 df -i 顯示的相同用量統計,但格式適合進一步拆解。
請改用計算項目數,而不是位元組數,找出檔案所在位置。
sudo du --inodes -x -d 1 / 2>/dev/null | sort -rn | head在排名最高的目錄上向下再執行一次相同命令,每次深入一層,直到找到建立這些檔案的目錄樹。如果你的 du 不支援 --inodes,則使用 sudo find /var -xdev -type f | wc -l 以較慢的方式計算子樹。
解決方法是刪除或移動這些檔案。你無法向現有的 ext4 檔案系統新增 inode,因為 inode 數量在 mkfs 時便已固定;若要提高數量,就必須重新建立檔案系統,並從備份還原。XFS 會依需求配置 inode,因此不會以相同方式受到固定上限限制。執行容器的機器通常會更早達到這兩項上限,因為映像層包含許多小檔案。在該機器上,清理 VPS 上 Docker 的磁碟用量 才是針對此問題的解決方法,能釋放的空間也遠多於一般掃描整個檔案系統。
掛載點下隱藏的空間
目錄可以在尚未掛載任何內容前存放檔案。將檔案系統掛載到該目錄後,底下的檔案仍維持原狀:仍然配置空間、仍計入 df,但無法再透過名稱存取。du 看不到這些檔案,因為掛載內容會遮蔽它們。
使用 tmpfs 即可示範,無須額外磁碟空間。這個操作需要具備掛載權限的機器,因此可在 KVM VPS 上執行。
sudo mkdir -p /srv/covered
sudo cp /etc/services /srv/covered/
ls /srv/covered
sudo mount -t tmpfs tmpfs /srv/covered
ls /srv/covered
sudo umount /srv/covered
ls /srv/covered中間的 ls 顯示空目錄。複製的檔案並沒有消失:它仍位於 root filesystem,解除掛載後就會再次出現。現在設想某個服務在有人將 volume 掛載到該路徑前,已經持續將日誌寫入其中 1 個月。
若要在執行中的伺服器上找出實際內容,請將 root filesystem 再次掛載到其他位置。bind mount 會顯示單一檔案系統,但不包含其中已掛載的檔案系統。
sudo mkdir -p /mnt/rootcheck
sudo mount --bind / /mnt/rootcheck
sudo du -xhs /mnt/rootcheck/* 2>/dev/null | sort -h
sudo umount /mnt/rootcheck清單中出現、但在一般路徑下看不到的內容,就是被埋在掛載點下方的檔案。完成後請解除 bind mount,否則之後執行不含 -x 的 du 時,會將相同檔案計算 2 次。
保留給 root 的區塊
ext4 會保留一部分區塊給 root 使用,避免磁碟已滿時 root 無法登入並修復系統。以一般使用者身分執行的程序會先遇到這項限制,而 df 仍會顯示少量可用空間。請讀取自己檔案系統的設定,不要直接假設使用預設值。
dev=$(df --output=source / | tail -n 1)
sudo tune2fs -l "$dev" | grep -i 'block count'此指令會以相同單位列出區塊總數與保留區塊數,因此可直接計算兩者的比例。df 將 available 欄位顯示為一般使用者仍可使用的空間,所以 used 加上 available 會小於容量。兩者的差額就是保留空間。
使用 sudo tune2fs -m <percent> "$dev" 變更設定。變更會立即套用,不需要重新掛載。在獨立的資料檔案系統上降低保留比例是合理的。對根檔案系統而言,請保留足夠空間讓 root 仍能寫入,因為完全沒有可用空間的根檔案系統更難修復。tune2fs 可用於 ext2、ext3 和 ext4。XFS 沒有對應的設定。
du 單獨使用時容易誤判的情況
du 有 4 個特性可能讓總數看起來不正確。
- 硬連結:即使有多個名稱指向同一個 inode,
du也只會計算一次。因此,充滿硬連結的目錄樹,其結果會小於檔案大小總和。 - 稀疏檔案:
du回報實際配置的區塊,而ls -l回報表面大小。加入--apparent-size即可查看另一個數值。 - 權限:以一般使用者身分執行時,
du會略過無法讀取的內容,因此低估用量。它輸出的錯誤,正是人們會重新導向至/dev/null後便不再查看的內容。 - 檔案系統邊界:未使用
-x時,du /會計算掛載在/下方的所有檔案系統,因此總數可能超過df /回報的數值。
df 也有一個值得了解的特性。它會分別回報每個檔案系統的用量,因此應針對寫入失敗所涉及的確切路徑執行。另一個 /boot 會依自身時程逐漸填滿,原因是 kernel package 持續累積;在 Ubuntu 移除舊 kernel 與清理 / 的空間是不同的工作。
實際事件的處理順序
- 在發生寫入失敗的目標檔案系統上執行
df -h <path>和df -i <path>,不要習慣性地對/執行。 - 執行
sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h,然後逐層進入最大的目錄。 - 如果
du無法說明df回報的使用量,請在/proc中搜尋仍由程序開啟的已刪除檔案。 - 如果兩者數據一致,請將該檔案系統 bind mount 到其他位置,並搜尋掛載點下是否有檔案。
- 如果達到上限的是 inode 使用量,請計算檔案數量,而不是位元組數。
其中每個步驟都會執行命令,且你可以讀取其輸出。這就是修正問題與憑猜測處理的差異。
FAQ
為什麼 df 顯示磁碟已滿,但 du 找到的用量少得多?
最常見的原因是某個檔案已刪除,但仍由程序保持開啟。刪除檔案會移除其目錄項目,因此 du 無法取得檔名來遍歷,也就不再計算該檔案。inode 及其區塊會持續配置,直到最後一個描述元關閉;df 則會計算已配置的區塊。在 /proc/<pid>/fd 中搜尋目標標示為 deleted 的符號連結,即可找出該檔案及持有它的程序。在採信比較結果前,確認你是以 root 執行 du,且使用了 -x;一般使用者若無法讀取某些目錄,工具會直接略過而不提示。
不使用 lsof 時,如何找出仍保持開啟的已刪除檔案?
使用 kernel 自身維護的開啟描述元記錄。sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null 會列出所有指向無名稱檔案的描述元,而程序 ID 會出現在它輸出的路徑中。對其中一個描述元路徑執行 sudo stat -Lc %s,即可取得檔案大小,接著排序並找出需要處理的項目。這完全不需要額外套件;當檔案系統沒有可用空間時,安裝套件本身可能會失敗,這點尤其重要。
不終止程序也能釋放空間嗎?
有時可以。sudo truncate -s 0 /proc/<pid>/fd/<n> 會透過描述元存取同一個 inode,並釋放其區塊,同時讓程序繼續執行。如果程序以 append mode 開啟檔案,這是最乾淨的做法,因為寫入內容一律會附加到目前檔案結尾。如果不是以該模式開啟,寫入偏移量會維持原值;下一次寫入會在檔案前端留下 hole,重新建立檔案,因此顯示的大小會恢復,而 hole 下方的區塊仍保持可用。重新啟動 unit,或依該程序文件指定的 signal 通知它重新開啟 log,才能避免留下 sparse file。
df 顯示有可用空間,但寫入仍然失敗。還可能是什麼原因?
使用 df -i 檢查相同路徑上的 inode,因為檔案系統即使仍有可用區塊,若沒有可用 inode,也會拒絕建立新檔案。確認寫入是否由非 root 使用者執行,且目標是 ext4 檔案系統,而目前只剩 reserved blocks;在該裝置上執行 sudo tune2fs -l 即可查看。確認你檢查的是寫入實際目標所在的檔案系統,因為獨立的 /boot 或 /var 會與 / 分開耗盡空間。
為什麼 du 回報的總量比 df 還大?
du 未使用 -x 時,會進入指定路徑下掛載的每個檔案系統,因此會加總多個檔案系統;df 則只描述其中一個檔案系統。Bind mount 會讓情況更複雜,因為相同檔案會依其出現的路徑分別計算。加入 -x,讓 du 限定在單一檔案系統內;同時將相同路徑提供給 df,確保兩個指令描述的是同一個範圍。