1

So I'm trying to import about 80 million page views from a log file. I'm trying to put them in the database as sessions, i.e. groups of page views separated with 20 minutes in between.

So eventually, in my user database I would like each user to have a list of dictionary objects like so:

{
    'id': 'user1'
    'sessions':[
                    {
                        "start" : ISODate("2011-04-03T23:21:59.639Z"),
                        "end" : ISODate("2011-04-03T23:50:05.518Z"),
                        "page_loads" : 136
                    },
                    {
                        "start" : ISODate("another date"),
                        "end" : ISODate("later date"),
                        "page_loads" : 20
                    },
                ]
}

Should be fairly simple. So I wrote this script:

howManyLinesTotal = 9999999 #i've done: wc -l in bash before to find the file size

blank_dict = {'page_loads':0, 'start':0, 'end':0}

latest_sessions = defaultdict(lambda: blank_dict)

for line in f: #opens a gigantic gzip file called "f"
    line = line.split('\t') #each entry is tab-delimited with: user \t datetime \t page_on_my_site

    user = line[1] #grab the data from this line in the file
    timestamp = datetime.utcfromtimestamp(float(line[2]))

    latest_sessions[user]['page_loads'] += 1 #add one to this user's current session

    if latest_sessions[user]['start'] == 0: #put in the start time if there isn't one
        latest_sessions[user]['start'] = timestamp

    if latest_sessions[user]['end'] == 0: #put in the end time if there isn't one
        latest_sessions[user]['end'] = timestamp
    else: #otherwise calculate if the new end time is 20 mins later
        diff = (timestamp - latest_sessions[user]['end']).seconds
        if diff > 1200: #if so, save the session to the database
            db.update({'id':user}, {'$push':{'sessions':latest_sessions[user]}})
            latest_sessions[user] = blank_dict
        else:
            latest_sessions[user]['end'] = timestamp #otherwise just replace this endtime

    count += 1
    if count % 100000 == 0: #output some nice stats every 100,000 lines
        print str(count) + '/' + str(howManyLinesTotal)

#now put the remaining last sessions in
for user in latest_sessions:
    db.update({'id':user}, {'$push':{'sessions':latest_sessions[user]}})

I'm getting about 0.002 seconds per line = 44 hours for an 80 million page view file.

This is with a 2TB 7200rpm seagate HDD, 32 GB of RAM and 3.4Ghz dualcore i3 processor.

Does this time sound reasonable or am I making some horrendous mistakes?


EDIT: We're looking at about 90,000+ users, i.e. keys in the defaultdict


EDIT2: Here's the cProfile output on a much smaller 106mb file. I commented out the actual mongoDB saves for testing purposes: http://pastebin.com/4XGtvYWD


EDIT3: Here's a barchart analysis of the cProfile: http://i.imgur.com/K6pu6xx.png

4

2 回答 2

4

我不能告诉你瓶颈在哪里,但我可以告诉你如何找到它。Python内置了分析工具,它会告诉你代码的每个部分花费了多少时间。将此工具用于脚本,就像运行一样简单:

python -m cProfile my_db_import_script.py

my_db_import_script.py您的实际脚本的名称在哪里。此命令将执行的操作是运行附加了探查器的脚本。脚本完成后,它会打印出每个函数被调用了多少次,在它们里面总共花费了多少时间,以及累计,以及其他一些统计数据。

要将它与您的脚本一起使用,您需要处理可以在合理时间内完成的数据子集。从那里您将能够分析您发现的任何瓶颈。

优化代码的关键是永远不要假设您知道问题出在哪里。先测量,再测量。

编辑:

在浏览了您的个人资料结果后,这些是我印象深刻的几行:

ncalls  tottime  percall  cumtime  percall filename:lineno(function)

        1    0.000    0.000    0.000    0.000 gzip.py:149(_init_read)
        1    0.000    0.000    0.000    0.000 gzip.py:153(_read_gzip_header)
  2709407    5.543    0.000    8.898    0.000 gzip.py:200(read)
        2    0.000    0.000    0.000    0.000 gzip.py:23(read32)
  2242878    3.267    0.000    3.727    0.000 gzip.py:232(_unread)
   107984    0.266    0.000    3.310    0.000 gzip.py:237(_read)
        1    0.000    0.000    0.000    0.000 gzip.py:26(open)
   107979    0.322    0.000    1.258    0.000 gzip.py:287(_add_read_data)
        1    0.000    0.000    0.000    0.000 gzip.py:293(_read_eof)
        1    0.000    0.000    0.000    0.000 gzip.py:308(close)
        1    0.000    0.000    0.000    0.000 gzip.py:35(GzipFile)
  2242878    8.029    0.000   23.517    0.000 gzip.py:385(readline)
        1    0.000    0.000    0.000    0.000 gzip.py:4(<module>)
        1    0.000    0.000    0.000    0.000 gzip.py:434(__iter__)
  2242878    1.561    0.000   25.078    0.000 gzip.py:437(next)
        1    0.000    0.000    0.000    0.000 gzip.py:44(__init__)

  2242878    2.889    0.000    2.889    0.000 {built-in method utcfromtimestamp}
   107979    1.627    0.000    1.627    0.000 {built-in method decompress}
  2709408    1.451    0.000    1.451    0.000 {method 'find' of 'str' objects}
  2242880    1.849    0.000    1.849    0.000 {method 'split' of 'str' objects}

您会注意到我已经突出显示了所有 gzip 代码。我对 gzip 模块不是很熟悉,所以我一直在查看源代码。看起来这个模块正在做的是将普通文件(如接口)暴露给 gzip 数据。有几种方法可以加快速度。

  1. 如果可行,您可以事先解压缩文件。这将摆脱 gzip 的一些开销。

  2. 您可以开始优化文件的读取方式。这是一个示例的链接,该示例说明根据您一次读取的文件量,情况会如何不同。在这个stackoverflow问题中也有一些很好的建议。

我还强调了转换时间戳会占用大量时间的事实,字符串操作函数也是如此。

归根结底,在这种规模上进行任何优化的最佳方法是运行基准测试,进行更改并重新运行。希望这是有启发性的!

于 2013-02-06T17:27:42.540 回答
2

如果我正确理解 cProfile 输出,则瓶颈是 gzip 流阅读器。

cumtime(“在函数上花费的时间,包括对其他函数的调用”)显示大约一半的运行时间(45.390 中的 25.078)花费在gzip.py:437(next). 大部分时间都花在gzip.py:385(readline).

不过,看起来磁盘 I/O 并不是瓶颈。更像是拆包逻辑本身。在使用常规 gzip 将其提供给程序之前尝试解压缩文件。注意 gzip 可以解压到标准输出;您的程序可以从标准输入读取它。

另一个消耗过多时间的函数是 utcfromtimestamp。如果可能,请尝试修改其逻辑。

试试这个:gunzip gigantic_file.gz - | head -n 100000 > small_unpacked,然后small_unpacked输入你的脚本,将它作为一个常规文件打开。再次配置文件。

于 2013-02-06T19:39:19.977 回答