我对Rx Buffer
运算符有一个奇怪的问题,我找不到合适的解决方案而且我不知道我做错了什么。如果第9行的Buffer
未使用EventLoopScheduler
,那么在没有项目被推送(item
)的情况下,它会在一段时间后开始泄漏内存吗?
第1行的item
是IObservable<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);
});
});
}
以下是负责读取发送到服务器的数据的代码。 WaitHandle
是EventWaitHandle
的包装器,如果数据库脱机以阻止数据在系统中累积,它将被阻塞。 (当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工具确认了内存泄漏。
答案 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);
为了解决记忆压力的问题,我强烈建议你提供一些让一群人过期的方法。即使只是在一段时间之后杀死您的订阅也很简单,然后立即重新订阅(Retry
和Publish
可能会有所帮助)。否则,如果您获得仅出现一次的类型/密钥对,您将支付组的价格,从而为整个订阅的生命周期支付缓冲区。
最后,在查看内存压力问题时,我会建议您实际捕获或分析您的应用程序,而不是瞥一眼可以向您发送各种虚假信息的任务管理器。尝试GC.GetTotalMemory(true)
或某些WMI挂钩,甚至只追踪GC.CollectionCount
值。