长期连接Multiplexer日志记录

时间:2019-03-26 15:00:41

标签: c# memorystream stackexchange.redis

我有一个监视服务,我正在使用StackExchange.Redis为Redis编写并订阅某些事件。我面临的问题是日志记录。它需要一个TextWriter。理想情况下,我希望将其分流到EventLog,所以我使用MemoryStream支持的StreamWriter,并在任务上使用EventLog转储到StreamReader基于计时器。

此实现的问题是,在我的测试中,MemoryStream泄漏严重,即使每次读取后我都使用MemoryStream.SetLength(int)进行清除。 ConnectionMultipler.Connect()方法仅使用一个对象,而我看不到如何替换该对象,这意味着我也必须定期更新ConnectionMultiplexer

这个声音关闭了吗?我想念什么吗?比较简单的方法似乎只管理一个对象,但是我看不到如何控制它。这是一个示例控制台应用程序来演示。

class Program
{
    private static MemoryStream _loggingStream;
    private static StreamReader _reader;

    private static object _padlock = new object();

    static async Task Main(string[] args)
    {
        _loggingStream = new MemoryStream();
        _reader = new StreamReader(_loggingStream);

        var logWriter = new StreamWriter(_loggingStream);

        ThreadPool.QueueUserWorkItem(async state => await WriteLog());

        while (true)
        {
            Monitor.Enter(_padlock);

            try
            {
                await logWriter.WriteLineAsync("hello world " + DateTime.Now.ToLongTimeString());
                await logWriter.FlushAsync();
            }
            finally
            {
                Monitor.Exit(_padlock);
            }
        }
    }

    private static async Task WriteLog()
    {
        while (_loggingStream.Length == 0)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(5));
        }

        string log;

        lock (_padlock)
        {
            _loggingStream.Position = 0;

            log = _reader.ReadToEnd();
            _reader.DiscardBufferedData();
            _loggingStream.SetLength(0);
        }

        Console.WriteLine(log);

        ThreadPool.QueueUserWorkItem(async state => await WriteLog());
    }
}

1 个答案:

答案 0 :(得分:1)

问题不在于内存流。问题是Console.WriteLine。向MemoryStream中写入内容/从d is rate at which logs are produced c is how long it takes to consume a unit of logs x(i) is the volume of logs produced during iteration i of the log-consumer y(i) is how long it takes to consume the logs produced in iteration i 中读取内容要比例如在控制台中写入内容要快得多。典型的Windows配置。您可能会在每次读取时将内存流清零,但是一旦将其清除,便放弃了锁,日志记录器开始非常快速地旋转。

在第一次迭代中,假设日志写入线程有5ms的时间来写入一些日志。将其写入控制台所需的时间少于5ms,因此,当控制台编写线程运行一次之后,其日志记录的时间超过5ms,这将比写入前5ms的日志所花费的时间更长。因此,每次控制台编写线程完成写出日志的先前状态时,都会发现它具有更多的功能,并且需要花费甚至更长的时间来写出来:是的,内存流正在消耗所有内存,但这是因为在控制台编写线程正在忙于消耗内存的同时,它需要内存来存储所有日志。最后一次加载。

这里是一些数学,只是为了好玩:

y(i) = c*x(i)     (time to consume logs is a linear function of volume)
x(i+1) = d*y(i)   (volume is a linear function of time between iterations)

我们可以编写一些简单的方程式:

x(i+1) = d*c*x(i)

随后,我们可以确定每次迭代的日志量(与内存使用量成比例)如何变化

d*c > 1

如果x,则d呈指数增长:对内存使用率不利(尽管它仍只能随时间线性增长,因为1/c是限制因素(请注意,我们正在研究每次迭代的费用,而不是时间))

如果我们考虑d > 1/c (i.e. rate at which logs are produced is greater than the rate at which logs are consumed) (消耗日志的速率),那么很明显,在以下情况下满足此条件

d > 1/c

写到内存流比写到控制台要便宜:Console.WriteLine,所以我们有一个根本的问题,聪明的问题无法解决:您不能写这样的东西到控制台的大量日志。

您可以在输出中看到这个问题,因为时间戳不能跟踪时钟时间:它会立即落后。删除byte[]会使应用程序驻留在我的计算机上大约10MB。您还可以在内存使用量中看到问题:它不时地跳跃,这是控制台编写器开始新迭代并将整个流(char[])复制到{{ 1}}(ReadToEnd)并最终产生一个string:可以立即释放byte[]并不重要,因为您有2个大小相同的对象可以容纳松弛


顺便说一句,使用SetLength(0)仅会通过创建更多的字节数组来掩盖此问题,并且可能实际上增加了峰值内存使用量,因为这不会降低内存流的最大容量,并且周围有丢弃的物体等待被垃圾收集。


正如注释中所讨论的,您不应在线程之间访问监视器。您使用await意味着在将控制权返回到日志写入方法时将保留上下文,但是不能保证您将获得相同的线程。