IO 队列必须自己控,ThreadPool
默认调度扛不住突发写入
Windows 默认线程池对短时密集
File.WriteAllText或
FileStream.Write完全不设防,尤其在日志批量刷盘、上传文件解压、监控轮询等场景下,几十个并发写操作可能瞬间拉起上百线程,争抢磁盘句柄和缓冲区,导致
IOException: The process cannot access the file because it is being used by another process或系统级
STATUS_SHARING_VIOLATION。
实操建议:
禁用直接调用Task.Run(() => File.WriteAllBytes(...))这类“看起来异步实则乱扔”的写法 用
ConcurrentQueue<action></action>+ 单后台线程(非
Task.Run)做写入节流,吞吐可控且无资源竞争 若需保序,队列元素带
TaskCompletionSource<bool></bool>,写完再
SetResult(true),避免上层盲目
await Task.Delay别依赖
async/await自动缓解 IO 压力——它只释放线程,不减少实际磁盘请求频次
FileStream
缓冲区大小不是越大越好,尤其小文件高频写
默认
FileStream缓冲区是 4KB,有人改成 1MB 想“提升性能”,结果在每秒数百个 2KB 日志条目场景下,反而因频繁触发
Flush()和内存拷贝,CPU 占用翻倍、延迟毛刺明显。
实操建议:
小文件(50 次/秒),缓冲区设为4096或
8192,匹配 NTFS 簇大小 大文件顺序写(如视频分片),可设为
65536,但必须配合
FileOptions.WriteThrough | FileOptions.SequentialScan永远显式传入
useAsync: true(即
new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous)),否则
WriteAsync会退化为同步阻塞 别复用同一个
FileStream跨长时间——句柄泄漏风险高,且 Windows 文件系统对长时打开句柄有内部锁开销
临时文件 + 原子替换比直接覆盖更稳,但要注意 MoveTo
的权限陷阱
直接
File.WriteAllText(path, content)在写入中途崩溃,会导致目标文件损坏或截断;而用
File.WriteAllText(tempPath, content); File.Move(tempPath, path)能保证原子性,但
File.Move在跨卷、NTFS 权限受限或防病毒软件拦截时会静默失败,抛出
UnauthorizedAccessException或卡住数秒。
实操建议:
临时路径必须和目标路径同盘符(用Path.GetPathRoot(target)校验),否则
MoveTo变成复制+删除,失去原子性 提前检查目标目录写权限:
new FileInfo(targetDir).Directory?.GetAccessControl().GetAccessRules(true, true, typeof(SecurityIdentifier)),比试错更可靠 防杀软干扰:临时文件名避开
*.tmp、
*.log等敏感后缀,改用带时间戳哈希的随机名,如
log_20240521_7f3a9b.tmpdata替换失败时,保留临时文件并记录完整路径——比删掉后重试更容易定位是磁盘满还是权限问题
监控不能只看 CPU 和内存,IOReadBytesPerSec
和 Handle Count
才是风暴前兆
系统看似空闲,但
Process.IOReadBytesPerSec持续 >50MB/s 或句柄数突破 5000,往往意味着文件操作已失控。此时
dotnet-counters或 PerfMon 中的
.NET CLR Memory/# of Pinned Objects也常同步飙升——大量
byte[]被 GC pinned 导致内存碎片。
实操建议:
在启动时注册AppDomain.CurrentDomain.ProcessExit和
Console.CancelKeyPress,强制清空 IO 队列并
WaitAll当前写任务,避免进程退出时丢数据 用
PerformanceCounter每 2 秒采样一次
IODataBytesPerSec,超阈值(如 30MB/s 持续 5 秒)就自动降级:暂停非关键写入、切到内存缓冲、发告警 别信“磁盘足够快就不用控速”——NVMe 盘的随机写 IOPS 上限仍是硬约束,且系统缓存压力会传导到内存和 pagefile
真正难的不是写几行节流代码,而是得想清楚哪些操作可以合并、哪些必须保序、哪些失败能容忍——这些决策点藏在业务逻辑里,没法靠通用库兜底。
