已经有一个关于这个问题的问题,但它并没有告诉我我需要知道什么:假设我有一个 Web 应用程序,并且每次往返都有很多日志记录。我不想就为什么有这么多的日志记录,或者我怎样才能做更少的登录操作展开辩论。我想知道为了使这个日志记录问题变得高效和干净,我有什么可能性。
到目前为止,我已经实现了声明性(基于属性)和命令式日志记录,这似乎是一种很酷且干净的方式......现在,我可以对性能做些什么,假设我可以预期这些日志需要更多时间超出预期。可以打开一个线程并将该工作留给它吗?
已经有一个关于这个问题的问题,但它并没有告诉我我需要知道什么:假设我有一个 Web 应用程序,并且每次往返都有很多日志记录。我不想就为什么有这么多的日志记录,或者我怎样才能做更少的登录操作展开辩论。我想知道为了使这个日志记录问题变得高效和干净,我有什么可能性。
到目前为止,我已经实现了声明性(基于属性)和命令式日志记录,这似乎是一种很酷且干净的方式......现在,我可以对性能做些什么,假设我可以预期这些日志需要更多时间超出预期。可以打开一个线程并将该工作留给它吗?
我会考虑的事情:
使用有效的文件格式来最小化要写入的数据量(例如,XML 和文本格式易于阅读,但通常效率极低 - 相同的信息可以以二进制格式存储在更小的空间中)。但是不要花费大量 CPU 时间来尝试“优化”打包数据。只需选择一种紧凑但编写速度快的简单格式。
测试对日志的压缩使用。快速 SSD 可能不是这种情况,但在大多数 I/O 情况下,压缩数据的开销小于 I/O 开销,因此压缩会带来净收益(尽管这是一种折衷方案——提高 CPU 使用率以降低 I/O /O 用法)。
只记录有用的信息。无论您认为一切有多重要,您都可能会找到一些可以删减的内容。
消除重复数据。例如,您是否重复记录客户端的 IP 地址或域名?这些可以在一个会话中报告一次,然后不再重复吗?或者您可以将它们存储在地图文件中,并在需要引用它们时使用紧凑的索引值?ETC
测试在 RAM 中缓冲记录的数据是否有助于提高性能(例如,写入一千个 20 字节的日志记录将意味着 1,000 次函数调用,并可能导致大量磁盘查找和其他写入开销,而一次写入单个 20,000 字节块仅意味着一个函数调用,可以显着提高性能并最大限度地提高您从磁盘中获取的突发速率)。通常写入大小为(4k、16k、32、64k)的数据块效果很好,因为它倾向于适合磁盘和 I/O 架构(但请检查您的特定架构以了解哪些大小可能会提高效率的线索)。RAM 缓冲区的缺点是,如果停电,您将丢失更多数据。因此,您可能必须在性能与稳健性之间取得平衡。
(特别是如果您正在缓冲......)将信息转储到内存数据结构并将其传递给另一个线程以将其流式传输到磁盘。这将有助于阻止您的主线程被日志 I/O 阻塞。但请注意线程 - 例如,您可能必须考虑如何处理创建数据的速度快于短时间突发记录的时间 - 您是否需要实现队列等?
您是否正在记录多个流?可以将这些多路复用到单个日志中以可能减少磁盘查找和打开文件的数量吗?
是否有一种硬件解决方案可以物超所值?例如,您是否使用过 SSD 或 RAID 磁盘?将数据转储到不同的服务器会帮助还是阻碍?如果您可以花费 500 美元来简单地升级磁盘,那么花费 10,000 美元的开发人员时间来改进性能可能并不总是很有意义。
我使用下面的代码来记录。它是一个接受 Logging 并将每条消息放入并发队列的单例。每两秒钟它会将所有进入磁盘的内容写入磁盘。您的应用程序现在只会延迟将每条消息放入列表所需的时间。这是我自己的代码,请随意使用。
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
namespace FastLibrary
{
public enum Severity : byte
{
Info = 0,
Error = 1,
Debug = 2
}
public class Log
{
private struct LogMsg
{
public DateTime ReportedOn;
public string Message;
public Severity Seriousness;
}
// Nice and Threadsafe Singleton Instance
private static Log _instance;
public static Log File
{
get { return _instance; }
}
static Log()
{
_instance = new Log();
_instance.Message("Started");
_instance.Start("");
}
~Log()
{
Exit();
}
public static void Exit()
{
if (_instance != null)
{
_instance.Message("Stopped");
_instance.Stop();
_instance = null;
}
}
private ConcurrentQueue<LogMsg> _queue = new ConcurrentQueue<LogMsg>();
private Thread _thread;
private string _logFileName;
private volatile bool _isRunning;
public void Message(string msg)
{
_queue.Enqueue(new LogMsg { ReportedOn = DateTime.Now, Message = msg, Seriousness = Severity.Info });
}
public void Message(DateTime time, string msg)
{
_queue.Enqueue(new LogMsg { ReportedOn = time, Message = msg, Seriousness = Severity.Info });
}
public void Message(Severity seriousness, string msg)
{
_queue.Enqueue(new LogMsg { ReportedOn = DateTime.Now, Message = msg, Seriousness = seriousness });
}
public void Message(DateTime time, Severity seriousness, string msg)
{
_queue.Enqueue(new LogMsg { ReportedOn = time, Message = msg, Seriousness = seriousness });
}
private void Start(string fileName = "", bool oneLogPerProcess = false)
{
_isRunning = true;
// Unique FileName with date in it. And ProcessId so the same process running twice will log to different files
string lp = oneLogPerProcess ? "_" + System.Diagnostics.Process.GetCurrentProcess().Id : "";
_logFileName = fileName == ""
? DateTime.Now.Year.ToString("0000") + DateTime.Now.Month.ToString("00") +
DateTime.Now.Day.ToString("00") + lp + "_" +
System.IO.Path.GetFileNameWithoutExtension(Application.ExecutablePath) + ".log"
: fileName;
_thread = new Thread(LogProcessor);
_thread.IsBackground = true;
_thread.Start();
}
public void Flush()
{
EmptyQueue();
}
private void EmptyQueue()
{
while (_queue.Any())
{
var strList = new List<string>();
//
try
{
// Block concurrent writing to file due to flush commands from other context
lock (_queue)
{
LogMsg l;
while (_queue.TryDequeue(out l)) strList.Add(l.ReportedOn.ToLongTimeString() + "|" + l.Seriousness + "|" + l.Message);
if (strList.Count > 0)
{
System.IO.File.AppendAllLines(_logFileName, strList);
strList.Clear();
}
}
}
catch
{
//ignore errors on errorlogging ;-)
}
}
}
public void LogProcessor()
{
while (_isRunning)
{
EmptyQueue();
// Sleep while running so we write in efficient blocks
if (_isRunning) Thread.Sleep(2000);
else break;
}
}
private void Stop()
{
// This is never called in the singleton.
// But we made it a background thread so all will be killed anyway
_isRunning = false;
if (_thread != null)
{
_thread.Join(5000);
_thread.Abort();
_thread = null;
}
}
}
}
在调用 logger.debug 之前检查记录器是否已启用调试,这意味着您的代码在关闭调试时不必评估消息字符串。
if (_logger.IsDebugEnabled) _logger.Debug($"slow old string {this.foo} {this.bar}");