【问题标题】:Logging frameworks and multithread compatibility日志框架和多线程兼容性
【发布时间】:2011-06-28 15:47:37
【问题描述】:

请注意,我在 SO(讨论 log4net 的线程安全)上看到了这些问题,我怀疑他们回答了我的问题,但我还是要问:

Multithread safe logging

Log4Net FileAppender not thread safe?

最近我编写了一个用于日志记录的 WCF 服务。这个想法与Clog(或look for Clog under Calcium)非常相似。基本上,我已经实现了一个用于 Silverlight 客户端(Silverlight 类库)的日志记录 API。日志 API 或多或少是 Common.Logging for .NET API 的克隆,我们在应用程序的其他地方使用它。 API 的实现将所有日志消息转发到 WCF 日志服务,该服务本身是根据 Common.Logging 实现的。

在查看 Clog 时,我发现 Log4NetStrategy 类中的以下代码让我觉得有点奇怪:

/// <summary> 
/// All Write log calls are done asynchronously because the DanielVaughan.Logging.Log 
/// uses the AppPool to dispatch calls to this method. Therefore we need to ensure 
/// that the call to Log is threadsafe. That is, if an appender such as FileAppender is used, 
/// then we need to ensure it is called from no more than one thread at a time. 
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="loggingEvent">The logging event.</param>
[MethodImpl(MethodImplOptions.Synchronized)]
static void WriteThreadSafe(ILogger logger, LoggingEvent loggingEvent)
{
  logger.Log(loggingEvent);
}

Log4NetStrategy(以及 NLog 和 EnterpriseLibrary 的策略)实现了一个接口,该接口具有类似于 Write(ILogEntry logEntry) 的方法。 ILogEntry 本质上是 Clog 日志记录服务从客户端接收的 DTO。提取来自 logEntry 的信息并用于创建 log4net LoggingEvent。还会根据 ILogEntry DTO 中的记录器名称检索适当的 log4net 记录器。 log4net LoggingEvent 创建后,它和记录器被发送到上面的WriteThreadSafe 方法并通过log4net 记录。 NLog 和 EnterpriesLibrary 的实现类似。

在伪代码中,Clog 日志记录服务上的“Write”方法如下所示:

// Write on the logging service
// Send the input log entry to each configured strategy.
public void Write(ILogEntry logEntry)
{
  foreach (ILoggingStrategy ls in loggingStrategies)
  {
    ls.Write(logEntry);
  }
}

// Write on the Log4NetStrategy
// Convert the logEntry to log4net form and log with log4net
public void Write(ILogEntry logEntry)
{
  log4net.LoggingEvent le = ConvertToLoggingEvent(logEntry);
  ILog logger = log4net.GetLogger(logEntry.Logger);
  WriteThreadSafe(logger, le);
}

所以这是我的问题... 通常 log4net(和 NLog 以及我认为 EnterpriseLibrary)被认为是多线程兼容的。也就是说,公共 API 的用户可以简单地调用 log.Info、log.Log 等,而不用担心在多线程环境中运行。日志框架应该注意确保日志调用(以及日志调用中的任何处理)是线程安全的。

如果日志框架是多线程兼容的,那么就是使用

[MethodImpl(MethodImplOptions.Synchronized]

属性真的需要吗?似乎这会(或可能)通过强制同步处理所有日志消息而导致瓶颈,即使底层日志框架应该能够在多线程环境中处理日志。

就我的日志服务(可能是多线程的)而言,似乎没有必要像这样同步调用。似乎我应该能够从服务调用中获取我的日志输入,构建适当的日志结构(基于底层日志框架),然后记录它。如果我的日志服务是多线程的,它应该“正常工作”,因为底层的日志框架应该支持它(多线程)。

这有意义吗?日志调用的显式同步(至少对于 log4net 和 NLog)真的有必要吗?

【问题讨论】:

  • 像 ILogger 这样的接口类型不可能对线程安全做出强有力的保证。除非它具有 IsThreadSafe 属性。
  • 我想我更具体地询问 log4net 和 NLog。如果在上面的示例中,日志记录“策略”是根据 log4net 和/或 NLog 实现的(因此 Clog 是一种抽象,可以配置为使用 log4net 或 NLog - 或两者)并且如果 log4net 和 NLog 都是线程安全(正如他们的网站所说的那样),明确强制“记录器”一次只能由一个线程写入有什么好处?这似乎相当于用这样的锁“保护”每个日志记录调用(在任何使用 log4net 或 NLog 的代码中):
  • lock(globalLoggerLocker) { logger.Info("Hello from protected logger");并且没有人这样做。在给定的框架(如 log4net)中,某些组件可能不是线程安全的(根据某些文档 log4net 的 FileAppender 不是线程安全的),但至少 log4net 和 NLog 宣传为线程安全的,因此它们必须处理使用所需的任何同步非线程安全组件。

标签: .net multithreading logging log4net nlog


【解决方案1】:

为什么是锁?

据我所知,在企业库中使用具有 IsThreadSafe 属性的 .NET Tracelistener 类。如果侦听器是线程安全的,则不执行同步。 对于作为输出设备的文件,没有理由同步来自多个线程的访问,除非您的内部保存写缓冲区的StreamWriter 可能被不同步的写入损坏。这就是为什么需要synchronized StreamWriter 才能从不同线程写入文件的原因。

但无论如何,这更多的是学术兴趣。当您一次只允许一个线程写入时,您仍然可以生成数百 MB 的输出,即使是最快的硬盘也会崩溃。当您写入文件时,您会受到 IO 限制。整个链条中最慢的事情是硬盘,而不是您对文件的同步写入。 但有时将多个进程的输出放在一个文件中会很好。 Log4Net 例如cannot 这样做。

...如何让多个进程登录 到同一个文件?

FileAppender 持有一个写锁 记录时的日志文件。这 阻止其他进程写入 到文件,因此它不是 可能有多个进程 直接登录到同一个日志文件, 即使他们在同一台机器上。 ...

格式化性能

即使您认为锁定会影响您的性能,也常常会忽略其他成本。可配置的输出格式非常好,但至少在 Enterprise Library 4.0 之前的格式是very slow。我将其速度提高了 13 倍以上,从而使净文件吞吐量增加了 2 倍。

并发写入文件

借助一点 Win32 魔法,possible 可以将不同进程的可靠数据同时写入同一个文件。 但是,如果需要同步,请回到您的问题:可能需要根据输出设备。

可扩展性

如果您关心的是可扩展性,那么您需要有一个在定义的时间点合并的每个线程缓冲区,以便快速收集数据。然后,您可以实现一个全局环形缓冲区,该缓冲区从所有线程获取格式化数据,其中最旧的数据被覆盖。当发生有趣的事情时,您可以将整个环形缓冲区转储到磁盘。这种设计比任何文件附加器快约 10-20 倍。 Windows Event Tracing 确实使用了非常快的环形缓冲区技巧。

既然您询问了锁定问题,我确实认为 速度 是您主要关心的问题。如果该假设是正确的,您应该查看一个 tracing framework,就像 ApiChange 工具使用的那个。它具有一些创新方法,例如当您的方法出现异常时自动跟踪异常或fault injection 对错误路径进行单元测试,而无需修改您的产品代码。

为了可扩展,您应该区分日志记录和跟踪,正如我在此处解释的那样:Logging is not Tracing.

无锁

如果您想创建一个可扩展的跟踪框架,您需要将一次跟踪调用的所有数据作为不可变数据传递,将数据格式化为字符串并将其传递到您的输出设备。它可以像(C#伪代码)一样简单

  1. Trace.Info("blahh {0}", data);
  2. traceString = Format(formatString, args, CurrentTime);
  3. 写入(traceString);

当数据未存储在类中或类成员在初始化后未更改时,您将永远不需要锁。您必须小心分享,例如线程之间的格式化缓冲区。为了具有可扩展性,您需要按以下顺序设置线程安全的设计优先级。

  1. 隔离 这是迄今为止最好的解决方案。如果可以以多个线程可以独立操作的方式对问题进行切片,则您永远不需要考虑锁定或共享数据。
  2. 不变性 第二个最佳解决方案是共享数据,但是一旦多个线程可以“看到”它,它就永远不能改变。这可以实现,例如使用只有 getter 的对象,所有数据都通过 ctor 传递。
  3. 锁定 如果您使用无锁或某些锁定策略并没有多大关系。您处于痛苦的世界中,您必须注意处理器缓存,false sharing,如果您勇敢,您甚至可以尝试一些无锁方法,结果却发现它充其量是普通锁的两倍。在某些情况下,无锁代码可能会更慢。

如果您想了解更多关于线程的详细信息,我推荐Joe Duffys blog

追踪策略

要编写可维护的软件,您需要能够遵循您的应用程序和数据流。只有这样,您才能诊断仅在您的客户现场发生的问题。用痕迹“弄乱”你的代码确实是值得的,这些痕迹确实为你提供了足够的信息来快速诊断问题。您可以跟踪太多或太少或不相关的数据。一条确实相关的良好跟踪线,例如您的 XPath 查询字符串及其操作的文件作为日志文件中分散的信息要好得多。

你的, 阿洛伊斯克劳斯

【讨论】:

  • 感谢您的详细解答。实际上,我已经阅读了您关于提高企业库日志格式的格式化性能的博客文章。干得好!我想我的问题实际上是关于(正如我在上面对@Hans Passant 评论的回复中所详述)通过只允许一个线程一次调用日志框架(通过方法上的 Synchronized 属性)显式地强制“线程安全” ) 当日志框架(特别是 log4net 和 NLog)宣传为线程安全时。上面来自 Clog 的代码示例似乎等同于像这样使用 log4net:
  • lock (globalLogLocker) { logger.Info("Hello from protected logger");与 I/O 相比,使用额外的锁(或通过方法属性进行同步)可能会或可能不会在性能方面付出代价——正如你所指出的——但我的问题是显式是否有任何好处使用“线程安全”日志框架(如 log4net 和 NLog)时的同步。我没有经常使用 System.Diagnostics,但我想很少有人这样编写日志调用: TraceSource ts = new TraceSource("aa");锁(globalLoggerLocker){ ts.Info(“abc”); } 在
  • 为了保证线程安全,即使底层 TraceListener 不是线程安全的(或者 IsThreadSafe 属性设置不正确)。
  • 当你建立一个日志框架(如 NLog 或类似的东西)时,我看不出为什么需要或需要全局锁的原因。一个写得不好的 appender 可能会导致消息重新排序,但对于大的 appender 不应该是这种情况。我唯一能想到的是两个 Web 服务附加程序,它们确实竞争线程池线程以异步记录到他们的 Web 服务器。然后可能会发生消息在一个日志中出现的顺序与在另一个 Web 服务中出现的顺序不同,具体取决于哪些消息先到达。
猜你喜欢
  • 2011-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-20
  • 2010-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多