高并发日志必须采用无锁缓冲、异步落盘、批量刷写、可控回压方案;NLog的AsyncWrapper配置或基于Channel自研轻量日志器可满足,禁用File.AppendAllText和伪异步Task.Run。

高并发下直接用 File.AppendAllText 或同步 ILogger.Log 写日志,等于给系统埋雷——不是慢,是卡死、丢日志、甚至拖垮业务线程。真正可用的方案,必须同时满足:无锁缓冲、异步落盘、批量刷写、可控回压。下面直说怎么做。
为什么不能只用 Task.Run(() => File.AppendAllText(...))
这是最常见也最危险的“伪异步”写法。表面看不阻塞主线程,但问题一堆:
- 每条日志都新建一个
Task,高并发时线程池被迅速耗尽,引发ThreadPool.GetAvailableThreads返回 0,后续所有异步操作排队等待 - 仍用
lock保护文件写入?那只是把锁从主线程搬到了后台线程,本质还是串行,吞吐上不去 - 没缓冲、没批处理,磁盘 I/O 次数和请求量 1:1,SSD 都扛不住每秒几千次小写
- 进程崩溃时,内存里还没刷出的日志永久丢失
用 NLog 的 是最快落地的方案
NLog 4.0+ 原生支持零侵入异步日志,不用改一行业务代码,靠配置就能实现缓冲+批写+独立写线程:
关键参数说明:
-
queueLimit="5000":内存队列上限,超限时默认丢弃(可配overflowAction="Grow"或"Block") -
timeToSleepWhenIdle="50":空闲时线程休眠毫秒数,太小=空转耗 CPU,太大=日志延迟升高 - 它自动启用
BlockingCollection+ 后台专属消费者线程,全程无锁
调用时和原来完全一样:logger.LogInformation("Order processed: {OrderId}", id);
自研轻量级异步日志器:适合不能引入 NLog 的场景
若项目受限(如嵌入式、极简部署),可用 Channel + 后台 Task 实现可控、低开销的方案:
public sealed class SimpleAsyncLogger
{
private readonly Channel _channel = Channel.CreateBounded(new BoundedChannelOptions(10000)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
private readonly string _logPath;
public SimpleAsyncLogger(string logPath) => _logPath = logPath;
public void Log(string msg) => _channel.Writer.TryWrite($"[{DateTime.Now:HH:mm:ss.fff}] {msg}");
public async Task StartAsync(CancellationToken ct)
{
await foreach (var line in _channel.Reader.ReadAllAsync(ct))
{
try
{
await File.AppendAllTextAsync(_logPath, line + Environment.NewLine, ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
catch (IOException) { /* 磁盘满/权限错等,可降级到 Console 或丢弃 */ }
}
}}
使用要点:
- 启动时调用
StartAsync 并 长期持有该 Task 引用(比如注册为 IHostedService),别让它被 GC
-
FullMode = DropOldest 防止突发流量打爆内存;若需保序保全,改用 Wait 模式并监控队列长度
- 不要在
Log() 方法里 await —— 它必须是纯内存入队,否则就退化成同步了
真正的难点不在“怎么写异步”,而在“怎么不让异步变成新瓶颈”:队列大小、刷盘频率、错误隔离、OOM 防御、以及——日志本身是否结构化(影响后续检索)。这些没对齐,再快的日志器,最后查问题时也只会让你更绝望。









