비동기 적으로 로깅-어떻게해야합니까?


11

내가 작업하는 많은 서비스에는 많은 로깅이 수행됩니다. 서비스는 대부분 .NET EventLogger 클래스를 사용하는 WCF 서비스입니다.

이러한 서비스의 성능을 향상시키는 과정에 있으며 비동기 적으로 로깅하면 성능에 도움이된다고 생각해야합니다.

여러 스레드가 로그를 요청할 때 발생하는 병목 현상과 병목 현상이 발생하더라도 실제로 실행중인 프로세스를 방해해서는 안된다고 생각합니다.

내 생각은 지금 호출하는 것과 동일한 로그 메소드를 호출해야하지만 실제 프로세스를 계속하면서 새 스레드를 사용하여 호출해야한다는 것입니다.

그것에 대한 몇 가지 질문 :

괜찮아?

단점이 있습니까?

다른 방법으로해야합니까?

어쩌면 너무 빨리 노력할 가치가 없을까요?


1
로깅이 성능에 상당한 영향을 미친다는 것을 알기 위해 런타임을 프로파일 링했습니까? 컴퓨터는 너무 느리고 무언가가 느려서 두 번 측정하고 한 번 자르는 것이 어떤 직업이든 좋은 조언이라고 생각하기에는 너무 복잡합니다.
Patrick Hughes

@PatrickHughes-하나의 특정 요청에 대한 내 테스트의 통계 : 61 (!!) 로그 메시지, 간단한 스레딩을하기 전에 150ms, 90ms 후에. 40 % 더 빠릅니다.
Mithir

답변:


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호출 될 때마다 잠금 대기 및 파일 차단 작업을 수행하는 대신 Penging Messages Queue에LogMessage 를 추가 하고 더 중요한 항목으로 돌아갑니다.

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));
    }
}

우리는이 간단한 비동기 로거를 사용했습니다 .

다음 단계는 들어오는 메시지를 처리하는 것입니다.

간단하게하기 위해 새 스레드를 시작 하고 응용 프로그램이 종료 될 때까지 기다리거나 Asynchronous Logger보류중인 큐에 새 메시지를 추가 합니다.

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 작업의 균형을 유지해야합니다.

모두 IO를 수행하는 10 개의 스레드를 만들면 성능이 향상되지 않습니다.


캐싱 로그를 어떻게 제안 하시겠습니까? 대부분의 로그 메시지에는 요청을 식별하기 위해 요청 특정 항목이 있지만 내 서비스에서는 거의 동일한 요청이 거의 발생하지 않습니다.
Mithir

0

로깅 스레드에서 지연 시간이 짧은 경우 비동기 적으로 로깅하는 것이 유일한 방법입니다. 이것이 최대 성능을 위해 수행되는 방식은 잠금 및 가비지없는 스레드 통신을 위한 방해 기 패턴 을 통하는 것입니다. 이제 여러 스레드가 동일한 파일에 동시에 로그 할 수있게하려면 로그 호출을 동기화하고 잠금 경합으로 가격을 지불하거나 잠금이없는 멀티플렉서를 사용해야합니다. 예를 들어 CoralQueue 는 아래에 설명 된대로 간단한 멀티플렉싱 큐를 제공합니다.

여기에 이미지 설명을 입력하십시오

비동기 로깅에 이러한 전략을 사용 하는 CoralLog 를 살펴볼 수 있습니다 .

면책 조항 : 저는 CoralQueue 및 CoralLog 개발자 중 하나입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.