Web アプリケーションのパフォーマンスを維持しながら、C# で大量 (>100K) のファイルを削除しますか?
-
25-09-2019 - |
質問
を削除しようとしています 大きい ある場所からのファイルの数 (大まかには 100,000 以上を意味します)。アクションは Web ページから開始されます。明らかに、私はただ使うことができます
string[] files = System.IO.Directory.GetFiles("path with files to delete");
foreach (var file in files) {
IO.File.Delete(file);
}
ディレクトリ.GetFileshttp://msdn.microsoft.com/en-us/library/wz42302f.aspx
この方法はすでに何度か投稿されています。ディレクトリ内のすべてのファイルとフォルダーを削除するにはどうすればよいですか?そしてファイル名に特定の単語が含まれている場合、ディレクトリからファイルを削除します
しかし、この方法の問題は、ファイルが 10 万個ある場合、ファイルをループする前に最初にすべてのファイルパスを生成する必要があるため、パフォーマンスの問題が発生することです。
Web ページがこれを実行するメソッドからの応答を待っている場合は、これに追加されます。ご想像のとおり、少しゴミのように見えます。
私が考えた 1 つの考えは、これを非同期 Web サービス呼び出しでラップし、完了したら Web ページに応答を返して、それらが削除されたことを通知するというものでした。delete メソッドを別のスレッドに置くとよいでしょうか?それとも、削除を実行するために別のバッチ プロセスを使用することもあるのでしょうか?
ディレクトリ内のファイルの数を数えようとするときに、ディレクトリに多数のファイルが含まれている場合にも、同様の問題が発生します。
これは少しやりすぎではないかと思いました。つまり、これに対処するもっと簡単な方法はありますか?助けていただければ幸いです。
解決
GetFiles
非常に遅いです。- Web サイトから呼び出す場合は、このトリックを実行する新しいスレッドをスローするだけで済みます。
- 一致するファイルがまだあるかどうかを返す ASP.NET AJAX 呼び出しを使用して、基本的な進行状況の更新を行うことができます。
以下の高速 Win32 ラッピングの実装 GetFiles
, 、次のような新しいスレッドと AJAX 関数と組み合わせて使用します。 GetFilesUnmanaged(@"C:\myDir", "*.txt*).GetEnumerator().MoveNext()
.
使用法
Thread workerThread = new Thread(new ThreadStart((MethodInvoker)(()=>
{
foreach(var file in GetFilesUnmanaged(@"C:\myDir", "*.txt"))
File.Delete(file);
})));
workerThread.Start();
//just go on with your normal requests, the directory will be cleaned while the user can just surf around
public static IEnumerable<string> GetFilesUnmanaged(string directory, string filter)
{
return new FilesFinder(Path.Combine(directory, filter))
.Where(f => (f.Attributes & FileAttributes.Normal) == FileAttributes.Normal
|| (f.Attributes & FileAttributes.Archive) == FileAttributes.Archive)
.Select(s => s.FileName);
}
}
public class FilesEnumerator : IEnumerator<FoundFileData>
{
#region Interop imports
private const int ERROR_FILE_NOT_FOUND = 2;
private const int ERROR_NO_MORE_FILES = 18;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool FindNextFile(SafeHandle hFindFile, out WIN32_FIND_DATA lpFindFileData);
#endregion
#region Data Members
private readonly string _fileName;
private SafeHandle _findHandle;
private WIN32_FIND_DATA _win32FindData;
#endregion
public FilesEnumerator(string fileName)
{
_fileName = fileName;
_findHandle = null;
_win32FindData = new WIN32_FIND_DATA();
}
#region IEnumerator<FoundFileData> Members
public FoundFileData Current
{
get
{
if (_findHandle == null)
throw new InvalidOperationException("MoveNext() must be called first");
return new FoundFileData(ref _win32FindData);
}
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
if (_findHandle == null)
{
_findHandle = new SafeFileHandle(FindFirstFile(_fileName, out _win32FindData), true);
if (_findHandle.IsInvalid)
{
int lastError = Marshal.GetLastWin32Error();
if (lastError == ERROR_FILE_NOT_FOUND)
return false;
throw new Win32Exception(lastError);
}
}
else
{
if (!FindNextFile(_findHandle, out _win32FindData))
{
int lastError = Marshal.GetLastWin32Error();
if (lastError == ERROR_NO_MORE_FILES)
return false;
throw new Win32Exception(lastError);
}
}
return true;
}
public void Reset()
{
if (_findHandle.IsInvalid)
return;
_findHandle.Close();
_findHandle.SetHandleAsInvalid();
}
public void Dispose()
{
_findHandle.Dispose();
}
#endregion
}
public class FilesFinder : IEnumerable<FoundFileData>
{
readonly string _fileName;
public FilesFinder(string fileName)
{
_fileName = fileName;
}
public IEnumerator<FoundFileData> GetEnumerator()
{
return new FilesEnumerator(_fileName);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class FoundFileData
{
public string AlternateFileName;
public FileAttributes Attributes;
public DateTime CreationTime;
public string FileName;
public DateTime LastAccessTime;
public DateTime LastWriteTime;
public UInt64 Size;
internal FoundFileData(ref WIN32_FIND_DATA win32FindData)
{
Attributes = (FileAttributes)win32FindData.dwFileAttributes;
CreationTime = DateTime.FromFileTime((long)
(((UInt64)win32FindData.ftCreationTime.dwHighDateTime << 32) +
(UInt64)win32FindData.ftCreationTime.dwLowDateTime));
LastAccessTime = DateTime.FromFileTime((long)
(((UInt64)win32FindData.ftLastAccessTime.dwHighDateTime << 32) +
(UInt64)win32FindData.ftLastAccessTime.dwLowDateTime));
LastWriteTime = DateTime.FromFileTime((long)
(((UInt64)win32FindData.ftLastWriteTime.dwHighDateTime << 32) +
(UInt64)win32FindData.ftLastWriteTime.dwLowDateTime));
Size = ((UInt64)win32FindData.nFileSizeHigh << 32) + win32FindData.nFileSizeLow;
FileName = win32FindData.cFileName;
AlternateFileName = win32FindData.cAlternateFileName;
}
}
/// <summary>
/// Safely wraps handles that need to be closed via FindClose() WIN32 method (obtained by FindFirstFile())
/// </summary>
public class SafeFindFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FindClose(SafeHandle hFindFile);
public SafeFindFileHandle(bool ownsHandle)
: base(ownsHandle)
{
}
protected override bool ReleaseHandle()
{
return FindClose(this);
}
}
// The CharSet must match the CharSet of the corresponding PInvoke signature
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct WIN32_FIND_DATA
{
public uint dwFileAttributes;
public FILETIME ftCreationTime;
public FILETIME ftLastAccessTime;
public FILETIME ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
他のヒント
すべてのファイルを同じディレクトリに配置できますか?
だったら、電話してみませんか Directory.Delete(string,bool)
削除したいサブディレクトリ上にありますか?
削除したいファイル パスのリストがすでにある場合は、各ファイルを手動で削除するよりも、それらを一時ディレクトリに移動してから削除する方が、実際にはより良い結果が得られる可能性があります。
乾杯、フロリアン
別のスレッドで実行するか、メッセージをキューに投稿します (おそらく MSMQ?) 別のアプリケーション (Windows サービスなど) がそのキューに登録され、コマンドを実行します (つまり、「e:\dir*.txt を削除」) を独自のプロセスで実行します。
メッセージにはおそらくフォルダー名のみが含まれるはずです。次のようなものを使用する場合 Nサービスバス およびトランザクション キューを使用すると、メッセージを投稿して、メッセージが正常に投稿された限りすぐに戻ることができます。実際にメッセージの処理に問題がある場合は、再試行され、最終的には処理が続行されます。 エラーキュー 監視してメンテナンスを行うことができます。
持っている 1000以上のファイル ディレクトリ内にあるのは大きな問題です。
現在開発段階にある場合は、 アルゴリズム これにより、ファイルがランダムなフォルダー (ルート フォルダー内) に配置され、そのフォルダー内のファイル数が確実に確保されます。 1024未満.
何かのようなもの
public UserVolumeGenerator()
{
SetNumVolumes((short)100);
SetNumSubVolumes((short)1000);
SetVolumesRoot("/var/myproj/volumes");
}
public String GenerateVolume()
{
int volume = random.nextInt(GetNumVolumes());
int subVolume = random.nextInt(GetNumSubVolumes());
return Integer.toString(volume) + "/" + Integer.toString(subVolume);
}
private static final Random random = new Random(System.currentTimeMillis());
これを行う際、ファイルを作成するたびに、それを HashMap またはリスト (パス) に同時に追加することも確認してください。次のようなものを使用してこれを定期的にシリアル化します JSON.net ファイルシステムに保存します (サービスが失敗した場合でもシリアル化された形式からファイル リストを取得できるようにするため、整合性が保たれます)。
ファイルをクリーンアップしたり、ファイル間でクエリを実行したりする場合は、 まずこの HashMap を検索します または、リストしてからファイルに動作します。これよりも優れています System.IO.Directory.GetFiles
その後、ブートワーカースレッドに出て仕事とユーザへのあなたの応答を返します。
私はあなたが同じ仕事をして、複数のスレッドの実行を停止するには、「ビッグ削除仕事」をやっていると言うのアプリケーション変数までフラグを思います。その後、あなたが望んでいた場合、これまであまりにも削除されたファイルの数の進捗状況の更新を与えることができる別のページをポーリングすることができますか?
ただ、クエリが、なぜこれほど多くのファイル?
あなたが後ろにあなたのaspxコードに簡単なAjaxのWebMethod属性を作成して、JavaScriptでそれを呼び出すことができます。
は最良の選択(私見)は、ファイルをカウントし、そうでない場合は、ブラウザのタイムアウトの問題を得る可能性がありますポーリングによって、進行状況を確認/削除する別々のプロセスを作成することです。
うわー。私はあなたが削除の世話をして、いくつかの他のサービスまたはエンティティを持って正しい軌道に乗って間違いだと思います。際に、あなたはまた、削除のプロセスを追跡し、非同期のJavaScriptを使用して、ユーザーに結果を示すためのメソッドを提供することができるようにます。
他の人が別のプロセスでこれを入れて言ったように素晴らしいアイデアです。あなたは、IISは、このような長時間実行ジョブを使用してリソースを占有したくありません。そうするためのもう一つの理由はセキュリティです。あなたはあなたの仕事のプロセスを提供したくない場合がありますそのディスクから削除ファイルへの能力ます。
私はそれが古いスレッドです知っているが、月Jongboomの答えに加えて、私は非常にパフォーマンスの高い、より普遍的である同様のソリューションを提案しています。私の解決策を迅速長いファイル名(> 255文字)をサポートしてDFSにディレクトリ構造を削除するために建設されました。
第1の相違点は、DLLインポート宣言です。
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr FindFirstFile(string lpFileName, ref WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool FindNextFile(IntPtr hDindFile, ref WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MashalAs(UnmanagedType.Bool]
static extern bool DeleteFile(string lpFileName)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MashalAs(UnmanagedType.Bool]
static extern bool DeleteDirectory(string lpPathName)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool FindClose(IntPtr hFindFile);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLAstError = true)]
static extern uint GetFileAttributes(string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLAstError = true)]
static extern bool SetFileAttributes(string lpFileName, uint dwFileAttributes);
はWIN32_FIND_DATA構造は若干異なっている
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode), Serializable, BestFitMapping(false)]
internal struct WIN32_FIND_DATA
{
internal FileAttributes dwFileAttributes;
internal FILETIME ftCreationTime;
internal FILETIME ftLastAccessTime;
internal FILETIME ftLastWriteTime;
internal int nFileSizeHigh;
internal int nFileSizeLow;
internal int dwReserved0;
internal int dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
internal string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
internal string cAlternative;
}
パスは以下のように調製する必要が長いパスを使用するために
をpublic void RemoveDirectory(string directoryPath)
{
var path = @"\\?\UNC\" + directoryPath.Trim(@" \/".ToCharArray());
SearchAndDelete(path);
}
ここでとは、主な方法です。
private void SearchAndDelete(string path)
{
var fd = new WIN32_FIND_DATA();
var found = false;
var handle = IntPtr.Zero;
var invalidHandle = new IntPtr(-1);
var fileAttributeDir = 0x00000010;
var filesToRemove = new List<string>();
try
{
handle = FindFirsFile(path + @"\*", ref fd);
if (handle == invalidHandle) return;
do
{
var current = fd.cFileName;
if (((int)fd.dwFileAttributes & fileAttributeDir) != 0)
{
if (current != "." && current != "..")
{
var newPath = Path.Combine(path, current);
SearchAndDelete(newPath);
}
}
else
{
filesToRemove.Add(Path.Combine(path, current));
}
found = FindNextFile(handle, ref fd);
} while (found);
}
finally
{
FindClose(handle);
}
try
{
object lockSource = new Object();
var exceptions = new List<Exception>();
Parallel.ForEach(filesToRemove, file, =>
{
var attrs = GetFileAttributes(file);
attrs &= ~(uint)0x00000002; // hidden
attrs &= ~(uint)0x00000001; // read-only
SetFileAttributes(file, attrs);
if (!DeleteFile(file))
{
var msg = string.Format("Cannot remove file {0}.{1}{2}", file.Replace(@"\\?\UNC", @"\"), Environment.NewLine, new Win32Exception(Marshal.GetLastWin32Error()).Message);
lock(lockSource)
{
exceptions.Add(new Exceptions(msg));
}
}
});
if (exceptions.Any())
{
throw new AggregateException(exceptions);
}
}
var dirAttr = GetFileAttributes(path);
dirAttr &= ~(uint)0x00000002; // hidden
dirAttr &= ~(uint)0x00000001; // read-only
SetfileAttributtes(path, dirAttr);
if (!RemoveDirectory(path))
{
throw new Exception(new Win32Exception(Marshal.GetLAstWin32Error()));
}
}
もちろん、私たちはその方法の別のリストの外に、さらに、店舗のディレクトリを移動して、後でこのようになります。別の方法でそれらを削除することができます:
private void DeleteDirectoryTree(List<string> directories)
{
// group directories by depth level and order it by level descending
var data = directories.GroupBy(d => d.Split('\\'),
d => d,
(key, dirs) => new
{
Level = key,
Directories = dirs.ToList()
}).OrderByDescending(l => l.Level);
var exceptions = new List<Exception>();
var lockSource = new Object();
foreach (var level in data)
{
Parallel.ForEach(level.Directories, dir =>
{
var attrs = GetFileAttributes(dir);
attrs &= ~(uint)0x00000002; // hidden
attrs &= ~(uint)0x00000001; // read-only
SetFileAttributes(dir, attrs);
if (!RemoveDirectory(dir))
{
var msg = string.Format("Cannot remove directory {0}.{1}{2}", dir.Replace(@"\\?\UNC\", string.Empty), Environment.NewLine, new Win32Exception(Marshal.GetLastWin32Error()).Message);
lock (lockSource)
{
exceptions.Add(new Exception(msg));
}
}
});
}
if (exceptions.Any())
{
throw new AggregateException(exceptions);
}
}
バックエンドを高速化するためのいくつかの改善:
使用
Directory.EnumerateFiles(..)
:これにより、ファイルが反復処理されます すべてのファイルが取得された後を待たずに。使用
Parallel.Foreach(..)
:これにより、ファイルも同時に削除されます。
高速になるはずですが、どうやら HTTP リクエストは多数のファイルでタイムアウトになるようです。そのため、バックエンド プロセスを別のワーカー スレッドで実行し、終了後に結果を Web クライアントに通知する必要があります。