不能用File.ReadAllLines读大日志文件,因其会将整个文件加载到内存导致OutOfMemoryException;应使用FileStream反向扫描换行符,逐字节从末尾向前读取并累计行数,兼容rn和n,注意文件共享、编码及并发写入问题。

为什么不能用 File.ReadAllLines 读大日志文件
直接加载整个文件到内存会触发 OutOfMemoryException,尤其当日志超几百 MB 甚至 GB 时。windows 下单个 .NET 进程在 32 位模式下默认只能用 2GB 虚拟地址空间,64 位虽宽裕但依然扛不住几十 GB 的日志。更关键的是,你只关心最后几行,却把前面几百万行全读进来再丢弃——纯属浪费 CPU 和 GC 压力。
用 FileStream 从文件末尾反向扫描换行符
核心思路是:打开文件流,Seek 到末尾,然后逐字节往前读,统计 'n'(unix/linux)或 "rn"(Windows)出现的次数,直到凑够目标行数或碰到文件开头。注意必须用 FileStream + BinaryReader 或手动 ReadByte,不能用 StreamReader,因为它内部缓冲机制会破坏反向定位逻辑。
常见坑:
-
File.OpenRead(path)默认不支持Seek,得用FileMode.Open+FileAccess.Read+FileShare.Read显式打开 - Windows 日志可能含
rn,Linux 是n,需兼容判断;若文件以rn结尾,末尾可能多出一个空行,要跳过 - 文件编码不确定时,别假设是 UTF8;建议按字节处理换行符,行内容再用
Encoding.default或Encoding.UTF8解码(日志通常不带 bom,UTF8更安全)
封装成可复用的 ReadTailLines 方法
下面是一个轻量实现,返回 IEnumerable<string></string>,支持延迟执行、避免一次性分配大数组:
public static IEnumerable<string> ReadTailLines(string path, int lineCount = 10) { using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan); if (fs.Length == 0) yield break; <pre class="brush:php;toolbar:false;">var buffer = new byte[1]; long pos = fs.Length - 1; int linesFound = 0; var lineBuilder = new List<byte>(); while (pos >= 0 && linesFound < lineCount) { fs.Seek(pos, SeekOrigin.Begin); fs.Read(buffer, 0, 1); if (buffer[0] == 'n' || (buffer[0] == 'r' && pos > 0 && fs.ReadByte() == 'n')) { if (lineBuilder.Count > 0) { linesFound++; yield return Encoding.UTF8.GetString(lineBuilder.AsReadOnly().ToArray()); lineBuilder.Clear(); } // 跳过 rn 两个字节 if (buffer[0] == 'r') pos--; } else if (pos == 0 || buffer[0] == 'r') { // 文件开头或孤立 r,也视为一行结束 if (lineBuilder.Count > 0 || pos == 0) { linesFound++; if (pos == 0 && buffer[0] != 'n' && buffer[0] != 'r') lineBuilder.Add(buffer[0]); yield return Encoding.UTF8.GetString(lineBuilder.AsReadOnly().ToArray()); } } else { lineBuilder.Insert(0, buffer[0]); } pos--; } // 处理第一行(文件开头没换行的情况) if (linesFound < lineCount && lineBuilder.Count > 0) { yield return Encoding.UTF8.GetString(lineBuilder.ToArray()); }
}
生产环境要注意文件被其他进程写入
日志文件常被追加写入,FileStream 打开时若没加 FileShare.Write,可能抛 IOException;但加了之后,需接受“读到半截行”的风险——比如某次写入正在写入第 1001 行中间,你刚好扫到那里,就可能解码失败或得到乱码。解决办法只有两个:
- 捕获
DecoderFallbackException或ArgumentException,对异常行用Encoding.GetChars加容错解码 - 更稳妥的做法是:先
GetLastWriteTime记录时间戳,再用FileSystemWatcher监听Changed事件,仅当文件大小增长且修改时间更新后才重新读尾部——这适合轮询场景,避免高频扫描
真正难处理的是多进程并发写同一个日志文件(如多个服务实例共用一个 log.txt),此时连文件长度都可能不准;这种架构本身就有问题,优先考虑换成按日期分片或用 ConcurrentQueue + 单独日志线程落盘。