c# 高并发下的日志记录方案 c#异步日志怎么写

来源:这里教程网 时间:2026-02-21 17:38:20 作者:

高并发下直接用

File.AppendAllText
或同步
ILogger.Log
写日志,等于给系统埋雷——不是慢,是卡死、丢日志、甚至拖垮业务线程。
真正可用的方案,必须同时满足:无锁缓冲、异步落盘、批量刷写、可控回压。下面直说怎么做。

为什么不能只用
Task.Run(() => File.AppendAllText(...))

这是最常见也最危险的“伪异步”写法。表面看不阻塞主线程,但问题一堆:

每条日志都新建一个
Task
,高并发时线程池被迅速耗尽,引发
ThreadPool.GetAvailableThreads
返回 0,后续所有异步操作排队等待
仍用
lock
保护文件写入?那只是把锁从主线程搬到了后台线程,本质还是串行,吞吐上不去
没缓冲、没批处理,磁盘 I/O 次数和请求量 1:1,SSD 都扛不住每秒几千次小写 进程崩溃时,内存里还没刷出的日志永久丢失

用 NLog 的
<asyncwrapper></asyncwrapper>
是最快落地的方案

NLog 4.0+ 原生支持零侵入异步日志,不用改一行业务代码,靠配置就能实现缓冲+批写+独立写线程:

<configuration>
  <targets>
    <target name="file" xsi:type="File"
            fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} ${uppercase:${level}} ${logger} ${message}" />
    <target name="asyncFile" xsi:type="AsyncWrapper" queueLimit="5000" timeToSleepWhenIdle="50">
      <target ref="file" />
    </target>
  </targets>
  <rules>
    <logger name="*" minlevel="Info" writeTo="asyncFile" />
  </rules>
</configuration>

关键参数说明:

queueLimit="5000"
:内存队列上限,超限时默认丢弃(可配
overflowAction="Grow"
"Block"
timeToSleepWhenIdle="50"
:空闲时线程休眠毫秒数,太小=空转耗 CPU,太大=日志延迟升高
它自动启用
BlockingCollection<logeventinfo></logeventinfo>
+ 后台专属消费者线程,全程无锁

调用时和原来完全一样:

logger.LogInformation("Order processed: {OrderId}", id);

自研轻量级异步日志器:适合不能引入 NLog 的场景

若项目受限(如嵌入式、极简部署),可用

Channel<string></string>
+ 后台
Task
实现可控、低开销的方案:

public sealed class SimpleAsyncLogger
{
    private readonly Channel<string> _channel = Channel.CreateBounded<string>(new BoundedChannelOptions(10000)
    {
        FullMode = BoundedChannelFullMode.DropOldest,
        SingleReader = true,
        SingleWriter = false
    });
    private readonly string _logPath;
<pre class='brush:php;toolbar:false;'>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 防御、以及——日志本身是否结构化(影响后续检索)。这些没对齐,再快的日志器,最后查问题时也只会让你更绝望。

相关推荐