Eşzamansız olarak günlüğe kaydetme - nasıl yapılmalı?


11

Üzerinde çalıştığım hizmetlerin çoğunda, çok sayıda günlük kaydı yapılıyor. Hizmetler, .NET EventLogger sınıfını kullanan WCF hizmetleridir (çoğunlukla).

Bu hizmetlerin performansını artırma sürecindeyim ve eşzamansız olarak günlüğe kaydetmenin performansa fayda sağlayacağını düşünmeliyim.

Birden çok iş parçacığının günlüğe kaydetmesini istediğinde ne olduğunun farkında değilim ve bir darboğaz oluşturuyorsa, ancak olmasa bile hala yürütülmekte olan gerçek sürece müdahale etmemesi gerektiğini düşünüyorum.

Düşüncelerim, şimdi çağırdığım aynı günlük yöntemini çağırmalı, ancak gerçek işlemle devam ederken yeni bir iş parçacığı kullanmalıyım.

Bununla ilgili bazı sorular:

Tamam mı?

Herhangi bir dezavantajı var mı?

Farklı bir şekilde mi yapılmalı?

Belki o kadar hızlı ki çabaya değmez bile?


1
Günlüğe kaydetmenin performans üzerinde ölçülebilir bir etkisi olduğunu bilmek için çalışma zamanlarını sürdünüz mü? Bilgisayarlar sadece bir şeyin yavaş olabileceğini düşünmek için çok karmaşıktır, iki kez ölçün ve bir kez kesin herhangi bir meslekte iyi bir tavsiyetir =)
Patrick Hughes 20:12

@PatrickHughes - belirli bir istek üzerine testlerimden bazı istatistikler: 61 (!!) günlük mesajları, bir çeşit basit iplik geçirmeden önce 150ms, 90ms sonra. yani% 40 daha hızlı.
Mithir

Yanıtlar:


14

G / Ç işlemi için ayrı iplik makul geliyor.

Örneğin, kullanıcının aynı UI iş parçacığında hangi düğmelere bastığını günlüğe kaydetmek iyi olmaz. Böyle bir kullanıcı arayüzü rastgele duracak ve yavaş algılanan performansa sahip olacaktır .

Çözüm olayı işlenmesinden ayırmaktır.

İşte oyun geliştirme dünyasından Üretici-Tüketici Sorunu ve Etkinlik Kuyruğu hakkında birçok bilgi

Genellikle bir kod var

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

Bu yaklaşım İş Parçacığı İçeriğine yol açacaktır. Tüm işleme iş parçacıkları aynı anda aynı dosyaya kilit ve yazma elde etmek için mücadele edecek.

Bazıları kilitleri çıkarmaya çalışabilir.

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...")
        }
      }    
}

Aynı anda 2 iş parçacığı yönteme girecekse sonucu tahmin etmek mümkün değildir.

Böylece sonunda geliştiriciler günlük kaydını devre dışı bırakacak ...

Düzeltmek mümkün mü?

Evet.

Diyelim ki arayüzümüz var:

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

Bunun yerine kilit bekliyor ve Dosya işlemi her zaman engelleme gerçekleştirme ILoggeryeni ekle edecektir denir LogMessage için Penging Mesajları Kuyruğu ve daha önemli şeylere dönüş:

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

Bu basit Asenkron Kaydedici ile yaptık .

Sonraki adım, gelen mesajları işlemektir.

Basitlik için, yeni İş Parçacığı başlatalım ve uygulama çıkıncaya veya Asenkron Kaydedici Bekleyen Kuyruğa yeni mesaj ekleyene kadar sonsuza kadar bekleyelim .

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

    }
}

Özel dinleyici zincirini geçiyorum. Muhtemelen sadece arama kaydı çerçevesi ( log4net, vb.) Göndermek isteyebilirsiniz.

İşte kodun geri kalanı:

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

Giriş noktası:

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

Dikkate alınması gereken temel faktörler, günlük dosyalarında güvenilirlik gereksiniminiz ve performans gereksiniminizdir. Aşağı taraflara bakın. Bunun yüksek performanslı durumlar için harika bir strateji olduğunu düşünüyorum.

İyi mi - evet

Günlüğünüzün ve uygulamanızın önem derecesine bağlı olarak herhangi bir olumsuz tarafı var mı? ("DB'ye bağlanmaya başlayarak" oturum açtığınız ve daha sonra sunucuyu çökerttiğiniz bir senaryo düşünün, olay gerçekleşse bile log olayı asla yazılamayabilir (!))

Farklı bir şekilde yapılması gerekirse - bu senaryo için neredeyse ideal olduğu için Disruptor modeline bakmak isteyebilirsiniz

Belki de o kadar hızlı ki çabaya bile değmez - katılmıyorum. Sizinki bir "uygulama" mantığıysa ve yaptığınız tek şey etkinliğin günlüklerini yazmaksa - o zaman günlüğü boşaltarak büyüklük daha düşük gecikme emirleri olacaksınız. Ancak 1-2 ifadeleri günlüğe kaydetmeden önce geri dönmek için 5sec DB SQL çağrısına güveniyorsanız, yararları karışık.


1

Günlüğe kaydetmenin genellikle doğası gereği senkronize bir işlem olduğunu düşünüyorum. Olursa veya mantığınıza bağlı değilse bir şeyleri günlüğe kaydetmek istersiniz, bu nedenle bir şeyi günlüğe kaydetmek için önce o şeyin değerlendirilmesi gerekir.

Bunu söyledikten sonra, günlükleri önbelleğe alıp bir iş parçacığı oluşturarak ve CPU bağlantılı bir işleminiz olduğunda bunları dosyalara kaydederek uygulamanızın performansını artırabilirsiniz.

Önbellek döneminde önemli günlük bilgilerinizi kaybetmemek için kontrol noktalarınızı akıllıca tanımlamanız gerekir.

İş parçacıklarınızda performans artışı istiyorsanız, IO işlemlerini ve CPU işlemlerini dengelemeniz gerekir.

Hepsi IO yapan 10 iş parçacığı oluşturursanız, performans artışı elde edemezsiniz.


Günlükleri önbelleğe almayı nasıl önerirsiniz? çoğu günlük iletisinde bunları tanımlamak için isteğe özel öğeler vardır, hizmetimde tam olarak aynı istekler nadiren gerçekleşir.
Mithir

0

Günlüğe kaydetme iş parçacıklarında düşük gecikme süresi gerekiyorsa, eşzamansız olarak günlüğe kaydetmenin tek yolu budur. Bunun maksimum performans için yapılma şekli , kilitsiz ve çöpsüz iplik iletişimi için ayırıcı modelidir . Şimdi aynı dosyaya aynı anda birden çok iş parçacığının oturum açmasına izin vermek istiyorsanız ya günlük aramalarını senkronize etmeli ve fiyatı kilit çekişmeli olarak ödemelisiniz VEYA kilitsiz bir çoklayıcı kullanmalısınız. Örneğin, CoralQueue aşağıda açıklandığı gibi basit bir çoğullama kuyruğu sağlar:

resim açıklamasını buraya girin

Bu stratejileri eşzamansız günlüğe kaydetme için kullanan CoralLog'a göz atabilirsiniz .

Feragatname: CoralQueue ve CoralLog'un geliştiricilerinden biriyim.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.