需要一种按日期对100GB日志文件进行排序的方法。

33

由于某种奇怪的原因,我得到了一个未排序的100GB日志文件(实际上它是部分排序的),而我尝试使用的算法需要排序的数据。日志文件中的一行如下所示:

data <date> data data more data

我有一台工作站,可以使用C# 4.0和大约4GB的RAM。我想,在这里最好使用某种归并排序算法,但除了自己实现这些算法之外,我想问是否有一些捷径可走。
顺带一提,使用DateTime.Parse()解析日期字符串非常缓慢,并且会占用大量CPU时间-速率仅为10 MB/sec。以下是否有更快的方法?
    public static DateTime Parse(string data)
    {            
        int year, month, day;

        int.TryParse(data.Substring(0, 4), out year);
        int.TryParse(data.Substring(5, 2), out month);
        int.TryParse(data.Substring(8, 2), out day);

        return new DateTime(year, month, day);
    }

我写了一个程序来加速 DateTime.Parse(),它确实起到了作用,但仍然需要大量的计算周期。

请注意,对于当前的日志文件,我也需要关注小时、分钟和秒。我知道我可以为 DateTime.Parse() 提供格式,但似乎并没有显著提高速度。

我正在寻找正确的方向,谢谢您的帮助。

编辑:有些人建议我使用字符串比较来比较日期。这对排序阶段是有效的,但我确实需要解析日期以进行算法处理。我仍然不知道如何在只有 4GB 可用内存的情况下手动排序 100GB 的文件。

编辑2:多亏了一些人建议我使用 Windows sort,我发现了一款类似 Linux 工具。基本上你调用 sort 命令,它就会为你解决所有问题。目前它正在执行某些操作,我希望它能尽快完成。我正在使用的命令是:

sort -k 2b 2008.log > 2008.sorted.log

-k表示我要按照第二行排序,这一行是一个通常的YYYY-MM-DD hh:mm:ss.msek格式的日期时间字符串。我必须承认man手册没有解释所有的选项,但我通过运行info coreutils 'sort invocation'找到了很多示例。

我会回报结果和时间。这部分日志大约有27GB。我正在考虑分别对2009年和2010年进行排序,然后使用sort -m选项将结果合并成一个文件。

编辑3检查iotop表明它正在读取数据文件的小块,然后疯狂地做某些事情以处理它们。这个过程似乎相当缓慢。=(

sort没有使用任何内存,只有一个核心。当它从驱动器中读取数据时,它没有处理任何东西。我做错了什么吗?

编辑4三个小时过去了,它还在做同样的事情。现在我已经到了想尝试调整函数参数的阶段,但我已经投入了三个小时......我将在四个小时内放弃它,并尝试使用更智能的内存和空间参数进行整夜计算......

编辑5在回家之前,我使用了以下命令重新启动了这个过程:

sort -k 2b --buffer-size=60% -T ~/temp/ -T "/media/My Passport" 2010.log -o 2010.sorted.log

今天早上它返回了这个:
sort: write failed: /media/My Passport/sortQAUKdT: File too large

哇啦哇啦!我本以为只要添加尽可能多的硬盘就能加快这个过程。但是,添加USB驱动器似乎是最糟糕的想法。目前,我甚至无法确定它是否与FAT/NTFS等有关,因为fdisk告诉我USB驱动器是一个“错误的设备”……不是开玩笑吗?稍后我会再试一次,现在让我们把这个项目放到可能失败的堆里。

最终通知 这一次成功了,使用与上面相同的命令,但没有问题的外部硬盘。谢谢大家的帮助!

基准测试

使用两个工作站级别(至少70MB/秒读/写IO)的硬盘在同一个SATA控制器上,花费了我162分钟来排序一个30GB的日志文件。今晚我还需要排序另一个52 GB的文件,我会发布进展情况。


3
你需要创建一个DateTime对象吗?我的意思是,数据格式为yyyy-mm-dd,你可以使用字符串排序来对这些日期进行排序,这应该会大大加快比较的速度。 - willvv
1
你如何读取文件?你如何存储数据? - Leo
我使用File.ReadLines().Select(line => line.Split('\t'))读取文件,该文件以制表符作为分隔符存储为平面文本文件。请注意,line.Split()占用了大约10%的计算量,这可能是正常的... - Gleno
请告诉我们进展如何,请! - James Westgate
1
在Oracle中,您可以将平面文件定义为“虚拟”表。这可能是实现它的最简单方法。 - NoChance
显示剩余8条评论
16个回答

18

这样的代码完全取决于你从磁盘获取数据的速度。该文件永远无法适应文件系统缓存,因此您总是在等待磁盘提供数据。以 10 MB/秒的速度表现还算不错,优化代码永远无法产生明显的效果。

购买更快的磁盘。作为中间步骤,请对您拥有的磁盘进行碎片整理。


4
我不知道该说什么,除了你完全错了。如果我不为每一行解析字符串,那么我可以以更舒适的速度达到80 MB/秒。但如果我解析字符串,那么CPU利用率将达到25%(4个核心,因此是1个核心的100%),整个进程会变慢到大约10 MB/秒左右。 - Gleno
11
看到一个核心的使用率达到100%确实非常糟糕。这应该完全被磁盘拖垮了。80 MB/sec很好,大多数消费级别的驱动器都无法超过~65 MB/sec。对于分析器来说是绝佳的机会。请在您的问题中透露这种信息,而不是在评论中,以避免阅读不准确的答案而为自己添麻烦。 - Hans Passant
1
这正是我发帖说明DateTime.Parse需要很长时间的原因。我进行了分析,发现这是瓶颈所在。如果我表述不清楚,我很抱歉。 - Gleno

16
如果使用字符串排序可以解决问题,那么就直接使用Windows的SORT命令。对文件进行排序即可完成任务。它可以轻松处理100GB的文件,并且使用简单。
如果需要过滤和转换文件,尤其是日期字段,我建议编写一个小型转换程序,将数据字段转换为填充0的整数(例如自1970年以来的秒数或其他格式),然后重写记录。 然后可以将输出流(|)输入到sort命令中,这样你就得到了一个最终的、已排序的文件,更容易被你的工具程序解析。
我认为你犯的错误就是试图一次性完成所有操作。100GB的数据是很多的,复制需要一些时间,但并不需要很长时间。因为你必须进行排序,所以在某个时候都必须处理文件的副本(例如使用归并排序等外部排序方法),即使是两个副本都需要占用机器上的大量空间。
编写一个简单的格式化程序并通过管道输入到排序命令中,可以节省反复读取文件的时间,并节省磁盘空间,因为你只需要这两个副本。
我还会调整格式化程序,只提取我真正感兴趣的字段,并在那一点进行所有“繁重”的解析,以便你最终得到的是一个易于处理的已格式化文件,可以轻松处理你的报表程序。如果可能,使用简单的CSV或固定长度的文件格式进行输出。
确保日期信息(如果选择使用整数)各个字段的长度相同。否则,SORT命令将无法正确排序它们(例如,你会得到1 10 2 3而不是1 2 3 10的结果,最好有01 02 03 10这样的输出)。最大的问题是“你是否需要所有这些数据”。这与早期建议中关于先进行重解析有关。显然,您能够减少初始集合的数量越多就越好。例如,仅删除10%的数据就是10GB。
特别是处理大量数据时,我喜欢想到一个经验法则:“如果您有100万个东西,那么每保存1毫秒,就可以缩短20分钟的时间。”
通常情况下,我们在工作中真的不会考虑毫秒,而是更多地凭感觉而定,“感觉更快”。但是1毫秒== 2000000/100000分钟是一个很好的度量标准,可以了解您要处理多少数据以及应该/可能需要多长时间。
对于您的情况,有100GB的数据。每个记录的大约为100字节,您将获得10亿行。每毫秒20,000分钟。-5.5小时。 (这是一个经验法则,如果您进行计算,它不完全符合这一点。)
因此,您可以理解尽可能减少原始数据的愿望。
这是我推迟使用Windows SORT命令的原因之一。这是一个基本的过程,但受到细微差别的影响,并且可以进行一些优化。编写SORT的人员有时间和机会使其“最优”,在许多方面。他们是否这样做,我无法确定。但是,可以合理地假设他们会花更多的时间和精力来使其SORT尽可能好,而不是像您一样,处于严格的截止日期下。
对于大型数据集,有第三方排序实用程序,可能(理想情况下)更适合该情况。但是,这些对您不可用(您可以获取它们,但我认为您不希望立即赶去获取其他实用程序)。因此,现在我们最好猜测SORT。
也就是说,减少数据集将比任何排序实用程序获得更多收益。你到底需要多少详细信息?你真正追踪的信息量有多少?比如说,如果是网络统计数据,你的网站可能有1000页。但即使每小时记录一年,365 * 24 * 1000,也只有8.7M个“桶”信息,距离10亿相差甚远。
那么,有没有一些预处理方法可以不需要排序呢?是否可以将信息总结成更粗略的粒度?你可以使用基于内存的哈希映射来实现这一点,而无需排序。即使你没有足够的内存来一次处理所有100GB的数据,也可能有足够的内存来分块处理(例如5块、10块),并输出中间结果。
你还可以把数据分割得更好。按月或按周将文件分成几块。也许这不容易做到,因为数据“大多数”已经排序过了。但在这种情况下,如果按日期排序,罪犯(即排序错误的数据)可能会聚集在文件中,而“排序错误”的部分仅仅是在时间段(比如在日转换周围)的边界上搅和在一起(例如11:58pm,11:59pm,00:00am,00:01am,11:58pm,00:02pm)。你也许可以利用这种启发式方法。
目标是,如果你可以在某种程度上确定那些是排序错误的子集,并将文件分成“正序数据”和“乱序数据”的块,那么你的排序任务可能会小得多。对那些不按顺序的少数行进行排序,然后就有了合并问题(比排序问题简单得多)。
因此,这些是解决问题的策略。总结显然是最好的方法,因为任何可减少任何可测量数据负载的事情都很值得尝试。当然,所有问题归根结底都取决于你真正想从数据中得到什么,报告会驱动这一点。这也是关于“过早优化”的一个好例子。如果他们没有报告这个问题,请不要处理它 :)。

2
到目前为止,这是迄今为止最好的答案。 - Gabe
威尔,我编辑了我的帖子以反映我正在尝试你的解决方案。感谢你的建议。有什么加速的技巧吗? - Gleno
Gleno:一定要让它使用大量的缓冲区和在快速硬盘上分配足够的临时空间! - Gabe
谢谢Will,它非常成功;尽管我使用了Linux的排序功能。 :) - Gleno
微软还提供了日志解析器(Log Parser),该工具已经被优化,可以对大型文本文件进行排序等操作:http://www.microsoft.com/downloads/en/details.aspx?FamilyID=890cd06b-abf8-4c25-91b2-f8d975cf8c07&displaylang=en - steamer25
显示剩余2条评论

13

简短回答:将数据加载到关系型数据库中(如Sql Express),创建索引,并使用基于游标的解决方案,例如DataReader从数据库中读取每个记录并将其写入磁盘。


4
我不确定为什么人们要费心费力地复制每个免费SQL类型服务器内已存在的功能。 - NotMe
1
@Gabe。Sql Express是免费的,只限制并发连接数。2.可以使用SSIS或其他可用工具(并可能再次导出)来加载数据。编辑:实际上,Sql Express仅限于4GB数据库,所以我猜它是180天试用版。 - James Westgate
4
詹姆斯:你看到了吗?你把一个排序问题变成了“研究数据库,学习如何使用数据库,学习如何调试数据库中出现的问题,学习如何编写访问数据库的代码,学习如何对数据库进行排序”的问题。如果你已经是数据库管理员,这可能是显而易见的解决方案,但对于普通程序员来说,花几个小时编写在CS101中学习的归并排序算法要简单得多。 - Gabe
1
不同意。当我们的 OP 弄清楚他在做什么时,代码已经完美地运行了 - 关系型数据库(使用处理器的所有核心)将索引该数据。它并不是那么大。我曾经在 Site Server 中使用过 IIS 日志文件和那么大的表格,而那已经是几年前的事情了。 - James Westgate
3
哇,数据库管理员本来就可以解决这个问题!数据已经存储在数据库中了! - James Westgate
显示剩余13条评论

9
为什么不试一下微软相对较为不知名的工具——日志分析器(logparser)呢?它基本上允许你在 CSV 文件(或任何其他格式的文本文件)上执行 SQL 查询。这样可以省去将其导入数据库、排序和再次导出的麻烦。

1
这是一个不错的想法,但我担心它就像一颗魔法子弹。当你处理大数据集时,虽然它声称可以为你排序,但采用次优方法可能会让你吃亏。 - Gleno
@gleno:它应该可以处理来自IIS的日志文件,这些文件可能也很大。谁知道呢,也许它会起作用。如果真的起作用的话,可以为你节省很多时间。;^) - Toad
是的,有人在这里发帖说它可能也针对大文件进行了优化。非常酷。我一定会稍后去看看。感谢您的建议! - Gleno
我尝试使用LogParser对一个807MB的日志文件进行排序并输出排序后的版本。测试是在一台配备4GB内存和4个核心的PC上运行的。大约15分钟后,我取消了测试。当时,LogParser正在使用所有可用的内存。我认为这不是对于排序日志文件的可行选项。这看起来更有前途:http://splinter.com.au/sorting-enormous-files-using-a-c-external-mer - jmacinnes

8

回答您有关对长文件进行排序的问题 - 您需要使用一些 外部排序 算法,例如归并排序。 大致过程如下:

  • 将输入分成几个适合内存且可以使用标准内存排序算法进行排序的部分(例如100 MB或更大 - 您需要同时在内存中保留约4个部分)。 对所有部分进行排序并将它们写回磁盘。

  • 从磁盘中读取两个部分(它们都已排序)并将它们合并,这可以通过同时迭代两个输入来完成。 将合并的数据集写入磁盘上的另一个位置。 请注意,您不需要将整个部分读入内存 - 只需在进行读取/写入时按块读取即可。

  • 重复合并部分,直到只剩下一个部分(这将是您原始输入数据集中所有数据的已排序文件)。

您提到数据已经部分排序,因此在内存中选择一些高效的算法进行排序(第一阶段)是个好主意。您可以在这个问题中看到一些建议(尽管我不确定对于非常大的数据集答案是否相同 - 这取决于输入有多少部分排序)。

谢谢你把它都打出来了,但我有点知道归并排序是怎么工作的。我正在寻找一个现成的实现 - 一种快捷方式。我想象中像.NET这样的库已经为我解决了所有问题。 :) - Gleno
@Gleno:好的 :-). 我认为这会有所帮助,因为你写道_"我仍然不知道如何在4GB的可用内存上对100GB文件进行排序"_。无论如何,我还没有看到任何现有的库可以做到这一点 - 编写一个通用库可能相当困难,因为使用情况会有很大的不同。 - Tomas Petricek
4
复杂的...使用任何SQL数据库。 - JPCF

4
优化解析日期的最好方法是根本不需要解析。
由于日期采用ISO 8601格式,只需将它们作为字符串进行比较即可,无需进行解析。
关于排序,您应该能够有效地利用其部分排序的事实。一种方法是读取文件并将其写入按时间范围划分的单独文件中,例如每天或每小时一个文件。如果每个文件足够小,则可以将它们读入内存并对其进行排序,然后合并所有文件。
另一种方法是读取文件并将顺序排列的记录写入一个文件,而将其他记录写入另一个文件。对第二个文件进行排序(如果它非常大,则可能会递归使用此过程),然后将两个文件压缩在一起。这就是修改后的归并排序。

这实际上是一个非常好的想法,但正如我在编辑中评论的那样,我确实需要解析日期。实际上,我需要知道事件组之间的秒差异,所以我正在减去两个 DateTime 对象。这似乎非常快,至少与 DateTime.Parse() 相比。 - Gleno
嗯,我很喜欢将不合适的数据剔除的想法,以至于我目前正在运行一个忽略所有这些数据的测试。你认为如果orderedseta很大而orderedsetb很小,.NET能够高效地执行orderedseta.Concat(orderedsetb).OrderBy(e => e)吗?我有些怀疑...也许有一种特定的容器可以做到这一点... - Gleno
@Gleno:您需要将所有数据装入内存才能使用OrderBy方法,它在开始返回任何项之前必须将整个源读入内存。 - Guffa
但是如果我使用一些被保证已经排序的神奇容器,它难道不会自动知道该怎么做吗? - Gleno
@Gleno:不,Concat方法只是返回一个集合中的所有项,然后返回另一个集合中的所有项,所以OrderBy方法不知道有两个集合。 - Guffa

4

对于排序,您可以实现基于文件的桶排序:

  1. 打开输入文件
  2. 逐行读取文件
  3. 从行中获取日期字符串
  4. 将行附加到文件 <date>.log

结果将是每天一个独立的日志文件,或者每小时一个独立的日志文件。选择使您能够轻松排序的文件大小。

剩下的任务是对创建的文件进行排序,可能需要再次合并文件。


+1,我觉得这可能是一个简单而优雅的解决方案。可能需要保留一组打开文件的缓存,以最小化不断打开关闭此类文件的影响,也许可以使用LRU缓存,因为OP说数据已经部分排序。 - Matteo Italia

3
我确实需要为算法解析日期。在*NIX上,我通常会先将日期转换成适合文本比较的简单格式,并将其作为字符串的第一个单词。现在还不到创建日期/时间对象的时候。我的常用日期格式是YYYYMMDD-hhmmss.millis。请确保所有文件都具有相同的日期格式。
我仍然不知道如何在只有4GB可用内存的情况下手动排序100GB大小的文件。正如你已经发现的那样,归并排序是唯一的选择。因此,对我来说,任务分为以下几步:
1.愚蠢的转换使日期可以排序。复杂度:顺序读/写100GB。
2.将数据拆分成可用尺寸的块,例如1GB,并在将其写入磁盘之前使用纯快速排序对每个块进行排序。复杂度:顺序读/写100GB;快速排序所需的内存。
3.将小文件归并排序成一个大文件。可以逐步执行此操作,使用一个程序将两个文件合并成一个文件。复杂度:顺序读/写100GB log(N)次(N是文件数)。HDD空间要求:2*100GB(将2个50GB文件合并成单个100GB文件的最后一次合并)。
4.编写自动执行步骤3的程序:选取两个(例如最小的)文件,启动程序将它们排序合并成一个新文件,删除原始文件。重复此操作,直到文件数大于1。
5.(可选)将100GB排序后的文件拆分成更小的易处理块。毕竟您将对它们执行某些操作。按顺序编号或将第一个和最后一个时间戳放入文件名中。
总体概念:不要试图找到快速完成任务的方法,传输100GB的数据本身就需要时间;计划在每个步骤上运行程序,作为批处理在夜间自动运行,无需您的注意。
在Linux上,可以使用shell/sort/awk/Perl实现所有这些,我不认为用任何其他编程语言编写这些都是问题。这可能涉及4个程序,但所有这些程序都相当简单。

3
假设您的日志文件只有1-2%的行是无序的,您可以通过完整地遍历日志文件来输出两个文件:一个文件是有序的,另一个文件包含1-2%的无序行。然后在内存中对无序行进行排序,并将原本无序的行与有序行进行一次合并。这比完全的归并排序要快得多,因为它只需要进行少量的遍历。
假设您的日志文件中没有行超过N行的位置,您可以通过具有N行深度的排序队列对日志文件进行单次遍历。每当您遇到一个无序的日志行时,只需将其插入队列中正确的位置即可。由于这只需要对日志进行一次遍历,所以速度是最快的。

2

先说一下:我的回答只涉及解析日期时间值的子问题。

DateTime.Parse 包含了对所有可能的日期格式的检查。如果你有一个固定的格式,你可以优化解析过程。一个简单的优化方法是直接转换字符:

class DateParserYyyyMmDd
{
    static void Main(string[] args)
    {
        string data = "2010-04-22";

        DateTime date = Parse(data);
    }

    struct Date
    {
        public int year;
        public int month;
        public int day;
    }

    static Date MyDate;

    static DateTime Parse2(string data)
    {
        MyDate.year = (data[0] - '0') * 1000 + (data[1] - '0') * 100 
            + (data[2] - '0') * 10 + (data[3] - '0');
        MyDate.month = (data[5] - '0') * 10 + (data[6] - '0');
        MyDate.day = (data[8] - '0') * 10 + (data[9] - '0');

        return new DateTime(MyDate.year, MyDate.month, MyDate.day);
    }
}

嗯,这就像我尝试过的一样,但我没有想到索引比子字符串更快,显然它必须是这样的。 - Gleno

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接