Apache logging services logo

Apache log4net™ 手册 - 内部机制

性能

人们经常反对日志记录的一个理由是它的计算成本。这是一个合理的担忧,因为即使中等规模的应用程序也可能生成数千个日志请求。我们花费了大量精力来衡量和调整日志记录性能。log4net 声称速度快且灵活:速度第一,灵活性第二。

用户应该注意以下性能问题。

  1. 关闭日志记录时的日志记录性能。

    当完全关闭日志记录或仅关闭一组级别时,日志请求的成本包括方法调用加上一个整数比较。

    但是,方法调用涉及参数构造的“隐藏”成本。

    例如,对于某个日志记录器 log,写入:

    log.Debug("Entry number: " + i + " is " + entry[i].ToString());

    会产生构造消息参数的成本,即无论消息是否会被记录,都要将整数 ientry[i] 转换为字符串,并将中间字符串连接起来。这种参数构造的成本可能相当高,并且取决于所涉及的参数数量和类型。

    为了避免参数构造成本,请写入

    if(log.IsDebugEnabled)
    {
        log.Debug("Entry number: " + i + " is " + entry[i].ToString());
    }

    如果禁用了调试,这将不会产生参数构造的成本。另一方面,如果日志记录器启用了调试,它将产生评估日志记录器是否启用的两倍成本:一次在 IsDebugEnabled 中,一次在 Debug 中。这是一个微不足道的开销,因为评估日志记录器大约需要实际记录时间的 1%。

    某些用户采用预处理或编译时技术来编译所有日志语句。这在日志记录方面带来了完美的性能效率。但是,由于生成的应用程序二进制文件不包含任何日志语句,因此无法为该二进制文件启用日志记录。许多人认为,为了换取微小的性能提升,付出如此大的代价是不值得的。

  2. 当启用日志记录时,决定是否记录的性能。

    这本质上是遍历日志记录器层次结构的性能。当启用日志记录时,log4net 仍然需要将日志请求的级别与请求日志记录器的级别进行比较。但是,日志记录器可能没有分配的级别;它们可以从日志记录器层次结构继承级别。因此,在继承级别之前,日志记录器可能需要搜索其祖先。

    我们已经做出了认真的努力,使这种层次结构遍历尽可能快。例如,子日志记录器仅链接到其现有的祖先。在前面显示的 BasicConfigurator 示例中,名为 Com.Foo.Bar 的日志记录器直接链接到 root 日志记录器,从而绕过了不存在的 ComCom.Foo 日志记录器。这显着提高了遍历速度,尤其是在“稀疏”层次结构中。

    遍历层次结构的典型成本通常比完全关闭日志记录时慢 3 倍。

  3. 实际输出日志消息

    这是格式化日志输出并将其发送到目标位置的成本。在这里,我们也做出了认真的努力,使布局(格式化程序)尽可能快地执行。附加程序也是如此。

尽管 log4net 具有许多功能,但其首要设计目标是速度。一些 log4net 组件已经多次重写以提高性能。尽管如此,贡献者经常提出新的优化方法。您应该很高兴知道,当与 SimpleLayout 配置时,性能测试表明 log4net 的日志记录速度与 System.Console.WriteLine 的速度在一个数量级内。

日志事件流

以下是消息在记录时经过的一系列步骤和检查。为了说明这个例子,我们将记录一个在日志记录器 ConsoleApp.LoggingExample 上记录的 INFO 级别的消息。此日志记录器配置为使用 log4net.Appender.ConsoleAppender。此示例中使用的存储库是 log4net.Repository.Hierarchy 对象。

  1. 用户使用 ILog.Info 方法在日志记录器上记录消息,该日志记录器是通过调用 log4net.LogManager.GetLogger("ConsoleApp.LoggingExample") 获取的。例如:log4net.LogManager.GetLogger("ConsoleApp.LoggingExample").Info("Application Start"); ILog 接口实际上是 log4net 的扩展,它提供了特定于级别的日志记录方法(即 Debug、Info、Warn、Error 和 Fatal)。

  2. 然后,消息通过适当的 log4net.Repository.Hierarchy.Logger 对象上的 ILogger.Log 方法进行记录。ILogger.Log 方法将要记录的 Level 作为参数,因此适用于所有级别。

  3. 将存储库阈值级别与消息级别进行比较,以确定是否可以记录消息。如果消息级别低于阈值级别,则不会记录消息。在本例中,存储库是 log4net.Repository.Hierarchy 对象。

  4. Logger 级别与消息级别进行比较,以确定是否可以记录消息。请注意,如果未为此 Logger 明确指定 Logger 级别,则它将从父 Logger 继承。如果消息级别低于 Logger 级别,则不会记录消息。

  5. 创建一个 LoggingEvent 实例来封装要记录的消息。

  6. 构建 Logger 的附加程序列表。这包括附加到父 Logger 的附加程序,除非被 Logger.Additivity 属性排除。

  7. LoggingEvent 对象传递给每个附加程序的 IAppender.DoAppend 方法。

对于传递 LoggingEvent 的每个附加程序,以下操作将执行

  1. 将附加程序阈值级别与消息级别进行比较,以确定是否可以记录消息。如果消息级别低于阈值级别,则不会记录消息。

  2. 如果附加程序具有过滤器链,则将 LoggingEvent 传递到过滤器链,过滤器链可以决定是否可以记录消息。

  3. 接下来,执行特定于附加程序的检查。通常,此检查将验证附加程序的所有必需属性是否已设置(例如,如果需要,则设置 Layout)。

  4. LoggingEvent 传递到特定于附加程序的 Append 方法。现在发生的事情是特定于附加程序的。

以下操作在 ConsoleAppender.Append 方法中执行

  1. ConsoleAppender 使用 Layout 将消息格式化为要显示的字符串。

  2. Layout 使用 LoggingEvent.RenderedMessage 属性获取消息对象的字符串。这将使用为消息对象类型注册的 IObjectRenderer

  3. 使用 Console.WriteLine 方法在控制台上显示消息文本。