1

そのため、ログ ファイルから約 8,000 万ページ ビューをインポートしようとしています。それらをセッションとしてデータベースに入れようとしています。つまり、20分間隔で区切られたページビューのグループです。

最終的に、私のユーザーデータベースでは、各ユーザーに次のような辞書オブジェクトのリストを持たせたいと思います:

{
    '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
                    },
                ]
}

かなり単純なはずです。だから私はこのスクリプトを書きました:

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]}})

1行あたり約0.002秒= 8000万ページビューのファイルで44時間です。

これには、2TB 7200rpm seagate HDD、32 GB の RAM、および 3.4Ghz デュアルコア i3 プロセッサが搭載されています。

今回は合理的に聞こえるでしょうか、それとも私は恐ろしい間違いを犯していますか?


編集: 約 90,000 人以上のユーザー、つまり defaultdict のキーを調べています。


EDIT2: これは、はるかに小さい 106 MB ファイルの cProfile 出力です。テスト目的で、実際の mongoDB の保存をコメントアウトしました: http://pastebin.com/4XGtvYWD


EDIT3: これは 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. ファイルの読み取り方法の最適化を開始できます。一度に読み込むファイルの量に基づいて状況がどのように異なるかの例へのリンクを次に示します。このスタックオーバーフローの質問には、いくつかの良い提案もあります。

また、文字列操作関数と同様に、タイムスタンプの変換にかなりの時間がかかるという事実も強調しました。

結局のところ、この規模で最適化を行う最善の方法は、ベンチマークを実行し、変更を加えて、再実行することです。これが有益だったことを願っています!

于 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 は stdout に解凍できることに注意してください。あなたのプログラムは標準入力からそれを読むことができます。

異常な時間を消費する別の関数は、utcfromtimestamp です。可能であれば、そのロジックを作り直してみてください。

これを試してください: gunzip gigantic_file.gz - | head -n 100000 > small_unpacked、次にスクリプトにフィードsmall_unpackedし、通常のファイルとして開きます。プロフィールをもう一度。

于 2013-02-06T19:39:19.977 に答える