首页 > 解决方案 > 长寿命连接多路复用器日志记录

问题描述

我有一个正在为 Redis 编写的监视服务,StackExchange.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());
    }
}

标签: c#memorystreamstackexchange.redis

解决方案


问题不在于内存流。问题是Console.WriteLine. 向/从 a 写入/读取内容比在典型的 Windows 配置中写入控制台要快得多。MemoryStream您可能会在每次读取内存流时将其归零,但是一旦您清除它,您就会放弃锁定,并且日志写入器开始快速旋转。

在第一次迭代中,假设写日志的线程有 5ms 的时间来写一些日志。将其写入控制台将花费超过5 毫秒,因此当控制台写入线程运行一次时,它的日志价值超过5毫秒,这将比写入前 5 毫秒的日志花费的时间更长。 logs... 所以每次写控制台的线程写完之前的日志状态,它发现它有更多,写出来需要更长的时间:是的,内存流正在消耗所有的内存,但那是因为它需要内存来存储所有日志,而控制台编写线程正忙于消耗最后一次加载。

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

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

我们可以写一些很好的简单方程:

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, thenx呈指数增长:对内存使用不利(尽管它仍然只能随时间线性增长,因为这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叶子应用程序在我的机器上解决了大约 10MB。您还可以在内存使用中看到问题:它时不时地跳转,这是控制台编写器开始新迭代并将整个流 ( byte[]) 复制到char[]( ReadToEnd) 并最终生成 a的 (不频繁) 事件string:可以立即释放它并不重要byte[],因为您有 2 个相同大小的对象来填补空缺。


顺便说一句, usingSetLength(0)只会通过创建更多字节数组来掩盖问题,并且实际上可能增加了峰值内存使用量,因为它不会减少内存流的最大容量,并且意味着周围有丢弃的对象等待被垃圾收集。


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


推荐阅读