df 顯示磁碟已滿,但 du 找不到空間?
當 df 顯示已滿、du 卻找不到對應檔案,通常是程序仍開啟已刪除的檔案。學會用 lsof 找出程序,無須重新開機即可釋放空間。
df 顯示已滿,但 du 顯示不同
df 顯示磁碟已滿,但 du 找不到相應的空間,原因是某個程序仍開啟已刪除的檔案。刪除檔案只會移除其在目錄中的名稱。只有指向該 inode 的最後一個開啟檔案描述元關閉後,資料區塊才會釋放。du 會遍歷檔案名稱,因此不會計入該檔案。df 會向檔案系統查詢已配置的區塊數量,因此仍會計入這個已不再具有檔名的檔案。
本指南會在已安裝相關工具的標準 Ubuntu VPS 上重現這個情況,透過 /proc 找出持有檔案的程序,並在不重新開機的情況下釋放空間。相同症狀的其他原因包括 inode table 沒有可用項目、檔案被隱藏在掛載點下,以及保留給 root 的區塊。
逐一執行每個命令,並查看自己的輸出。數值會依磁碟而異,因此請在自己的機器上比較操作前後的結果,不要拿自己的結果與指南中列出的數值比較。
df 計算的內容與 du 計算的內容
df(disk free)會向每個已掛載的檔案系統查詢自身的統計資料:總共有多少區塊、已配置多少區塊,以及剩餘多少區塊。它不會開啟任何目錄。其結果涵蓋所有已配置的區塊,包括屬於某個檔案、但目前沒有任何目錄項目指向該檔案的區塊。
du(disk usage)則相反。它會從指定的路徑開始,讀取目錄、取得找到的每個項目的狀態,然後加總區塊。一個沒有檔名的檔案對它而言不可見。任何無權讀取的目錄也是如此,因此一般使用者取得的總量會小於 root 取得的總量。在根據比較結果下結論前,請以 sudo 執行 du。
每次比較這兩者時,都有兩個選項很重要。
-x會讓du只在同一個檔案系統內運作。若未使用此選項,du /會進入掛載在/下方的每個檔案系統,並產生一個df /從未計算的總量。-s會針對每個引數輸出一行摘要,而不是針對每個目錄輸出一行。
因此,在需要檢查的檔案系統上,應並列執行以下兩個命令。
df -h /
sudo du -xhs / 2>/dev/nulldf 會立即回應。du 在大型檔案系統上可能需要數分鐘,因為它會沿途取得每個檔案的狀態。當兩個總量差距很大,且 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 redirect 會捨棄 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 會列出連結計數已降至零的開啟檔案,並在同一個表格中顯示大小。最精簡的 Ubuntu 映像檔中不會預先安裝它,而且在沒有可用磁碟空間的檔案系統上安裝套件本身也可能失敗,因此 /proc walk 是永遠可用的做法。
釋放空間,無須重新開機
重新開機確實能解決問題,但不應是第一個處置方式:它會讓服務停止,也會破壞證據。共有 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 模式開啟檔案時,這種方式能正常運作,因為每次寫入都會前往檔案目前的結尾。若不是以該模式開啟,程序會保留原本的寫入偏移量,因此下一次寫入會落在檔案很後面,並在前方重新建立一個 hole。hole 不會配置儲存空間,因此區塊仍保持釋放狀態,df 也會保留剛釋放的空間。重新出現的只有檔案大小:程序寫入後再次執行 sudo stat -L "/proc/$pid/fd/$n",會看到原本的大小,但區塊數量已與大小不一致。若也希望檔案大小從 0 開始,請重新啟動程序。
第三,要求服務重新開啟其 log。log 檔案在服務執行期間遭到刪除,是這個問題最常見的實際情況。許多 daemon 會在收到 signal 時重新開啟 log 檔案:nginx 使用 SIGUSR1,rsyslog 使用 SIGHUP。請查閱目前 daemon 的說明文件,不要自行猜測,因為將錯誤的 signal 傳送給錯誤的 daemon 可能會使其停止。
sudo systemctl kill -s USR1 nginx這會將 signal 傳送給 systemd 記錄為該 unit 主要程序的程序。因此,如果 unit 宣告的 Type= 與其 daemon 實際啟動方式不符,signal 可能會傳送給從未持有刪除檔案的程序,空間也就不會釋放。
第四,重新啟動該 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 會以較慢的方式計算子樹。
解決方法是刪除或移動這些檔案。你無法將 inode 加入現有的 ext4 檔案系統,因為 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 掛載到該路徑前,已經連續一個月將 log 寫入其中。
若要在執行中的伺服器上找出實際檔案,請將 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 時,會將相同檔案計算兩次。
保留給 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 檔案系統則應保留足夠空間,讓 root 仍能寫入,因為完全沒有可用空間的 root 檔案系統會難以修復。這項保留空間也能避免你被拒於系統之外:如果檔案系統已無剩餘空間,附加至 authorized_keys 的金鑰可能只寫入一部分,或完全無法寫入,下一次登入便會因為 公鑰的權限遭拒 而失敗,原因與金鑰本身無關。tune2fs 適用於 ext2、ext3 和 ext4。XFS 沒有相等的設定。
du 單獨使用時容易誤判的情況
du 有四種行為會產生看似錯誤的總計。
- 硬連結:即使多個名稱指向同一個 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 使用相同路徑,使兩個指令描述相同內容。