长尾延迟到底卡在哪个环节?先用 Stopwatch
和 ETW 定位真实瓶颈
文件操作的 P99 延迟高,不等于磁盘慢——更可能是线程阻塞、AV 软件扫描、防病毒实时监控、或 NTFS 日志刷盘行为在后台拖慢个别请求。直接看
File.ReadLines耗时没用,得测端到端路径中真正被挂起的时间点。
实操建议:
用Stopwatch包裹实际业务逻辑(比如打开 + 读取前 1KB + 关闭),不是只测
File.OpenRead同时开启 Windows ETW trace:
logman start fileio -p "Microsoft-Windows-Kernel-FileIO" 0x10000 -o fileio.etl -ets,之后用
Windows Performance Analyzer查看单次
CreateFile或
ReadFile是否被
FltMgr(过滤驱动)或
avp(卡巴斯基等)长时间拦截 避开杀毒软件默认监控目录(如
%USERPROFILE%\Documents),改用临时目录
Path.GetTempPath()复现,对比延迟是否骤降
FileStream
的 Buffered
和 Unbuffered
怎么选?
默认构造的
FileStream是缓冲的,但缓冲区大小、是否启用 OS 缓存、同步/异步模式三者叠加后,P99 表现差异极大。尤其在小文件高频读写场景下,
FileOptions.Asynchronous不等于“不卡主线程”,它只是把 IO 提交到线程池,底层仍可能因未完成的
WriteFile调用而阻塞后续请求。
实操建议:
对小文件(bufferSize = 4096,禁用 OS 缓存(加FileOptions.NoBuffering)反而更稳——避免 OS 层面 page fault 或 dirty page 回写抖动 对大文件顺序读:保持默认缓冲(
bufferSize = 8192),但必须配
FileOptions.SequentialScan,让 Windows 预读逻辑生效,否则 P99 可能因某次缺页中断飙升 绝对不要混用
FileOptions.Asynchronous和
FileOptions.WriteThrough——后者强制绕过所有缓存直写磁盘,前者又依赖系统完成端口,两者冲突会导致完成通知延迟不可控
为什么 Directory.GetFiles
在百万级文件目录下 P99 突增到秒级?
Directory.GetFiles底层调用的是
FindFirstFileEx,它需要一次性枚举并加载全部匹配项到内存再返回数组。当目录含几十万文件时,光是字符串分配和 GC 就能吃掉几百毫秒,且该方法不支持流式遍历或超时控制。
实操建议:
替换为Directory.EnumerateFiles,它返回
IEnumerable<string></string>,按需迭代,内存友好,P99 更平滑 若需过滤+排序,别链式调用
.Where(...).OrderBy(...)——这会强制全量枚举后再筛选;改用
foreach (var f in Directory.EnumerateFiles(...)) { if (MeetsCondition(f)) Process(f); }
避免通配符如 "*.*",改用更窄的模式(如
"*.log"),减少内核层文件名比对开销
异步文件 API(ReadAsync
/WriteAsync
)真的能压低 P99 吗?
不能一概而论。.NET 6+ 的
FileStream.ReadAsync在 Windows 上默认走 I/O Completion Ports(IOCP),但前提是文件句柄以
FILE_FLAG_OVERLAPPED打开——而
File.OpenRead(path)默认不带这个标志。结果就是看似 async,实际仍是同步阻塞调用,只是包装了一层
Task。
实操建议:
手动创建FileStream:用
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true),其中
useAsync: true才确保底层启用 overlapped IO 注意:
useAsync: true会禁用
FileStream内部缓冲(即
bufferSize仅用于托管层暂存),所以要自己权衡缓冲区大小 别在
async void方法里调用文件 async API——异常会直接崩掉进程,P99 统计就失去意义
最麻烦的其实是 UNC 路径和符号链接:它们会让 async 文件操作退化为同步,且不报错。如果业务依赖网络共享,务必在目标路径上跑一遍
fsutil behavior query disablelastaccess和
fsutil behavior set disablelastaccess 1,关掉最后访问时间更新,这是隐藏的 P99 杀手。
