异步记录-应该怎么做?


11

在我从事的许多服务中,已经完成了许多日志记录。这些服务是(大多数)使用.NET EventLogger类的WCF服务。

我正在改善这些服务的性能,并且我认为异步记录将有益于性能。

我不知道当多个线程要求登录时会发生什么,是否会造成瓶颈,但是即使不是这样,我仍然认为它不应该干扰正在执行的实际进程。

我的想法是,我应该调用现在调用的同一日志方法,但是在继续实际过程的同时,使用新线程来执行此操作。

关于此的一些问题:

可以吗

有没有缺点?

是否应该以其他方式完成?

也许它是如此之快,甚至不值得付出努力?


1
您是否对运行时进行了分析,以了解日志记录对性能有可测量的影响?计算机太复杂了,以至于不能认为某些事情可能很慢,两次测量并且一次裁掉是一个很好的建议=)
Patrick Hughes 2012年

@PatrickHughes-我对一个特定请求的测试得出的一些统计信息:61(!!)日志消息,执行某种简单线程之前150ms,之后90ms。所以快40%
秘瑟2012年

Answers:


14

用于I \ O操作的单独线程听起来很合理。

例如,记录用户在同一UI线程中按下了哪些按钮不是很好。这样的UI会随机挂起,并且性能会变慢。

解决方案是将事件与其处理分离。

这是来自游戏开发世界的有关生产者-消费者问题和事件队列的大量信息。

通常会有类似的代码

///Never do this!!!
public void WriteLog_Like_Bastard(string msg)
{
    lock (_lockBecauseILoveThreadContention)
    {
        File.WriteAllText("c:\\superApp.log", msg);
    }
}

这种方法将导致线程争用。所有处理线程都将努力争取能够立即获得锁定并写入同一文件。

有些人可能会尝试解除锁定。

public void Log_Like_Dumbass(string msg)
{
      try 
      {  File.Append("c:\\superApp.log", msg); }
        catch (Exception ex) 
        {
            MessageBox.Show("Log file may be locked by other process...")
        }
      }    
}

如果2个线程同时进入方法,则无法预测结果。

因此最终开发人员将完全禁用日志记录...

有可能解决吗?

是。

可以说我们有接口:

 public interface ILogger
 {
    void Debug(string message);
    // ... etc
    void Fatal(string message);
 }

而不是等待锁和执行每当时间阻止文件操作ILogger被称为我们将新添加的LogMessagePenging消息队列和返回到更重要的事情:

public class AsyncLogger : ILogger
{
    private readonly BlockingCollection<LogMessage> _pendingMessages;
    private readonly Type _loggerFor;
    private readonly IThreadAdapter _threadAdapter;

    public AsyncLogger(BlockingCollection<LogMessage> pendingMessages, Type loggerFor, IThreadAdapter threadAdapter)
    {
        _pendingMessages = pendingMessages;
        _loggerFor = loggerFor;
        _threadAdapter = threadAdapter;
    }

    public void Debug(string message)
    {
        Push(LoggingLevel.Debug, message);
    }

    public void Fatal(string message)
    {
        Push(LoggingLevel.Fatal, message);
    }

    private void Push(LoggingLevel importance, string message)
    {
        // since we do not know when our log entry will be written to disk, remember current time
        var timestamp = DateTime.Now;
        var threadId = _threadAdapter.GetCurrentThreadId();

        // adds message to the queue in lock-free manner and immediately returns control to caller
        _pendingMessages.Add(LogMessage.Create(timestamp, importance, message, _loggerFor, threadId));
    }
}

我们已经完成了这个简单的 异步记录器

下一步是处理传入消息。

为简单起见,让我们启动新线程并永远等待,直到应用程序退出,或者异步记录器将新消息添加到待处理队列中

public class LoggingQueueDispatcher : IQueueDispatcher
{
    private readonly BlockingCollection<LogMessage> _pendingMessages;
    private readonly IEnumerable<ILogListener> _listeners;
    private readonly IThreadAdapter _threadAdapter;
    private readonly ILogger _logger;
    private Thread _dispatcherThread;

    public LoggingQueueDispatcher(BlockingCollection<LogMessage> pendingMessages, IEnumerable<ILogListener> listeners, IThreadAdapter threadAdapter, ILogger logger)
    {
        _pendingMessages = pendingMessages;
        _listeners = listeners;
        _threadAdapter = threadAdapter;
        _logger = logger;
    }

    public void Start()
    {
        //  Here I use 'new' operator, only to simplify example. Should be using interface  '_threadAdapter.CreateBackgroundThread' to allow unit testing
        Thread thread = new Thread(MessageLoop);
        thread.Name = "LoggingQueueDispatcher Thread";
        thread.IsBackground = true;

        thread.Start();
        _logger.Debug("Asked to start log message Dispatcher ");

        _dispatcherThread = thread;
    }

    public bool WaitForCompletion(TimeSpan timeout)
    {
        return _dispatcherThread.Join(timeout);
    }

    private void MessageLoop()
    {
        _logger.Debug("Entering dispatcher message loop...");
        var cancellationToken = new CancellationTokenSource();
        LogMessage message;

        while (_pendingMessages.TryTake(out message, Timeout.Infinite, cancellationToken.Token))
        {
            // !!!!! Now it is safe to use File.AppendAllText("c:\\my.log") without ever using lock or forcing important threads to wait.
            // this is example, do not use in production
            foreach (var listener in _listeners)
            {
                listener.Log(message);
            }
        }

    }
}

我正在传递自定义侦听器链。您可能只想发送呼叫记录框架log4net等)。

这是其余的代码:

public enum LoggingLevel
{
    Debug,
    // ... etc
    Fatal,
}


public class LogMessage
{
    public DateTime Timestamp { get; private set; }
    public LoggingLevel Importance { get; private set; }
    public string Message { get; private set; }
    public Type Source { get; private set; }
    public int ThreadId { get; private set; }

    private LogMessage(DateTime timestamp, LoggingLevel importance, string message, Type source, int threadId)
    {
        Timestamp = timestamp;
        Message = message;
        Source = source;
        ThreadId = threadId;
        Importance = importance;
    }

    public static LogMessage Create(DateTime timestamp, LoggingLevel importance, string message, Type source, int threadId)
    {
        return  new LogMessage(timestamp, importance, message, source, threadId);
    }

    public override string ToString()
    {
        return string.Format("{0}  [TID:{4}] {1:h:mm:ss} ({2})\t{3}", Importance, Timestamp, Source, Message, ThreadId);
    }
}

public class LoggerFactory : ILoggerFactory
{
    private readonly BlockingCollection<LogMessage> _pendingMessages;
    private readonly IThreadAdapter _threadAdapter;

    private readonly ConcurrentDictionary<Type, ILogger> _loggersCache = new ConcurrentDictionary<Type, ILogger>();


    public LoggerFactory(BlockingCollection<LogMessage> pendingMessages, IThreadAdapter threadAdapter)
    {
        _pendingMessages = pendingMessages;
        _threadAdapter = threadAdapter;
    }

    public ILogger For(Type loggerFor)
    {
        return _loggersCache.GetOrAdd(loggerFor, new AsyncLogger(_pendingMessages, loggerFor, _threadAdapter));
    }
}

public class ThreadAdapter : IThreadAdapter
{
    public int GetCurrentThreadId()
    {
        return Thread.CurrentThread.ManagedThreadId;
    }
}

public class ConsoleLogListener : ILogListener
{
    public void Log(LogMessage message)
    {
        Console.WriteLine(message.ToString());
        Debug.WriteLine(message.ToString());
    }
}

public class SimpleTextFileLogger : ILogListener
{
    private readonly IFileSystem _fileSystem;
    private readonly string _userRoamingPath;
    private readonly string _logFileName;
    private FileStream _fileStream;

    public SimpleTextFileLogger(IFileSystem fileSystem, string userRoamingPath, string logFileName)
    {
        _fileSystem = fileSystem;
        _userRoamingPath = userRoamingPath;
        _logFileName = logFileName;
    }

    public void Start()
    {
        _fileStream = new FileStream(_fileSystem.Path.Combine(_userRoamingPath, _logFileName), FileMode.Append);
    }

    public void Stop()
    {
        if (_fileStream != null)
        {
            _fileStream.Dispose();
        }
    }

    public void Log(LogMessage message)
    {
        var bytes = Encoding.UTF8.GetBytes(message.ToString() + Environment.NewLine);
        _fileStream.Write(bytes, 0, bytes.Length);
    }
}

public interface ILoggerFactory
{
    ILogger For(Type loggerFor);
}

public interface ILogListener
{
    void Log(LogMessage message);
}

public interface IThreadAdapter
{
    int GetCurrentThreadId();
}

public interface IQueueDispatcher
{
    void Start();
}

入口点:

public static class Program
{
    public static void Main()
    {
        Debug.WriteLine("[Program] Entering Main ...");

        var pendingLogQueue = new BlockingCollection<LogMessage>();


        var threadAdapter = new ThreadAdapter();
        var loggerFactory = new LoggerFactory(pendingLogQueue, threadAdapter);


        var fileSystem = new FileSystem();
        var userRoamingPath = GetUserDataDirectory(fileSystem);

        var simpleTextFileLogger = new SimpleTextFileLogger(fileSystem, userRoamingPath, "log.txt");
        simpleTextFileLogger.Start();
        ILogListener consoleListener = new ConsoleLogListener();
        ILogListener[] listeners = new [] { simpleTextFileLogger , consoleListener};

        var loggingQueueDispatcher = new LoggingQueueDispatcher(pendingLogQueue, listeners, threadAdapter, loggerFactory.For(typeof(LoggingQueueDispatcher)));
        loggingQueueDispatcher.Start();

        var logger = loggerFactory.For(typeof(Console));

        string line;
        while ((line = Console.ReadLine()) != "exit")
        {
            logger.Debug("you have entered: " + line);
        }

        logger.Fatal("Exiting...");

        Debug.WriteLine("[Program] pending LogQueue will be stopped now...");
        pendingLogQueue.CompleteAdding();
        var logQueueCompleted = loggingQueueDispatcher.WaitForCompletion(TimeSpan.FromSeconds(5));

        simpleTextFileLogger.Stop();
        Debug.WriteLine("[Program] Exiting... logQueueCompleted: " + logQueueCompleted);

    }



    private static string GetUserDataDirectory(FileSystem fileSystem)
    {
        var roamingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        var userDataDirectory = fileSystem.Path.Combine(roamingDirectory, "Async Logging Sample");
        if (!fileSystem.Directory.Exists(userDataDirectory))
            fileSystem.Directory.CreateDirectory(userDataDirectory);
        return userDataDirectory;
    }
}

1

要考虑的关键因素是您对日志文件的可靠性和性能的需求。参考缺点。我认为这是应对高性能情况的绝佳策略。

可以吗-是的

是否存在任何缺点-是-根据日志记录的重要性和实施的情况,可能会发生以下任何情况-日志顺序写错,日志线程操作在事件操作完成之前未完成。(想象一下这样一种情况:登录“开始连接到DB”,然后使服务器崩溃,即使事件已经发生,日志事件也可能永远不会被写入(!)。

是否应该以其他方式完成-您可能需要查看Disruptor模型,因为它在这种情况下几乎是理想的

也许它是如此之快以至于它甚至都不值得付出努力-不同意。如果您的逻辑是“应用程序”逻辑,而您唯一要做的就是写活动日志-那么,通过卸载日志记录,您的延迟将减少几个数量级。但是,如果您在记录1-2条语句之前依靠5秒的DB SQL调用返回,则好处是混合的。


1

我认为日志记录本质上通常是同步操作。您希望记录事物发生的时间或是否不依赖于您的逻辑,因此要记录事物,首先需要评估该事物。

话虽如此,您可以通过缓存日志,然后创建线程并将其保存到文件中进行CPU绑定操作,从而提高应用程序的性能。

您需要巧妙地确定检查点,以免在该缓存期间丢失重要的日志记录信息。

如果要提高线程的性能,则需要平衡IO操作和CPU操作。

如果创建10个都执行IO的线程,则不会提高性能。


您如何建议缓存日志?大多数日志消息中都有特定于请求的项以便识别它们,在我的服务中,很少会出现完全相同的请求。
秘瑟2012年

0

如果您需要日志线程中的低延迟,则异步日志记录是唯一的方法。达到最高性能的方法是通过破坏程序模式进行无锁和无垃圾线程通信。现在,如果要允许多个线程同时登录到同一文件,则必须同步日志调用并在锁争用中付出代价,或者使用无锁多路复用器。例如,CoralQueue提供了一个简单的多路复用队列,如下所述:

在此处输入图片说明

您可以看一下使用这些策略进行异步日志记录的CoralLog

免责声明:我是CoralQueue和CoralLog的开发人员之一。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.