調查一起 C 槽空間莫名消失的茶包,發現踩到 ManageEngine Desktop Central, MEDC 的地雷,軟體出錯在 C:\ProgramData\Microsoft\Crypto\SystemKeys 塞了超過 750 萬個私鑰檔案,檔案大小合計約 7G,但因空間配置以 4K 為單位,實際用掉約 30G:


(圖片為網友案例,非本案)

確認問題後,先移除 MEDC 軟體,再來想辦法清掉殘留的 SystemKeys 檔案騰出空間。

刪檔案?簡單,打開檔案總管選取,按 Shift-Del 永久刪除不就好了?

一般狀況是這樣沒錯,但是當資料夾內檔案數量超過百萬,你會發現世界變了:(參考:超過一百萬個檔案的 NTFS 資料夾…)

  • 用檔案總管開資料夾時會卡死沒反應,還無法取消或關閉
  • DIR 可以執行列出檔案,但只見檔案清單無窮無盡捲個不停,捲到此恨綿綿無絕期,超越常人的耐心極限
  • 單純 DIR 還能看到檔名狂跑,DIR /OD(依日期排序)、DIR *_Blah.txt (篩選檔名特徵)則是一執行就沒反應直到地老天荒…
  • 想用 .NET Directory.GetFiles() 逐一抓取檔案刪除,GetFiles() 會一次讀入完整清單,登楞!
  • 試用 PowerShell Get-ChildItem 也是指令下完音訊全無

與上回一百萬個檔案相比,750 萬個檔案造成的卡頓更是讓人嘆為觀止,連用唯一能動的 DIR 指令,查詢時間也是以小時為單位(註:與後來對照實驗結果有些出入,見下方更新)。想過用del .來個射後不理,暴力清空資料夾所有檔案,但 SystemKeys 可能包含使用中的私鑰,胡亂刪除擔心後遺症。對照過幾台 Windows 的 SystemKeys 目錄(正常檔案數量為個位數),發現可以用長度來識別,正常金鑰檔案清一色是 1597 Bytes,MEDC 產生的只有 958 Bytes,故可以鎖定大小是 958 的檔案進行刪除。

為了方便反覆測試、除錯,我決定將 dir 結果先存成檔案,再寫程式篩選刪除檔案。金鑰檔為系統隱藏檔,要用 dir /a 才會出現,750 萬個檔案 dir 要跑一個小時以上,我用dir /a | tee d:\temp\syskeys.txt 將結果同時輸出到螢幕跟檔案,隨時觀察進度比較不會焦躁。

(2022-07-25 更新:一般而言,數百萬個檔案的 dir 寫入檔案比輸出螢幕快 N 倍,不要串 tee 直接用 dir /a > file_path 轉存能省超多時間。如要觀察進度,用 tail 工具偷看檔案會比較有效率。)

刪檔程式用 C# 寫,邏輯很簡單,用 StreamReader 逐行讀取檔案資料,比對大小為 958 時取出檔案名稱,組成完整路徑以 File.Delete() 刪除。(原本猶豫要不要開多執行緒跑,但作業瓶頸在 Disk IO,實測一個執行緒刪檔就讓 Disk Queue Length > 5,Active Time 100%,還是別雪上加霜)

using System.IO;

var sysKeyFolder = @"C:\ProgramData\Microsoft\Crypto\SystemKeys";
using (var sr = new StreamReader("d:\\temp\\syskeys.txt")) 
{
    string line;
    while ((line = sr.ReadLine()) != null) 
    {
        var pos = line.IndexOf("958 ");
        if (pos > -1)
        {
            var fn = line.Substring(pos + 4);
            var path = Path.Combine(sysKeyFolder, fn);
            Console.Write(fn);
            if (File.Exists(path)) 
            {
                File.Delete(path);
                Console.WriteLine(" deleted");
            }
            else 
                Console.WriteLine(" not found");
        }
    }
}

經過漫長的等待,最後成功取回 30G 空間。

有人好奇 752 萬個檔案要花多久刪完嗎?

答案:16 小時。

Experience of cleaning the folder with 7.5 millions files with DIR and .NET program.


Comments

# by Jack

mkdir C:/emptyfolder robocopy C:/emptyfolder C:/destinationfolder /mir /MT:128 解釋 利用空資料夾以鏡像方式清理目標資料夾,用128執行緒進行清理作業(CPU使用量UPUP)

# by Jeffrey

to Jack, 妙招,感謝分享。缺點是會完全清空,若要用於本案例,要反向操作把欲保留檔案放入 emptyfolder 中。

# by 阿光

感謝黑暗大大,我最近是遇到一個主目錄裡面有12萬個資料夾裡面不知道有多少個檔案要整理,也是檔案總管一進去就!#@#$%^&,

# by HiTeQ

dir /a /os > syskeys.txt 在產出清單的時候,以檔案大小排序, 然後,使用 Notepad++ 把檔案大小不等於 958 的都刪除, 剩下的就無腦全部交給 Delete 指令。 擔心檔案名稱出現 958 這個關鍵字, 造成程式誤判,導致誤刪 缺點:人工作業稍微多一點

# by 8BQ

除非設備除了你會操作外,若有其他用戶也在用該設備,那你在Robocopy中下 /MT:128 這變數量,保證卡死的不只是DISK IO...

# by Jeffrey

to HiTeQ, 有幾個小問題:dir /a /os,得把全部檔案查出來再排序,會卡住很久看不到進度,匯出的 txt 近 1GB,用 Notepad++ 編輯也會很吃力。誤判的確要小心,本案是因為檔名規則一致,判斷條件算簡單。

# by Jeffrey

to 8BQ,我也有點好奇,刪檔作業的瓶頸應在 Disk IO,開多 Thread 的效能提升效果不知如何。

# by Anonymous

Git Bash find /c/ProgramData/Microsoft/Crypto/SystemKeys/ -type f -size 958c

# by Vt

DIR如果只輸出到檔案的話會比輸出到螢幕快一些,不知道百萬級別的檔案數是不是也是這樣

# by Jeffrey

to Vt, 我的理論:對我這種急性子,能看到進度就感覺比較快,執行後大半天沒回應的都慢到靠北 https://blog.darkthread.net/blog/ps-report-progress/

# by zero

其實你都知道有Powershell了,用DIR後面接 " | " 去比對大小,再用"|"去接刪除就可以了

# by Jack

$EmptyFolder = 'C:\EmptyFolder' $DestinationFolder = 'C:\DestinationFolder' New-Item -Path $EmptyFolder -ItemType Directory -Force Get-ChildItem -Path $DestinationFolder -Recurse | Where-Object { $_.Length -eq 958 } | Move-Item -Destination $EmptyFolder robocopy $EmptyFolder $DestinationFolder /MIR /MT:128 不好意思,昨晚沒注意到有要排除958位元組檔案的限制,單純以為要清空超多無用檔案資料夾的案子 註解 1. 建立EmptyFolder資料夾,Force可不加 2. 列出DestinationFolder資料夾(含子資料夾),過濾檔案大小為958的檔案,移動到EmptyFolder資料夾 3. 利用EmptyFolder資料夾以鏡像方式清理DestinationFolder資料夾,用128執行緒進行清理作業(非離峰時間建議移除或調低MT,用CPU和IO換執行速度)

# by Vt

寫1G的檔案花不了硬碟多少時間的,你用的硬碟多快算一下就知道了,單位應該還能用秒吧 讓螢幕顯示750萬行你根本不會去看也不需要的資料,真的很浪費時間 你都願意花這麼多時間等螢幕顯示750萬行資料了,你現在有時間的話可以再多花個幾秒鐘試試你的硬碟有多快 dir c:\ /s/a >list.txt 不過就算省了這一小時,後面還有刪檔案的十幾小時呢 ^_^;;;

# by Jeffrey

to Vt, DIR 750 萬筆瓶頸在查詢(它不是循序讀取)而不是寫入,導到螢幕的損耗應沒那麼可怕,說不定慢不到 20%(蠻有趣的題目,真的可以做實驗驗證一下)。比起看不到進度焦慮對心血管的威脅,這點成本值得 XD ** 更新 *** 一般而言,將數百萬檔案 DIR 存成檔案比導向螢幕快 N 倍,原本假設有錯,已更新於文章。

# by Anthony LEE

>>想用 .NET Directory.GetFiles() 逐一抓取檔案刪除,GetFiles() 會一次讀入完整清單,登楞! 用 DirectoryInfo.EnumerateFileSystemInfos 出 IEnumerable<System.IO.FileSystemInfo> - 可以逐個file慢慢filter入KeysToDelete.txt, 至少Win api 會如此 (另外就係Enumerate期間不要delete/modify...) >>匯出的 txt 近 1GB 大概要逐行讀, 所以grep之類這時很有用 要睇整個txt 可能vim (in Linux console)還ok...

# by Jeffrey

to Anthony LEE,EnumerateFileSystemInfos 這招好,學習了。感謝分享~

# by dir

dir 查詢不會是瓶頸,它的資料來源是 file table,而不是實際的檔案,讀取時間可以忽略不計

# by Jason Lo

刪除大量檔案,我一般使用下列兩種方式: 1、cmd下輸入del /f/q/s *.* > nul 2、安裝Cygwin,輸入linux指令 rm -rf,這種方式比較快,但需額外安裝Cygwin

Post a comment