非同期のロギング-どのように実行する必要がありますか?


11

私が取り組んでいるサービスの多くでは、多くのロギングが行われています。サービスは、.NET EventLoggerクラスを使用する(主に)WCFサービスです。

私はこれらのサービスのパフォーマンスを改善している最中です。非同期にログを記録するとパフォーマンスが向上すると思います。

複数のスレッドがログを要求したときに何が起こるか、そしてそれがボトルネックを作成するかどうかはわかりませんが、それが実行されない場合でも、実行されている実際のプロセスに干渉しないはずです。

私が思っているのは、今呼び出す同じログメソッドを呼び出す必要があるということですが、実際のプロセスを続行しながら、新しいスレッドを使用して呼び出します。

それに関するいくつかの質問:

大丈夫ですか?

欠点はありますか?

別の方法で行う必要がありますか?

多分それはあまりにも速くて、努力する価値すらありませんか?


1
ロギングがパフォーマンスに測定可能な影響を与えることを知るために、ランタイムをプロファイルしましたか?コンピューターは複雑すぎて、何かが遅いかもしれないと考えたり、2回測定したり、1回カットしたりすることは、どの職業でも良いアドバイスです=)
Patrick Hughes

@PatrickHughes-特定の1つのリクエストに対する私のテストからの統計:61(!!)ログメッセージ、なんらかの単純なスレッド化を実行する前の150ミリ秒、90ミリ秒後。40%高速です。
ミシール

回答:


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ペンギングメッセージキューに追加して、より重要なものに戻ります。

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

考慮すべき重要な要素は、ログファイルの信頼性の必要性とパフォーマンスの必要性です。欠点を参照してください。これは、パフォーマンスの高い状況に最適な戦略だと思います。

大丈夫ですか-はい

マイナス面はありますか-はい-ロギングの重要度と実装に応じて、次のいずれかが発生する可能性があります-シーケンス外に書き込まれたログ、イベントアクションが完了する前に完了していないログスレッドアクション。(「DBへの接続の開始」をログに記録し、その後サーバーをクラッシュさせるシナリオを想像してください。ログイベントは、イベントが発生しても(!)

別の方法で実行する必要がある場合-このシナリオにはほぼ理想的であるため、Disruptorモデルを確認することをお勧めします。

多分それは非常に速いので、努力する価値すらありません-同意しません。あなたが「アプリケーション」ロジックであり、あなたがする唯一のことはアクティビティのログを書くことであるなら、あなたはロギングをオフロードすることにより桁違いに低いレイテンシになります。ただし、1〜2のステートメントをログに記録する前に、5秒のDB SQL呼び出しに依存している場合、利点はさまざまです。


1

ロギングは本質的に同期操作であると思います。それらが発生した場合、またはロジックに依存しない場合にログに記録したいので、何かをログに記録するには、まず評価する必要があります。

そうは言っても、CPUにバインドされた操作がある場合は、ログをキャッシュしてからスレッドを作成してファイルに保存することで、アプリケーションのパフォーマンスを向上させることができます。

キャッシュ期間中に重要なログ情報を失わないように、チェックポイントを巧みに識別する必要があります。

スレッドのパフォーマンスを向上させたい場合は、IO操作とCPU操作のバランスをとる必要があります。

すべてがIOを実行する10個のスレッドを作成すると、パフォーマンスは向上しません。


ログのキャッシュをどのように提案しますか?ほとんどのログメッセージには、それらを識別するための要求固有の項目があります。私のサービスでは、まったく同じ要求がほとんど発生しません。
Mithir、2012年

0

非同期ロギングは、ロギングスレッドで低レイテンシが必要な場合の唯一の方法です。最大のパフォーマンスを得るためにこれが行われる方法は、ロックフリーおよびガベージフリーのスレッド通信のディスラプターパターンによるものです。ここで、複数のスレッドが同じファイルに同時にログを記録できるようにする場合は、ログの呼び出しを同期して、ロックの競合で代金を支払うか、ロックのないマルチプレクサを使用する必要があります。たとえば、CoralQueueは、以下に説明するような単純な多重化キューを提供します。

ここに画像の説明を入力してください

非同期ロギングにこれらの戦略を使用するCoralLogご覧ください。

免責事項:私はCoralQueueおよびCoralLogの開発者の1人です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.