我正在阅读和处理非常大量的Sql Server数据(数百万行中的数据,数百万行,100行以上)。在每个源行上执行的处理很重要。单线程版本没有达到预期效果。我目前的并行处理版本在一些较小的批次(300,000个源行,1M个输出行)上表现非常好,但是我遇到了一些Out of Memory异常,非常大的运行。
此处提供的答案极大地启发了代码: Is there a way to use the Task Parallel Library(TPL) with SQLDataReader?
以下是一般概念:
获取源数据(数据太大而无法读入内存,因此我们将“流式传输”)
public static IEnumerable<MyObject> ReadData()
{
using (SqlConnection con = new SqlConnection(Settings.ConnectionString))
using (SqlCommand cmd = new SqlCommand(selectionSql, con))
{
con.Open();
using (SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
while (dr.Read())
{
// make some decisions here – 1 to n source rows are used
// to create an instance of MyObject
yield return new MyObject(some parameters);
}
}
}
}
一旦我们达到并行处理的目的,我们希望使用SqlBulkCopy对象来写入数据。因此,我们不希望并行处理单个MyObjects,因为我们希望每个线程执行批量复制。因此,我们将从上面读取另一个返回“批量”MyObjects的IEnumerable
class MyObjectBatch
{
public List<MyObject> Items { get; set; }
public MyObjectBatch (List<MyObject> items)
{
this.Items = items;
}
public static IEnumerable<MyObjectBatch> Read(int batchSize)
{
List<MyObject> items = new List<MyObjectBatch>();
foreach (MyObject o in DataAccessLayer.ReadData())
{
items.Add(o);
if (items.Count >= batchSize)
{
yield return new MyObjectBatch(items);
items = new List<MyObject>(); // reset
}
}
if (items.Count > 0) yield return new MyObjectBatch(items);
}
}
最后,我们开始并行处理“批次”
ObjectProcessor processor = new ObjectProcessor();
ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = Settings.MaxThreads };
Parallel.ForEach(MyObjectBatch.Read(Settings.BatchSize), options, batch =>
{
// Create a container for data processed by this thread
// the container implements IDataReader
ProcessedData targetData = new ProcessedData(some params));
// process the batch… for each MyObject in MyObjectBatch –
// results are collected in targetData
for (int index = 0; index < batch.Items.Count; index++)
{
processor.Process(batch.Item[index], targetData);
}
// bulk copy the data – this creates a SqlBulkCopy instance
// and loads the data to the target table
DataAccessLayer.BulkCopyData(targetData);
// explicitly set the batch and targetData to null to try to free resources
});
以上所有内容都已大大简化,但我相信它包含了所有重要概念。这是我看到的行为:
性能非常好 - 对于合理大小的数据集,我的效果非常好。
然而,随着它的处理,消耗的内存继续增长。对于较大的数据集,这会导致异常。
我通过日志记录证明,如果我减慢数据库的读取速度,它会减慢批量读取的速度,然后创建并行线程(特别是如果我设置了MaxDegreeOfParallelization)。我担心我的阅读速度比我能处理的要快,但是如果我限制线程,它应该只读取每个线程可以处理的内容。
较小或较大的批量大小对性能有一定影响,但使用的内存量与批量大小一致。
哪里有机会在这里恢复一些记忆?由于我的“批次”超出范围,是否应恢复记忆?我可以在前两个层面做些什么来帮助释放一些资源吗?
回答一些问题: 它可以纯粹用SQL完成 - 不,处理逻辑非常复杂(和动态)。一般来说,它正在进行低级二进制解码。 我们尝试过SSIS(取得了一些成功)。问题是源数据的定义以及输出是非常动态的。 SSIS似乎需要非常严格的输入和输出列定义,在这种情况下不起作用。
有人还问过ProcessedData对象 - 实际上这很简单:
class ProcessedData : IDataReader
{
private int _currentIndex = -1;
private string[] _fieldNames { get; set; }
public string TechnicalTableName { get; set; }
public List<object[]> Values { get; set; }
public ProcessedData(string schemaName, string tableName, string[] fieldNames)
{
this.TechnicalTableName = "[" + schemaName + "].[" + tableName + "]";
_fieldNames = fieldNames;
this.Values = new List<object[]>();
}
#region IDataReader Implementation
public int FieldCount
{
get { return _fieldNames.Length; }
}
public string GetName(int i)
{
return _fieldNames[i];
}
public int GetOrdinal(string name)
{
int index = -1;
for (int i = 0; i < _fieldNames.Length; i++)
{
if (_fieldNames[i] == name)
{
index = i;
break;
}
}
return index;
}
public object GetValue(int i)
{
if (i > (Values[_currentIndex].Length- 1))
{
return null;
}
else
{
return Values[_currentIndex][i];
}
}
public bool Read()
{
if ((_currentIndex + 1) < Values.Count)
{
_currentIndex++;
return true;
}
else
{
return false;
}
}
// Other IDataReader things not used by SqlBulkCopy not implemented
}
更新和结论:
我收到了大量有价值的意见,但我想将其总结为一个结论。首先,我的主要问题是,是否还有其他任何事情(我发布的代码)可以积极地回收内存。共识似乎是方法是正确的,但我的特定问题并不完全受CPU限制,因此简单的Parallel.ForEach将无法正确管理处理。
感谢usr的调试建议以及他非常有趣的PLINQ建议。感谢zmbq帮助澄清什么是和未发生的事情。
最后,任何可能正在追逐类似问题的人都可能会发现以下讨论有用:
答案 0 :(得分:8)
我不完全理解Parallel.ForEach
是如何拉动项目的,但我认为默认情况下它会提取多个以节省锁定开销。这意味着多个项目可能会在Parallel.ForEach
内部排队。这可能会很快导致OOM,因为您的项目非常大。
你可以尝试给它一个Partitioner
that returns single items。
如果这没有帮助,我们需要深入挖掘。调试Parallel
和PLINQ的内存问题是令人讨厌的。例如,其中一个中存在错误导致旧物品无法快速释放。
作为解决方法,您可以在处理后清除列表。这将至少允许在处理完成后确定性地回收所有项目。
关于您发布的代码:它是干净的,高质量的,并且您遵守高标准的资源管理。我不会怀疑你的内存或资源泄漏。这仍然不是不可能的。您可以通过在Parallel.ForEach
内注释掉代码并将其替换为Thread.Sleep(1000 * 60)
来对此进行测试。如果泄漏仍然存在,那你就没有错。
根据我的经验,PLINQ更容易获得精确的并行度(因为当前版本使用您指定的精确DOP,永远不会更少)。像这样:
GetRows()
.AsBatches(10000)
.AsParallel().WithDegreeOfParallelism(8)
.Select(TransformItems) //generate rows to write
.AsEnumerable() //leave PLINQ
.SelectMany(x => x) //flatten batches
.AsBatches(1000000) //create new batches with different size
.AsParallel().WithDegreeOfParallelism(2) //PLINQ with different DOP
.ForEach(WriteBatchToDB); //write to DB
这将为您提供一个从数据库中提取的简单管道,使用针对CPU优化的特定DOP进行CPU绑定工作,并使用更大批量和更少DOP写入数据库。
这非常简单,它应该使用各自的DOP独立地最大化CPU和磁盘。玩DOP号码。
答案 1 :(得分:1)
你在记忆中保留了两件事 - 输入数据和输出数据。您已经尝试并行读取和处理这些数据,但是您并没有减少整体内存占用 - 您仍然最终将大部分数据保留在内存中 - 您拥有的线程越多,您在内存中保留的数据就越多。 / p>
我猜大部分内存都是由输出数据占用的,因为你创建的输出记录比输入记录多10倍。所以你有几个(10?30?50)SqlBulkCopy操作。
实际上太多了。通过批量写入100,000条记录,您可以获得很多的速度。你应该做的是拆分工作 - 读取10,000-20,000条记录,创建输出记录,将SqlBulkCopy写入数据库,然后重复。你的记忆消耗量会大幅下降。
当然,您可以并行执行此操作 - 并行处理多个10,000个记录批次。
请记住,Parallel.ForEach和一般的线程池旨在优化CPU使用率。有可能限制您在数据库服务器上的I / O.虽然数据库可以很好地处理并发性,但它们的限制并不依赖于客户端计算机上的内核数量,因此您最好使用并发线程数并查看最快的内容。