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

为什么不能用 File.ReadAllLines 读大日志文件
直接加载整个文件到内存会触发 OutOfMemoryException,尤其当日志超几百 MB 甚至 GB 时。Windows 下单个 .NET 进程在 32 位模式下默认只能用 2GB 虚拟地址空间,64 位虽宽裕但依然扛不住几十 GB 的日志。更关键的是,你只关心最后几行,却把前面几百万行全读进来再丢弃——纯属浪费 CPU 和 GC 压力。
用 FileStream 从文件末尾反向扫描换行符
核心思路是:打开文件流,Seek 到末尾,然后逐字节往前读,统计 '\n'(Unix/Linux)或 "\r\n"(Windows)出现的次数,直到凑够目标行数或碰到文件开头。注意必须用 FileStream + BinaryReader 或手动 ReadByte,不能用 StreamReader,因为它内部缓冲机制会破坏反向定位逻辑。
常见坑:
-
File.OpenRead(path)默认不支持Seek,得用FileMode.Open+FileAccess.Read+FileShare.Read显式打开 - Windows 日志可能含
\r\n,Linux 是\n,需兼容判断;若文件以\r\n结尾,末尾可能多出一个空行,要跳过 - 文件编码不确定时,别假设是 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();
}
// 跳过 \r\n 两个字节
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 + 单独日志线程落盘。










