Log secara asinkron - bagaimana seharusnya dilakukan?


11

Di banyak layanan yang saya kerjakan ada banyak penebangan yang dilakukan. Layanan ini adalah layanan WCF (kebanyakan) yang menggunakan kelas .NET EventLogger.

Saya sedang dalam proses meningkatkan kinerja layanan-layanan ini, dan saya berpikir bahwa pencatatan secara tidak sinkron akan menguntungkan kinerja.

Saya tidak mengetahui apa yang terjadi ketika beberapa utas meminta untuk login, dan jika itu menciptakan kemacetan, tetapi bahkan jika tidak, saya masih berpikir bahwa itu tidak boleh mengganggu proses aktual yang sedang dieksekusi.

Pikiran saya adalah bahwa saya harus menggunakan metode log yang sama yang saya panggil sekarang tetapi melakukannya dengan menggunakan utas baru, sambil melanjutkan proses yang sebenarnya.

Beberapa pertanyaan tentang itu:

Apakah itu oke?

Apakah ada kerugian?

Haruskah itu dilakukan dengan cara yang berbeda?

Mungkin ini sangat cepat sehingga tidak sepadan dengan usaha?


1
Sudahkah Anda membuat profil runtime untuk mengetahui bahwa pencatatan memiliki efek yang terukur pada kinerja? Komputer terlalu rumit untuk hanya berpikir bahwa sesuatu mungkin lambat, mengukur dua kali dan memotong sekali adalah nasihat yang baik dalam profesi apa pun =)
Patrick Hughes

@ PatrickrickHughes - beberapa statistik dari pengujian saya pada satu permintaan spesifik: 61 (!!) pesan log, 150 ms sebelum melakukan semacam threading sederhana, 90 ms setelah. jadi 40% lebih cepat.
Mithir

Jawaban:


14

Utas terpisah untuk operasi I \ O terdengar masuk akal.

Misalnya, tidak baik untuk mencatat tombol mana yang ditekan pengguna pada utas UI yang sama. UI seperti itu akan menggantung secara acak dan memiliki kinerja yang lambat .

Solusinya adalah dengan memisahkan acara dari prosesnya.

Berikut ini banyak informasi tentang Produser-Consumer Problem dan Event Queue dari dunia pengembangan game

Seringkali ada kode seperti

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

Pendekatan ini akan mengarah ke Thread Contention. Semua utas pemrosesan akan berjuang untuk bisa mendapatkan kunci dan menulis ke file yang sama sekaligus.

Beberapa mungkin mencoba untuk menghapus kunci.

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

Tidak mungkin untuk memprediksi hasil jika 2 utas akan memasuki metode pada saat yang sama.

Jadi pada akhirnya pengembang akan menonaktifkan logging sama sekali ...

Apakah mungkin diperbaiki?

Iya.

Katakanlah kita memiliki antarmuka:

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

Alih-alih menunggu kunci dan melakukan operasi pemblokiran file setiap kali ILoggerdipanggil, kami akan Menambahkan LogMessage baru ke Antrian Pesan Penging dan kembali ke hal-hal yang lebih penting:

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

Kami telah selesai dengan Asynchronous Logger sederhana ini .

Langkah selanjutnya adalah memproses pesan yang masuk.

Untuk kesederhanaan, mari mulai Thread baru dan tunggu selamanya sampai aplikasi keluar atau Asynchronous Logger akan menambahkan pesan baru ke Antrian Tertunda .

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

    }
}

Saya melewati rantai pendengar khusus. Anda mungkin ingin mengirim kerangka pencatatan panggilan ( log4net, dll ...)

Ini adalah sisa kode:

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

Titik masuk:

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

Faktor utama yang perlu dipertimbangkan adalah kebutuhan Anda akan keandalan dalam file log dan kebutuhan untuk kinerja. Rujuk sisi negatifnya. Saya pikir ini adalah strategi hebat untuk situasi kinerja tinggi.

Apakah boleh - ya

Apakah ada kerugian - ya - tergantung pada kekritisan pencatatan Anda dan implementasi Anda, salah satu dari yang berikut ini dapat terjadi - log dituliskan secara berurutan, tindakan thread log tidak lengkap sebelum tindakan peristiwa selesai. (Bayangkan sebuah skenario di mana Anda log "mulai terhubung ke DB" dan Anda kemudian crash server, log event mungkin tidak pernah ditulis walaupun acara telah terjadi (!))

Jika itu dilakukan dengan cara yang berbeda - Anda mungkin ingin melihat model Disruptor karena hampir ideal untuk skenario ini

Mungkin itu sangat cepat sehingga bahkan tidak sepadan dengan usaha - tidak setuju. Jika Anda adalah logika "aplikasi", dan satu-satunya hal yang Anda lakukan adalah menulis log aktivitas - maka Anda akan mendapatkan urutan latensi yang lebih rendah dengan melepas log. Namun, jika Anda mengandalkan panggilan SQL 5sec DB untuk kembali sebelum mencatat 1-2 pernyataan, manfaatnya beragam.


1

Saya pikir logging pada umumnya adalah operasi yang sinkron. Anda ingin mencatat sesuatu jika terjadi atau tidak bergantung pada logika Anda, jadi untuk mencatat sesuatu, hal itu perlu dievaluasi terlebih dahulu.

Karena itu, Anda dapat meningkatkan kinerja aplikasi Anda dengan caching log dan kemudian membuat utas dan menyimpannya ke file ketika Anda memiliki operasi terikat CPU.

Anda perlu mengidentifikasi pos-pos pemeriksaan Anda secara cerdik sehingga Anda tidak kehilangan informasi logging penting Anda selama periode cache itu.

Jika Anda ingin meningkatkan kinerja di utas Anda, Anda harus menyeimbangkan operasi IO dan operasi CPU.

Jika Anda membuat 10 utas yang semuanya menggunakan IO, maka Anda tidak akan mendapatkan peningkatan kinerja.


Bagaimana Anda menyarankan caching log? ada item khusus permintaan di sebagian besar pesan log untuk mengidentifikasi mereka, di layanan saya permintaan yang sama jarang terjadi.
Mithir

0

Logging secara asinkron adalah satu-satunya cara untuk pergi jika Anda membutuhkan latensi rendah di utas log. Cara ini dilakukan untuk kinerja maksimum adalah melalui pola pengganggu untuk komunikasi utas bebas kunci dan bebas sampah. Sekarang jika Anda ingin mengizinkan beberapa utas untuk masuk secara bersamaan ke file yang sama, Anda harus menyinkronkan panggilan log dan membayar harganya dalam pertikaian kunci ATAU menggunakan multipleksor bebas kunci. Misalnya, CoralQueue menyediakan antrian multipleks sederhana seperti yang dijelaskan di bawah ini:

masukkan deskripsi gambar di sini

Anda bisa melihat pada CoralLog yang menggunakan strategi ini untuk logging asynchronous.

Penafian: Saya adalah salah satu pengembang CoralQueue dan CoralLog.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.