Registrazione in modo asincrono: come dovrebbe essere fatto?


11

In molti dei servizi su cui lavoro ci sono molte registrazioni effettuate. I servizi sono servizi WCF (principalmente) che utilizzano la classe .NET EventLogger.

Sono in procinto di migliorare le prestazioni di questi servizi e ho pensato che la registrazione in modo asincrono avrebbe giovato alle prestazioni.

Non sono consapevole di ciò che accade quando più thread chiedono di accedere e se crea un collo di bottiglia, ma anche se non lo è, penso ancora che non dovrebbe interferire con il processo in corso.

Il mio pensiero è che dovrei invocare lo stesso metodo di registro che chiamo ora ma farlo utilizzando un nuovo thread, continuando con il processo effettivo.

Alcune domande al riguardo:

Va bene?

Ci sono degli aspetti negativi?

Dovrebbe essere fatto in modo diverso?

Forse è così veloce che non vale nemmeno la pena?


1
Hai profilato il runtime (s) per sapere che la registrazione ha un effetto misurabile sulle prestazioni? I computer sono troppo complessi per pensare che qualcosa potrebbe essere lento, misurare due volte e tagliare una volta è un buon consiglio in qualsiasi professione =)
Patrick Hughes,

@PatrickHughes - alcune statistiche dai miei test su una richiesta specifica: 61 (!!) messaggi di log, 150 ms prima di fare una sorta di threading semplice, 90 ms dopo. quindi è più veloce del 40%.
Mithir,

Risposte:


14

Il thread separato per l'operazione I \ O sembra ragionevole.

Ad esempio, non sarebbe utile registrare quali pulsanti l'utente ha premuto nello stesso thread dell'interfaccia utente. Tale UI si bloccherà a caso e avrà prestazioni percepite lentamente .

La soluzione è disaccoppiare l'evento dalla sua elaborazione.

Ecco molte informazioni sul problema produttore-consumatore e la coda degli eventi dal mondo dello sviluppo di giochi

Spesso c'è un codice simile

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

Questo approccio porterà alla discussione sul thread. Tutti i thread di elaborazione combatteranno per poter ottenere il blocco e scrivere nello stesso file in una sola volta.

Alcuni potrebbero provare a rimuovere i blocchi.

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

Non è possibile prevedere il risultato se 2 thread accederanno al metodo contemporaneamente.

Quindi alla fine gli sviluppatori disabiliteranno la registrazione a tutti ...

È possibile riparare?

Sì.

Diciamo che abbiamo un'interfaccia:

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

Invece di attendere il blocco e di eseguire l'operazione di blocco dei file ogni volta che ILoggerviene chiamato, aggiungeremo il nuovo LogMessage alla coda dei messaggi Penging e torneremo alle cose più importanti:

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

Abbiamo fatto con questo semplice Logger asincrono .

Il prossimo passo è elaborare i messaggi in arrivo.

Per semplicità, avvia il nuovo thread e attendi per sempre fino alla chiusura dell'applicazione o il Logger asincrono aggiungerà un nuovo messaggio alla coda in sospeso .

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

    }
}

Sto passando una catena di ascoltatori personalizzati. Probabilmente potresti voler semplicemente inviare il framework di registrazione delle chiamate ( log4net, ecc ...)

Ecco il resto del codice:

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

Punto d'entrata:

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

I fattori chiave da considerare sono la necessità di affidabilità nei file di registro e la necessità di prestazioni. Fare riferimento agli aspetti negativi. Penso che questa sia un'ottima strategia per situazioni ad alte prestazioni.

Va bene, sì

Ci sono degli aspetti negativi - sì - a seconda della criticità della registrazione e dell'implementazione potrebbe verificarsi una delle seguenti situazioni: registri scritti fuori sequenza, azioni del thread di registro non completate prima del completamento delle azioni dell'evento. (Immagina uno scenario in cui accedi "cominciando a collegarti al DB" e poi blocchi il server, l'evento di registro potrebbe non essere mai scritto anche se si è verificato l'evento (!))

Dovrebbe essere fatto in un modo diverso - potresti voler guardare il modello Disruptor in quanto è quasi ideale per questo scenario

Forse è così veloce che non vale nemmeno la pena, non sono d'accordo. Se la tua è una logica "applicativa" e l'unica cosa che fai è scrivere i registri dell'attività, allora otterrai ordini di magnitudo con una latenza inferiore scaricando la registrazione. Se tuttavia si fa affidamento su una chiamata SQL 5sec DB per la restituzione prima di registrare le istruzioni 1-2, i vantaggi sono misti.


1

Penso che la registrazione sia generalmente un'operazione sincrona per sua natura. Vuoi registrare le cose se accadono o se non dipendono dalla tua logica, quindi per poter registrare qualcosa, quella cosa deve essere valutata prima.

Detto questo, puoi migliorare le prestazioni della tua applicazione memorizzando nella cache i log e quindi creando un thread e salvandoli in file quando hai un'operazione legata alla CPU.

È necessario identificare i punti di controllo in modo intelligente in modo da non perdere le informazioni importanti di registrazione durante quel periodo di cache.

Se vuoi avere un aumento delle prestazioni nei tuoi thread devi bilanciare le operazioni di I / O e le operazioni della CPU.

Se crei 10 thread che fanno tutti IO, non otterrai un aumento delle prestazioni.


Come consiglieresti i log di cache? ci sono elementi specifici della richiesta nella maggior parte dei messaggi di log per identificarli, nel mio servizio raramente si verificano esattamente le stesse richieste.
Mithir,

0

La registrazione in modo asincrono è l'unica strada da percorrere se è necessaria una bassa latenza nei thread di registrazione. Il modo in cui ciò viene fatto per le massime prestazioni è attraverso il modello di disgregatore per la comunicazione thread senza lock e garbage. Ora, se si desidera consentire a più thread di accedere contemporaneamente allo stesso file, è necessario sincronizzare le chiamate del registro e pagare il prezzo in contesa di blocco OPPURE utilizzare un multiplexor senza blocco. Ad esempio, CoralQueue fornisce una semplice coda multiplexing come descritto di seguito:

inserisci qui la descrizione dell'immagine

Puoi dare un'occhiata a CoralLog che utilizza queste strategie per la registrazione asincrona.

Disclaimer: sono uno degli sviluppatori di CoralQueue e CoralLog.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.