Ghi nhật ký không đồng bộ - nên thực hiện như thế nào?


11

Trong nhiều dịch vụ tôi làm việc có rất nhiều đăng nhập đang được thực hiện. Các dịch vụ là các dịch vụ WCF (hầu hết) sử dụng lớp .NET EventLogger.

Tôi đang trong quá trình cải thiện hiệu suất của các dịch vụ này và tôi đã nghĩ rằng việc đăng nhập không đồng bộ sẽ có lợi cho hiệu suất.

Tôi không biết điều gì xảy ra khi nhiều luồng yêu cầu đăng nhập và nếu nó tạo ra một nút cổ chai, nhưng ngay cả khi nó không tôi vẫn nghĩ rằng nó không nên can thiệp vào quá trình thực tế đang được thực thi.

Suy nghĩ của tôi là tôi nên gọi cùng một phương thức nhật ký mà tôi gọi bây giờ nhưng thực hiện bằng cách sử dụng một luồng mới, trong khi tiếp tục với quy trình thực tế.

Một số câu hỏi về điều đó:

Là nó ổn?

Có bất kỳ nhược điểm?

Nó có nên được thực hiện theo một cách khác?

Có lẽ nó nhanh đến mức nó thậm chí không đáng nỗ lực?


1
Bạn đã định hình (các) thời gian chạy để biết rằng việc ghi nhật ký có ảnh hưởng đến hiệu suất? Máy tính quá phức tạp để chỉ nghĩ rằng một cái gì đó có thể chậm, đo hai lần và cắt một lần là lời khuyên tốt trong bất kỳ ngành nghề nào =)
Patrick Hughes

@PatrickHughes - một số số liệu thống kê từ các thử nghiệm của tôi theo một yêu cầu cụ thể: 61 (!!) thông điệp nhật ký, 150ms trước khi thực hiện một số luồng đơn giản, 90ms sau. vì vậy nó nhanh hơn 40%.
Mithir

Câu trả lời:


14

Chủ đề riêng cho hoạt động I \ O nghe có vẻ hợp lý.

Ví dụ, sẽ không tốt khi đăng nhập những nút mà người dùng đã nhấn trong cùng một luồng UI. Giao diện người dùng như vậy sẽ treo ngẫu nhiên và có hiệu suất nhận thức chậm .

Giải pháp là tách rời sự kiện từ quá trình xử lý của nó.

Dưới đây là rất nhiều thông tin về Vấn đề Nhà sản xuất-Người tiêu dùng và Hàng đợi Sự kiện từ thế giới phát triển trò chơi

Thường có một mã như

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

Cách tiếp cận này sẽ dẫn đến sự tham gia của chủ đề. Tất cả các luồng xử lý sẽ chiến đấu để có thể có được khóa và ghi vào cùng một tệp cùng một lúc.

Một số có thể cố gắng để loại bỏ ổ khóa.

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

Không thể dự đoán kết quả nếu 2 luồng sẽ nhập phương thức cùng một lúc.

Vì vậy, cuối cùng các nhà phát triển sẽ vô hiệu hóa đăng nhập ...

Có thể sửa chữa?

Đúng.

Hãy nói rằng chúng tôi có giao diện:

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

Thay vì chờ khóa và thực hiện thao tác chặn tệp mỗi khi ILoggerđược gọi, chúng tôi sẽ Thêm LogMessage mới vào Hàng đợi Tin nhắn Bành và quay lại những điều quan trọng hơ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));
    }
}

Chúng tôi đã thực hiện với Trình ghi nhật ký không đồng bộ đơn giản này .

Bước tiếp theo là xử lý tin nhắn đến.

Để đơn giản, hãy bắt đầu Chủ đề mới và đợi mãi cho đến khi ứng dụng thoát hoặc Trình ghi nhật ký không đồng bộ sẽ thêm thông báo mới vào Hàng đợi đang chờ xử lý .

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

    }
}

Tôi đang vượt qua chuỗi người nghe tùy chỉnh. Bạn có thể muốn gửi khung ghi nhật ký cuộc gọi ( log4net, v.v ...)

Đây là phần còn lại của mã:

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

Điểm vào:

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

Các yếu tố chính cần xem xét là nhu cầu về độ tin cậy của bạn trong logfiles và nhu cầu về hiệu suất. Tham khảo nhược điểm. Tôi nghĩ rằng đây là một chiến lược tuyệt vời cho các tình huống hiệu suất cao.

Có ổn không

Có bất kỳ nhược điểm nào không - có - tùy thuộc vào mức độ quan trọng của việc ghi nhật ký của bạn và việc thực hiện của bạn bất kỳ điều nào sau đây có thể xảy ra - nhật ký được viết ra khỏi chuỗi, hành động luồng nhật ký không hoàn thành trước khi hành động sự kiện hoàn tất. (Hãy tưởng tượng một kịch bản nơi bạn đăng nhập "bắt đầu kết nối với DB" và sau đó bạn gặp sự cố máy chủ, sự kiện nhật ký có thể không bao giờ được ghi lại mặc dù sự kiện đã xảy ra (!))

Nếu nó được thực hiện theo một cách khác - bạn có thể muốn xem mô hình Disruptor vì nó gần như lý tưởng cho kịch bản này

Có lẽ nó nhanh đến mức nó thậm chí không đáng để nỗ lực - không đồng ý. Nếu logic của bạn là logic "ứng dụng" và điều duy nhất bạn làm là viết nhật ký của hoạt động - thì bạn sẽ nhận được các đơn đặt hàng có độ trễ thấp hơn bằng cách giảm tải nhật ký. Tuy nhiên, nếu bạn dựa vào lệnh gọi DB SQL 5 giây để trả về trước khi ghi 1-2 câu lệnh, thì lợi ích sẽ bị xáo trộn.


1

Tôi nghĩ rằng đăng nhập nói chung là một hoạt động đồng bộ bởi bản chất của nó. Bạn muốn ghi nhật ký mọi thứ nếu chúng xảy ra hoặc nếu chúng không phụ thuộc vào logic của bạn, vì vậy để ghi nhật ký, điều đó cần được đánh giá trước.

Phải nói rằng, bạn có thể cải thiện hiệu suất của ứng dụng bằng cách lưu trữ nhật ký và sau đó tạo một luồng và lưu chúng vào các tệp khi bạn có hoạt động ràng buộc CPU.

Bạn cần xác định điểm kiểm tra của mình một cách khéo léo để không bị mất thông tin đăng nhập quan trọng trong khoảng thời gian lưu trữ.

Nếu bạn muốn tăng hiệu năng trong các luồng của mình, bạn cần cân bằng các hoạt động IO và hoạt động của CPU.

Nếu bạn tạo 10 luồng mà tất cả đều thực hiện IO, thì bạn sẽ không được tăng hiệu suất.


Làm thế nào bạn sẽ đề nghị nhật ký lưu trữ? có các mục dành riêng cho yêu cầu trong hầu hết các thông điệp tường trình để xác định chúng, trong dịch vụ của tôi chính xác các yêu cầu tương tự hiếm khi xảy ra.
Mithir

0

Ghi nhật ký không đồng bộ là cách duy nhất để thực hiện nếu bạn cần độ trễ thấp trong các luồng ghi nhật ký. Cách thức này được thực hiện để có hiệu suất tối đa là thông qua mẫu ngắt để truyền thông luồng không khóa và không có rác. Bây giờ nếu bạn muốn cho phép nhiều luồng đăng nhập đồng thời vào cùng một tệp, bạn phải đồng bộ hóa các cuộc gọi nhật ký và trả giá khi tranh chấp khóa HOẶC sử dụng bộ ghép kênh không khóa. Ví dụ, CoralQueue cung cấp một hàng đợi ghép kênh đơn giản như được mô tả dưới đây:

nhập mô tả hình ảnh ở đây

Bạn có thể xem CoralLog sử dụng các chiến lược này để ghi nhật ký không đồng bộ.

Tuyên bố miễn trừ trách nhiệm: Tôi là một trong những nhà phát triển của CoralQueue và CoralLog.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.