我们每天最多有30 GB的GZip压缩日志文件。每个文件保存100.000行,压缩后为6到8 MB。解析逻辑已被剥离的简化代码利用Parallel.ForEach循环。
在两个NUMA节点上,MaxDegreeOfParallelism为8处理的最大行数达到峰值,32个逻辑CPU盒(Intel Xeon E7-2820 @ 2 GHz):
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
namespace ParallelLineCount
{
public class ScriptMain
{
static void Main(String[] args)
{
int maxMaxDOP = (args.Length > 0) ? Convert.ToInt16(args[0]) : 2;
string fileLocation = (args.Length > 1) ? args[1] : "C:\\Temp\\SomeFiles" ;
string filePattern = (args.Length > 1) ? args[2] : "*2012-10-30.*.gz";
string fileNamePrefix = (args.Length > 1) ? args[3] : "LineCounts";
Console.WriteLine("Start: {0}", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"));
Console.WriteLine("Processing file(s): {0}", filePattern);
Console.WriteLine("Max MaxDOP to be used: {0}", maxMaxDOP.ToString());
Console.WriteLine("");
Console.WriteLine("MaxDOP,FilesProcessed,ProcessingTime[ms],BytesProcessed,LinesRead,SomeBookLines,LinesPer[ms],BytesPer[ms]");
for (int maxDOP = 1; maxDOP <= maxMaxDOP; maxDOP++)
{
// Construct ConcurrentStacks for resulting strings and counters
ConcurrentStack<Int64> TotalLines = new ConcurrentStack<Int64>();
ConcurrentStack<Int64> TotalSomeBookLines = new ConcurrentStack<Int64>();
ConcurrentStack<Int64> TotalLength = new ConcurrentStack<Int64>();
ConcurrentStack<int> TotalFiles = new ConcurrentStack<int>();
DateTime FullStartTime = DateTime.Now;
string[] files = System.IO.Directory.GetFiles(fileLocation, filePattern);
var options = new ParallelOptions() { MaxDegreeOfParallelism = maxDOP };
// Method signature: Parallel.ForEach(IEnumerable<TSource> source, Action<TSource> body)
Parallel.ForEach(files, options, currentFile =>
{
string filename = System.IO.Path.GetFileName(currentFile);
DateTime fileStartTime = DateTime.Now;
using (FileStream inFile = File.Open(fileLocation + "\\" + filename, FileMode.Open))
{
Int64 lines = 0, someBookLines = 0, length = 0;
String line = "";
using (var reader = new StreamReader(new GZipStream(inFile, CompressionMode.Decompress)))
{
while (!reader.EndOfStream)
{
line = reader.ReadLine();
lines++; // total lines
length += line.Length; // total line length
if (line.Contains("book")) someBookLines++; // some special lines that need to be parsed later
}
TotalLines.Push(lines); TotalSomeBookLines.Push(someBookLines); TotalLength.Push(length);
TotalFiles.Push(1); // silly way to count processed files :)
}
}
}
);
TimeSpan runningTime = DateTime.Now - FullStartTime;
// Console.WriteLine("MaxDOP,FilesProcessed,ProcessingTime[ms],BytesProcessed,LinesRead,SomeBookLines,LinesPer[ms],BytesPer[ms]");
Console.WriteLine("{0},{1},{2},{3},{4},{5},{6},{7}",
maxDOP.ToString(),
TotalFiles.Sum().ToString(),
Convert.ToInt32(runningTime.TotalMilliseconds).ToString(),
TotalLength.Sum().ToString(),
TotalLines.Sum(),
TotalSomeBookLines.Sum().ToString(),
Convert.ToInt64(TotalLines.Sum() / runningTime.TotalMilliseconds).ToString(),
Convert.ToInt64(TotalLength.Sum() / runningTime.TotalMilliseconds).ToString());
}
Console.WriteLine();
Console.WriteLine("Finish: " + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"));
}
}
}
以下是结果摘要,MaxDegreeOfParallelism = 8处有明显的峰值:
CPU负载(此处汇总显示,大部分负载都在单个NUMA节点上,即使DOP在20到30范围内):
我发现CPU负载超过95%标记的唯一方法是将文件分成4个不同的文件夹并执行相同的命令4次,每个命令都针对所有文件的子集。
有人能找到瓶颈吗?
答案 0 :(得分:8)
一个问题可能是默认FileStream
构造函数使用的小缓冲区大小。我建议你使用更大的输入缓冲区。如:
using (FileStream infile = new FileStream(
name, FileMode.Open, FileAccess.Read, FileShare.None, 65536))
默认缓冲区大小为4千字节,其中该线程对I / O子系统进行多次调用以填充其缓冲区。 64K的缓冲区意味着您可以更频繁地进行这些调用。
我发现32K到256K之间的缓冲区大小可以提供最佳性能,当我在一段时间内进行一些详细测试时,64K是“最佳点”。缓冲区大小超过256K实际上会降低性能。
此外,虽然这不太可能对性能产生重大影响,但您可能应该用64位整数替换这些ConcurrentStack
实例,并使用Interlocked.Add
或Interlocked.Increment
来更新它们。它简化了代码,无需管理集合。
更新
重新阅读你的问题描述,我对这句话感到震惊:
我发现使CPU负载超过95%标记的唯一方法是拆分 跨4个不同文件夹的文件并执行相同的命令4 时间,每一个都针对所有文件的子集。
对我而言,这指向打开文件的瓶颈。好像操作系统在目录上使用互斥锁。即使所有数据都在缓存中并且不需要物理I / O,进程仍然需要等待此锁定。文件系统也可能正在写入磁盘。请记住,它必须在文件打开时更新文件的上次访问时间。
如果I / O真的是瓶颈,那么你可能会考虑让一个线程除了加载文件之外什么也不做,并将它们填充到BlockingCollection
或类似的数据结构中,以便处理线程不必彼此争夺锁定目录。您的应用程序成为具有一个生产者和N个消费者的生产者/消费者应用程序。
答案 1 :(得分:2)
原因通常是线程同步太多。
在代码中寻找同步我可以看到集合上的大量同步。你的线程是单独推动线。这意味着每条线最多会产生一个互锁操作,最坏的情况是内核模式锁等待。互锁操作将会激烈竞争,因为所有线程都会竞争将其当前行放入集合中。他们都试图更新相同的内存位置。这会导致缓存行ping。
将其更改为更大块的推线。推线数组为100线或更多。越多越好。
换句话说,首先在线程局部集合中收集结果,但很少合并到全局结果中。
您甚至可能希望完全摆脱手动数据推送。这就是PLINQ的用途:同时传输数据。 PLINQ以良好的方式抽象出所有并发的集合操作。
答案 2 :(得分:2)
我不认为并行化磁盘读取对您有所帮助。事实上,这可能会严重影响您的表现,同时在多个存储区域进行阅读时会产生争用。
我会重构程序,首先将原始文件数据单线程读取到byte []的内存流中。然后,在每个流或缓冲区上执行Parallel.ForEach()以解压缩并计算行数。
您可以预先进行初始IO读取,但让操作系统/硬件优化大多数顺序读取,然后在内存中解压缩和解析。
请记住,像decomprless,Encoding.UTF8.ToString(),String.Split()等操作会占用大量内存,因此在不再需要时会清理/处理旧缓冲区的引用
如果你不能让机器以这种方式产生严重的浪费,我会感到惊讶。
希望这有帮助。
答案 3 :(得分:2)
我认为问题在于您使用阻塞I / O,因此您的线程无法充分利用并行性。
如果我理解你的算法正确(抱歉,我更像是一个C ++人),这就是你在每个线程中所做的事情(伪代码):
while (there is data in the file)
read data
gunzip data
相反,更好的方法是这样的:
N = 0
read data block N
while (there is data in the file)
asyncRead data block N+1
gunzip data block N
N = N + 1
gunzip data block N
asyncRead
调用没有阻塞,所以基本上你有块N的解码与块N + 1的读取同时发生,所以当你完成块N的解码时你可能有块N + 1准备好(如果I / O比解码慢,则接近准备就绪)。
然后,只需找到可以提供最佳吞吐量的块大小。
祝你好运。