Ведення асинхронного журналу - як це робити?


11

У багатьох службах, над якими я працюю, робиться багато журналів. Сервіси - це сервіси WCF (в основному), які використовують клас .NET EventLogger.

Я зараз вдосконалюю продуктивність цих служб, і мені довелося думати, що асинхронний журнал буде корисним для продуктивності.

Мені невідомо про те, що відбувається, коли декілька потоків запитують увійти, і якщо це створює вузьке місце, але навіть якщо це не так, я все ще думаю, що він не повинен заважати виконувати фактичний процес.

Думаю, що я повинен використовувати той самий метод журналу, який я зараз закликаю, але робити це за допомогою нової нитки, продовжуючи власне процес.

Деякі запитання щодо цього:

Це нормально?

Чи є якісь недоліки?

Чи слід це робити по-іншому?

Може, це так швидко, що навіть не варто докладати зусиль?


1
Ви профілювали час виконання, щоб знати, що ведення журналу має вимірний вплив на продуктивність? Комп'ютери просто занадто складні, щоб просто подумати, що щось може бути повільним, поміряти двічі і скоротити один раз - хороша порада в будь-якій професії =)
Патрік Х'юз

@PatrickHughes - деякі статистичні дані з моїх тестів на одному конкретному запиті: 61 (!!) лог-повідомлень, 150 мс перед тим, як робити якусь просту нитку, через 90 мс після. тому швидше на 40%.
Мітір

Відповіді:


14

Окремий потік для роботи вводу-виводу звучить розумно.

Наприклад, було б непогано реєструвати, які кнопки користувач натискав в одній нитці інтерфейсу користувача. Такий інтерфейс буде висіти навмання та матиме повільну сприйняття продуктивності .

Рішення - від'єднати подію від її обробки.

Тут ви знайдете багато інформації про проблеми виробника-споживача та черги подій зі світу розвитку ігор

Часто є такий код, як

///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буде викликано, ми додамо новий LogMessage до черги повідомлень Penging і повернемося до більш важливих речей:

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

Ключові фактори, які слід враховувати - це ваша потреба в надійності журналів і потреба в продуктивності. Зверніться до недоліків. Я думаю, що це відмінна стратегія для ситуацій з високою ефективністю.

Це нормально - так

Чи є якісь недоліки - так - залежно від критичності вашої реєстрації та вашої реалізації може статися будь-яке з наступного - журнали виписані з послідовності, дії потоку журналу не завершені до завершення дій події. (Уявіть собі сценарій, коли ви входите в систему "починаючи підключатися до БД", а потім виходите з ладу сервера, подія журналу ніколи не може бути записана, хоча подія сталася (!))

Якщо це буде зроблено по-іншому - ви можете подивитися на модель Disruptor, оскільки це майже ідеально підходить для цього сценарію

Можливо, це так швидко, що навіть не варто витрачати зусиль - не погоджуйтесь. Якщо ваша - логіка «застосунку», і єдине, що ви робите, - це писати журнали діяльності - тоді ви будете на порядок меншими затримкою, завантажуючи журнал. Якщо ви покладаєтесь на 5сек-дзвінок DB SQL для повернення перед входом у систему 1-2 висловлювань, переваги неоднозначні.


1

Я думаю, що вхід із журналу - це, як правило, синхронна операція. Ви хочете записати речі, якщо вони трапляються або якщо вони не залежать від вашої логіки, тому для того, щоб щось записати, цю штуку потрібно спочатку оцінити.

Сказавши це, ви можете покращити продуктивність програми, кешуючи журнали, а потім створивши потік і зберігаючи їх у файлах, коли у вас є операція, пов'язана з процесором.

Потрібно розумно визначити контрольно-пропускні пункти, щоб у цей період кешу не втратити важливу інформацію для реєстрації.

Якщо ви хочете мати підвищення продуктивності в потоках, вам потрібно збалансувати операції вводу-виводу та операції з процесором.

Якщо ви створите 10 ниток, які всі роблять IO, ви не отримаєте підвищення продуктивності.


Як би ви запропонували кешування журналів? в більшості повідомлень журналу є специфічні для запиту елементи, щоб їх ідентифікувати, в моїй службі рідко трапляються точно такі самі запити.
Мітір

0

Асинхронний журнал - це єдиний спосіб, якщо вам потрібні низькі затримки в потоках журналу. Це робиться для досягнення максимальної продуктивності через схему руйнування для безперервного зв'язку та без сміття. Тепер, якщо ви хочете дозволити декілька потоків одночасно входити в один і той же файл, ви повинні або синхронізувати дзвінки в журналі та сплатити ціну в конфлікті блокування АБО використовувати мультиплексор без блокування. Наприклад, CoralQueue забезпечує просту чергу мультиплексування, як описано нижче:

введіть тут опис зображення

Ви можете поглянути на CoralLog який використовує ці стратегії для асинхронного ведення журналу.

Відмова: Я один із розробників CoralQueue та CoralLog.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.