TPL Dataflow - 生产速度非常快,消费者OutOfMemory异常

时间:2016-10-31 10:21:19

标签: c# .net producer-consumer tpl-dataflow

我正在尝试使用TPL Dataflow,然后将其移植到我的生产代码中。 生产代码是传统的生产者/消费者系统 - 生产者生成消息(与金融领域相关),消费者处理这些消息。

我感兴趣的是,如果在某些时候生产者的生产速度比消费者能够处理的速度快得多(系统会爆炸,或者会发生什么),那么环境将会保持稳定。更重要的是在这些情况下该怎么做。

因此,为了尝试使用类似的简单应用,我想出了以下内容。

    var bufferBlock = new BufferBlock<Item>();

    var executiondataflowBlockOptions = new ExecutionDataflowBlockOptions
                        {
                            MaxDegreeOfParallelism = Environment.ProcessorCount
                            ,
                            BoundedCapacity = 100000
                        };

        var dataFlowLinkOptions = new DataflowLinkOptions
                        {
                            PropagateCompletion = true
                        };

        var actionBlock1 = new ActionBlock<Item>(t => ProcessItem(t),
 executiondataflowBlockOptions);

            bufferBlock.LinkTo(actionBlock1, dataFlowLinkOptions);
            for (int i = 0; i < int.MaxValue; i++)
            {
                 bufferBlock.SendAsync(GenerateItem());
            }

            bufferBlock.Complete();
            Console.ReadLine();

Item是一个非常简单的类

internal class Item
    {
        public Item(string itemId)
        {
            ItemId = itemId;
        }

        public string ItemId { get; }
    }

GenerateItem只是新闻Item

static Item GenerateItem()
{
   return new Item(Guid.NewGuid().ToString());
}

现在,为了模仿不那么快消费者 - 我让ProcessItem代表100ms

static async Task ProcessItem(Item item)
{
    await Task.Delay(TimeSpan.FromMilliseconds(100));
    Console.WriteLine($"Processing #{item.ItemId} item.");
}

执行此操作会导致20秒左右的OOM异常。

然后我继续添加更多的消费者(更多的ActionBlocks,最多10个),这会赢得更多的时间,但最终会导致相同的OOM异常。

我还注意到GC受到巨大压力(VS 2015诊断工具显示GC几乎一直在运行),所以我为Item引入了对象池(非常简单,实际上是ConcurrentBag存储项) ,但我仍然在同一堵墙上(抛出OOM异常)。

提供内存中的内容的一些细节,为什么它已经用完了。

  • 最大尺寸的对象类型为SingleProducerSingleConsumerQueue+Segment<TplDataFlow.Item>&amp; ConcurrentQueue+Segment<TplDataFlow.Item>
  • 我看到BufferBlock的InputBuffer已满Item秒(计数= 14,562,296)
  • 自从BoundedCapacity(s)设置ActionBlock后,其输入缓冲区也接近可配置数字(InputCount = 99,996)

为了确保较慢的生产者能够让消费者跟上,我让生产者在迭代之间睡觉:

for (int i = 0; i < int.MaxValue; i++)
{
    Thread.Sleep(TimeSpan.FromMilliseconds(50));
    bufferBlock.SendAsync(GenerateItem());
}

它工作正常 - 没有异常被抛出,内存使用率一直很低,我不再看到任何GC压力了。

所以我很少有问题

  1. 在尝试使用TPL Dataflow构建块重现非常快的生产者/慢速消费者场景时,我做了什么本质错误
  2. 有没有办法让这项工作成功,而且不会因OOM异常而失败。
  3. 关于如何在TPL数据流上下文中处理此类场景(非常快的生产者/慢速消费者)的最佳实践的任何评论/链接。
  4. 我对这个问题的理解是 - 由于消费者无法跟上,BufferBlock的内部缓冲区正在快速地填充消息,并且在消费者回来询问消息之前一直保持消息下一条消息作为结果应用程序内存不足(由于填充了BufferBlock的内部缓冲区) - 您是否同意这一点?
  5. 我正在使用Microsoft.Tpl.Dataflow包-version 4.5.24。 .NET 4.5(C#6)。进程是32位。

2 个答案:

答案 0 :(得分:6)

您已经很好地确定了问题:BufferBlock正在填充其输入缓冲区,直到它到达OOM。

要解决此问题,您还应该在缓冲区块中添加BoundedCapacity选项。这将为您自动限制生产者(不需要生产者中的Thread.Sleep。)

答案 1 :(得分:1)

以下代码可能存在严重的问题:

for (int i = 0; i < int.MaxValue; i++)
{
    bufferBlock.SendAsync(GenerateItem()); // Don't do this!
}

SendAsync方法返回一个Task,这在对象内存方面可能比要发送到块的实际项目重得多。在特定示例中,由于BufferBlock具有无限制的容量,因此返回的任务始终会完成,因此该任务的内存占用量几乎为零(始终返回相同的缓存Task<bool>实例)。但是,在使用较小的BoundedCapacity值配置块之后,事情很快就会变得有趣起来(以不愉快的方式)。每次对SendAsync的调用将很快开始返回不完整的Task,每次都不同,每个任务的内存占用量约为200字节(如果还使用CancellationToken参数,则占用300字节)。显然,这将无法很好地扩展。

解决方案是按照预期的方式使用SendAsync。这意味着应该等待它:

for (int i = 0; i < int.MaxValue; i++)
{
    await bufferBlock.SendAsync(GenerateItem()); // It's OK now
}

这样,生产者将被异步阻止,直到块内有足够的空间容纳已发送的项目。希望这是您想要的。否则,如果您不想阻止生产者,请不要使用异步SendAsync方法,而应使用同步Post方法:

for (int i = 0; i < int.MaxValue; i++)
{
    var item = GenerateItem();
    while (true)
    {
        bool accepted = bufferBlock.Post(item); // Synchronous call
        if (accepted) break; // Break the inner loop
        if (bufferBlock.Completion.IsCompleted) return; // Break both loops

        // Here do other things for a while, before retrying to post the item
    }
}