การล็อกอินแบบอะซิงโครนัส - ควรทำอย่างไร?


11

ในบริการหลายอย่างที่ฉันทำงานมีการบันทึกจำนวนมาก บริการเป็นบริการ WCF (ส่วนใหญ่) ซึ่งใช้คลาส. NET EventLogger

ฉันอยู่ระหว่างการปรับปรุงประสิทธิภาพของบริการเหล่านี้และฉันต้องคิดว่าการบันทึกแบบอะซิงโครนัสจะเป็นประโยชน์ต่อประสิทธิภาพการทำงาน

ฉันไม่ทราบว่าจะเกิดอะไรขึ้นเมื่อหลายเธรดขอให้บันทึกและถ้ามันสร้างคอขวด แต่ถึงแม้ว่ามันจะไม่ฉันก็ยังคิดว่ามันจะไม่รบกวนกระบวนการที่เกิดขึ้นจริง

ความคิดของฉันคือฉันควรจะเรียกใช้วิธีการบันทึกเดียวกันกับที่ฉันเรียกตอนนี้ แต่ใช้เธรดใหม่ในขณะที่ดำเนินการตามกระบวนการจริงต่อไป

บางคำถามเกี่ยวกับที่:

มันโอเคไหม

มีข้อเสียอะไรบ้าง?

ควรทำในวิธีที่ต่างออกไปหรือไม่?

อาจจะเร็วจนไม่คุ้มค่ากับความพยายามหรือ


1
คุณได้ทำโปรไฟล์ runtime ให้ทราบหรือไม่ว่าการบันทึกมีผลกระทบต่อประสิทธิภาพที่วัดได้หรือไม่? คอมพิวเตอร์เป็นเพียงซับซ้อนเกินไปเพียงแค่คิดว่าบางสิ่งบางอย่างที่อาจจะช้าวัดสองครั้งและตัดครั้งหนึ่งเคยเป็นคำแนะนำที่ดีในอาชีพใด =)
แพทริคฮิวจ์

@PatrickHughes - สถิติบางอย่างจากการทดสอบของฉันสำหรับคำขอเฉพาะหนึ่งรายการ: ข้อความบันทึก 61 (!!) 150 มิลลิวินาทีก่อนที่จะทำการทำเกลียวง่าย ๆ 90 วินาทีหลังจากนั้น ดังนั้นมันจึงเร็วขึ้น 40%
Mithir

คำตอบ:


14

การแยกเธรดสำหรับการทำงานของ I \ O นั้นสมเหตุสมผล

ตัวอย่างเช่นจะเป็นการดีถ้าจะบันทึกว่าผู้ใช้กดปุ่มใดในเธรด UI เดียวกัน UI ดังกล่าวจะแขวนที่สุ่มและได้ช้ารับรู้ผลการดำเนินงาน

การแก้ปัญหาคือการแยกเหตุการณ์จากการประมวลผล

นี่คือข้อมูลจำนวนมากเกี่ยวกับปัญหาผู้ผลิตและผู้บริโภคและคิวเหตุการณ์จากโลกการพัฒนาเกม

มักจะมีรหัสเช่น

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

เราได้ทำกับ Logger Asynchronousนี้

ขั้นตอนต่อไปคือการประมวลผลข้อความขาเข้า

เพื่อความง่ายให้เริ่มหัวข้อใหม่และรอตลอดไปจนกระทั่งออกจากแอปพลิเคชันหรือตัวบันทึกแบบอะซิงโครนัสจะเพิ่มข้อความใหม่ไปยังคิวที่ค้างอยู่

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

ปัจจัยสำคัญที่ต้องพิจารณาคือความต้องการความเชื่อถือได้ของคุณใน logfiles และความต้องการประสิทธิภาพ อ้างอิงข้อเสีย ฉันคิดว่านี่เป็นกลยุทธ์ที่ยอดเยี่ยมสำหรับสถานการณ์ที่มีประสิทธิภาพสูง

ใช่หรือไม่

มีข้อเสีย - ใช่ - ขึ้นอยู่กับความสำคัญของการบันทึกของคุณและการดำเนินการใด ๆ ต่อไปนี้อาจเกิดขึ้น - บันทึกที่เขียนเรียงตามลำดับการดำเนินการเธรดบันทึกการทำงานไม่สมบูรณ์ก่อนการดำเนินการกิจกรรมเสร็จสมบูรณ์ (ลองนึกภาพสถานการณ์ที่คุณเข้าสู่ระบบ "เริ่มการเชื่อมต่อกับฐานข้อมูล" จากนั้นคุณล้มเหลวเซิร์ฟเวอร์เหตุการณ์การบันทึกอาจไม่ถูกเขียนแม้ว่าเหตุการณ์จะเกิดขึ้น (!))

ควรจะทำในลักษณะที่แตกต่างออกไป - คุณอาจต้องการดูโมเดล Disruptor เพราะเกือบเหมาะสำหรับสถานการณ์นี้

บางทีมันเร็วจนไม่น่าเห็นด้วย หากคุณเป็นตรรกะ "แอปพลิเคชัน" และสิ่งเดียวที่คุณทำคือเขียนบันทึกกิจกรรม - จากนั้นคุณจะได้รับคำสั่งที่มีขนาดความหน่วงแฝงที่ต่ำกว่าโดยการถ่ายทำบันทึก อย่างไรก็ตามถ้าคุณใช้การเรียก DB 5s SQL SQL เพื่อส่งคืนก่อนบันทึกคำสั่ง 1-2 ประโยชน์จะถูกนำมาผสมกัน


1

ฉันคิดว่าการบันทึกโดยทั่วไปแล้วเป็นการดำเนินการแบบซิงโครนัส คุณต้องการบันทึกสิ่งต่าง ๆ หากสิ่งนั้นเกิดขึ้นหรือไม่ขึ้นอยู่กับตรรกะของคุณดังนั้นเพื่อที่จะบันทึกบางสิ่งสิ่งนั้นจะต้องได้รับการประเมินก่อน

ต้องบอกว่าคุณสามารถปรับปรุงประสิทธิภาพการทำงานของแอปพลิเคชันของคุณได้โดยการแคชบันทึกจากนั้นสร้างเธรดและบันทึกลงในไฟล์เมื่อคุณมีการทำงานที่เชื่อมโยงกับ CPU

คุณต้องระบุจุดตรวจของคุณอย่างชาญฉลาดเพื่อที่คุณจะไม่สูญเสียข้อมูลการบันทึกที่สำคัญในช่วงเวลาแคชนั้น

หากคุณต้องการเพิ่มประสิทธิภาพการทำงานในเธรดของคุณคุณต้องปรับสมดุลการทำงานของ IO และการทำงานของ CPU

หากคุณสร้าง 10 เธรดที่ทำ IO ทั้งหมดคุณจะไม่ได้รับการเพิ่มประสิทธิภาพ


คุณจะแนะนำการแคชบันทึกอย่างไร มีรายการเฉพาะคำขอในข้อความบันทึกส่วนใหญ่เพื่อระบุพวกเขาในบริการของฉันคำขอเดียวกันที่เกิดขึ้นบ่อยมาก
Mithir

0

การบันทึกแบบอะซิงโครนัสเป็นวิธีเดียวที่จะไปหากคุณต้องการเวลาแฝงต่ำในเธรดการบันทึก วิธีนี้ทำเพื่อประสิทธิภาพสูงสุดคือผ่านรูปแบบ disruptorสำหรับการสื่อสารแบบเธรดที่ไม่ล็อคและปลอดขยะ ตอนนี้ถ้าคุณต้องการอนุญาตให้หลายเธรดเข้าสู่ระบบพร้อมกันในไฟล์เดียวกันคุณต้องซิงโครไนซ์การเรียกบันทึกและชำระราคาในการช่วงชิงล็อกหรือใช้มัลติเพล็กเซอร์ที่ไม่ล็อค ตัวอย่างเช่นCoralQueueจัดเตรียมคิวการมัลติเพล็กซ์แบบง่ายดังอธิบายด้านล่าง:

ป้อนคำอธิบายรูปภาพที่นี่

คุณสามารถดูCoralLogซึ่งใช้กลยุทธ์เหล่านี้สำหรับการบันทึกแบบอะซิงโครนัส

ข้อจำกัดความรับผิดชอบ:ฉันเป็นหนึ่งในผู้พัฒนาของ CoralQueue และ CoralLog

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.