Rx缓冲存储器泄漏

时间:2016-06-19 11:11:04

标签: c# memory-leaks system.reactive reactive-programming

我对Rx Buffer运算符有一个奇怪的问题,我找不到合适的解决方案而且我不知道我做错了什么。如果第9行的Buffer未使用EventLoopScheduler,那么在没有项目被推送(item)的情况下,它会在一段时间后开始泄漏内存吗?

第1行的itemIObservable<Entity>,它推送从TCP套接字下游检索的解析数据。使Buffer使用EventLoopScheduler解决问题,但降低整体系统性能。

如何在不强迫Buffer运算符使用EventLoopScheduler的情况下解决内存泄漏问题?

var groupedItems = items
    .GroupBy(entity => entity._type)
    .Select(o => new {Type = o.Key, Categories = o.GroupBy(entity => entity._key)});

var ev = new EventLoopScheduler();

var collections = from item in groupedItems
    from category in item.Categories
    from entities in category.Buffer(intervalTime, intervalSize, /* ev */)
    where entities.Any()
    select new LogCollection(item.Type, category.Key, entities);

collections.Buffer(TimeSpan.FromSeconds(1)).Where(o => o.Any()).Subscribe(Insert);

更新

经过一些调查后,Buffer运算符似乎不是问题,除了它在EventLoopScheduler上安排问题时“解决”了问题。在绝望的情况下,我发布了关键的代码片段,因为我对Rx是相当新的我不知道如果我正确使用范例 - 所以如果我误用它,请纠正我! :)

背景知识:应用程序通过TCP套接字检索二进制数据,并在一些转换后将其插入数据库。

接收

客户端可以连接到服务器,并且将转换从客户端发送的数据。如果约定中发生任何异常,它将捕获异常并断开客户端。

public IObservable<LogEntity> StartListening(IDataConverter converter) 
{
    return Observable.Create<LogEntity>(observer => 
    {
        return _endPoint.ToListenerObservable(_backlog).Subscribe(client => 
        {
            var stream = client.ToClientObservable(_bufferSize, _waitHandle);

            converter.Convert(stream)
            .Catch<LogEntity, Exception>(exception =>
            {
                client.Close(); // dc client
                return Observable.Empty<LogEntity>();
            })
            .Subscribe(observer.OnNext);
        });
    });
}

以下是负责读取发送到服务器的数据的代码。 WaitHandleEventWaitHandle的包装器,如果数据库脱机以阻止数据在系统中累积,它将被阻塞。 (WaitHandle阻止且未检索到任何数据时会出现问题

public static IObservable<ArraySegment<byte>> ToClientObservable(this TcpClient client, int size, WaitHandle waitHandle)
{
    return client.GetStream().ToStreamObservable(size, waitHandle);
}

public static IObservable<ArraySegment<byte>> ToStreamObservable(this Stream stream, int size, WaitHandle waitHandle)
{
    return Observable.Create<ArraySegment<byte>>(async (observer, token) =>
    {
        var buffer = new byte[size];

        try
        {
            while (!token.IsCancellationRequested)
            {
                waitHandle.BlockingWait();
                var received = await stream.ReadAsync(buffer, 0, size, token);
                if (received == 0) break;
                observer.OnNext(new ArraySegment<byte>(buffer, 0, received));
            }
            observer.OnCompleted();
        }
        catch (Exception error)
        {
            observer.OnError(error);
        }
    });
}

转换器

转换器使用Scan运算符来解析数据流。内部可能会出现例外情况。目前,异常将传播到StartListing方法,其中发送错误数据的客户端将断开连接。

public IObservable<LogMessage> Convert(IObservable<ArraySegment<byte>> bytes)
{
    return bytes.Scan(
        new
        {
            Leftovers = new byte[0],
            Logs = new List<LogMessage>(),
        },
        (saved, current) =>
        {
            // Parse bytes
            // Exception here if invalid data retrieved

            return new
            {
                Leftovers = data.ToArray(),
                Logs = logs,
            };
        })
        .SelectMany(o => o.Logs);
}

你们能看到任何可能导致内存泄漏的内容吗?这基本上是所有负责检索数据的代码,在将数据发送到转换阶段之前对其进行转换(第一个问题)。而且,我用dotMemory工具确认了内存泄漏。

1 个答案:

答案 0 :(得分:4)

您的示例代码有一些值得注意的事情。 首先,它不是@Enigmativity指出的MVCE,例如什么类型是items,它的值,它们的属性/(字段?),以及LogCollection的相同类型。

其次,您似乎正在运行过多的GroupBy操作。这将创建可观察序列的3深度嵌套。我想你只想GroupBy一次,依靠anon打字为你做正确的事,即.GroupBy(entity => new { entity.Type, entity.Key})。我这样说是因为一旦你按两次分组,你似乎只是再打开它。

第三,你缓冲两次。两次检查空缓冲区。一旦使用调度程序(也许)而另一个没有?第二个缓冲区似乎是多余的。

第四,您似乎没有关闭任何GroupBy“窗口”。这意味着对于每个嵌套分组,您将创建独立的缓冲区。每个都可以在线程/任务池上运行,具体取决于您的平台。因此,您可以在程序中释放无限制和未知的并发级别。因此,每个新组都使用_type&amp;的新组合创建。 _key您正在创建永远不会停止/处置/清理的新缓冲池,并且会不断消耗资源。

第五,我们不知道你的记忆问题是否只是因为没有足够的记忆压力迫使GC因此你看到内存压力上升。

我认为您的查询可以简化为:

from item in items
group item by new { item.Type, item.Key} into grp
from buffer in grp.Buffer(intervalTime, intervalSize, scheduler)
where buffer.Any()
select new LogCollection(grp.Key.Type, grp.Key.Key, buffer);    

为了解决记忆压力的问题,我强烈建议你提供一些让一群人过期的方法。即使只是在一段时间之后杀死您的订阅也很简单,然后立即重新订阅(RetryPublish可能会有所帮助)。否则,如果您获得仅出现一次的类型/密钥对,您将支付组的价格,从而为整个订阅的生命周期支付缓冲区。

最后,在查看内存压力问题时,我会建议您实际捕获或分析您的应用程序,而不是瞥一眼可以向您发送各种虚假信息的任务管理器。尝试GC.GetTotalMemory(true)或某些WMI挂钩,甚至只追踪GC.CollectionCount值。