2011-06-23 7 views
2

私は少し読み込んで、解析して、面白い(実際にはない)統計をApacheログファイルから取得します。今までは、ログファイルのすべての要求で送信された合計バイト数と最も一般的なIPアドレスの上位10個の2つの簡単なオプションを作成しました。マップとバイトストリングキーを使用した折り畳みのパフォーマンス解析

最初の「モード」は、すべての解析されたバイトの単純な合計です。 2番目のマップはマップ(Data.Map)の倍数で、insertWith (+) 1'を使用して出現を数えます。

最初のものは予想どおりに動作し、ほとんどの時間は解析に費やされ、一定のスペースで実行されます。使用中のGC 113712バイト最大レジデンシー(1553サンプル(複数可)) 145872バイト最大スロープ 2メガバイト総メモリ(によるフラグメンテーションのために失わ0メガバイト中にコピー に割り当て

42359709344バイトヒープ 72405840バイト)

発生0:76311のコレクション、
0平行、0.89s、0.99sが
1世代経過:1553点のコレクション、0 平行、0.21s、0.22sは

経過しました

INIT時間0.00s(0.00s 経過)MUT時間21.76s( 24.82s経過)GC時間1.10s(1.20s経過)EXIT時間
0.00s(0.00s経過)合計時間22.87s(26.02s )経過

% 4.8%(4.6%経過GC時間)

のAllocレート1946258962バイト秒MUTあたり

生産総ユーザの95.2%で、全体の 83.6パーセントが

経過

ただし、2番目のものはありません! に割り当て

49398834152バイトヒープ使用におけるGC 718385088バイト最大レジデンシー(15試料(S)) 134532128バイト最大スロープ 1393メガバイト総メモリによるフラグメンテーションに失われた(172メガバイト中にコピー 580579208バイト)

発生0:91275輪のコレクション、
0平行、252.65s、254.46s経過
ジェネレーション1:15点のコレクション、0がパラレル、0.12sは、0.12s経過

INIT時間0.00s(0.00s 経過)MUT時間41.11s( 48.87s経過)GC時間252.77s(254.58s経過)EXIT時間
0.00s(0.01秒経過)時間の合計293.88s(303.45 S経過)

%のGC時間経過86.0パーセント(83.9パーセント)

のAllocレート 生産14

秒MUTあたり1201635385バイト。合計ユーザーの0% 合計の13.5%が

ここに記載されています。

{-# LANGUAGE OverloadedStrings #-} 

module Main where 

import qualified Data.Attoparsec.Lazy as AL 
import Data.Attoparsec.Char8 hiding (space, take) 
import qualified Data.ByteString.Char8 as S 
import qualified Data.ByteString.Lazy.Char8 as L 
import Control.Monad (liftM) 
import System.Environment (getArgs) 
import Prelude hiding (takeWhile) 
import qualified Data.Map as M 
import Data.List (foldl', sortBy) 
import Text.Printf (printf) 
import Data.Maybe (fromMaybe) 

type Command = String 

data LogLine = LogLine { 
    getIP  :: S.ByteString, 
    getIdent :: S.ByteString, 
    getUser :: S.ByteString, 
    getDate :: S.ByteString, 
    getReq :: S.ByteString, 
    getStatus :: S.ByteString, 
    getBytes :: S.ByteString, 
    getPath :: S.ByteString, 
    getUA  :: S.ByteString 
} deriving (Ord, Show, Eq) 

quote, lbrack, rbrack, space :: Parser Char 
quote = satisfy (== '\"') 
lbrack = satisfy (== '[') 
rbrack = satisfy (== ']') 
space = satisfy (== ' ') 

quotedVal :: Parser S.ByteString 
quotedVal = do 
    quote 
    res <- takeTill (== '\"') 
    quote 
    return res 

bracketedVal :: Parser S.ByteString 
bracketedVal = do 
    lbrack 
    res <- takeTill (== ']') 
    rbrack 
    return res 

val :: Parser S.ByteString 
val = takeTill (== ' ') 

line :: Parser LogLine 
l ine = do 
    ip <- val 
    space 
    identity <- val 
    space 
    user <- val 
    space 
    date <- bracketedVal 
    space 
    req <- quotedVal 
    space 
    status <- val 
    space 
    bytes <- val 
    (path,ua) <- option ("","") combined 
    return $ LogLine ip identity user date req status bytes path ua 

combined :: Parser (S.ByteString,S.ByteString) 
combined = do 
    space 
    path <- quotedVal 
    space 
    ua <- quotedVal 
    return (path,ua) 

countBytes :: [L.ByteString] -> Int 
countBytes = foldl' count 0 
    where 
     count acc l = case AL.maybeResult $ AL.parse line l of 
      Just x -> (acc +) . maybe 0 fst . S.readInt . getBytes $ x 
      Nothing -> acc 

countIPs :: [L.ByteString] -> M.Map S.ByteString Int 
countIPs = foldl' count M.empty 
    where 
     count acc l = case AL.maybeResult $ AL.parse line l of 
      Just x -> M.insertWith' (+) (getIP x) 1 acc 
      Nothing -> acc 

--------------------------------------------------------------------------------- 

main :: IO() 
main = do 
    [cmd,path] <- getArgs 
    dispatch cmd path 

pretty :: Show a => Int -> (a, Int) -> String 
pretty i (bs, n) = printf "%d: %s, %d" i (show bs) n 

dispatch :: Command -> FilePath -> IO() 
dispatch cmd path = action path 
    where 
     action = fromMaybe err (lookup cmd actions) 
     err = printf "Error: %s is not a valid command." cmd 

actions :: [(Command, FilePath -> IO())] 
actions = [("bytes", countTotalBytes) 
      ,("ips", topListIP)] 

countTotalBytes :: FilePath -> IO() 
countTotalBytes path = print . countBytes . L.lines =<< L.readFile path 

topListIP :: FilePath -> IO() 
topListIP path = do 
    f <- liftM L.lines $ L.readFile path 
    let mostPopular (_,a) (_,b) = compare b a 
     m = countIPs f 
    mapM_ putStrLn . zipWith pretty [1..] . take 10 . sortBy mostPopular . M.toList $ m 

編集:+ RTS -A16M追加

は20%までGCを減少させました。もちろん、メモリの使用は変わりません。

+1

解決策ではありませんが、蓄積マップ上の「foldl」は無駄です。通常の 'foldl'を使用してください。 –

+0

@John L、あなたは大丈夫です。この場合、foldlとfoldlの間のスピード、GCまたはメモリの使用に違いは見られません。 –

答えて

3

私は、コードを次のように変更することをお勧め:

@@ -1,4 +1,4 @@ 
-{-# LANGUAGE OverloadedStrings #-} 
+{-# LANGUAGE BangPatterns, OverloadedStrings #-} 

module Main where 

@@ -9,7 +9,7 @@ 
import Control.Monad (liftM) 
import System.Environment (getArgs) 
import Prelude hiding (takeWhile) 
-import qualified Data.Map as M 
+import qualified Data.HashMap.Strict as M 
import Data.List (foldl', sortBy) 
import Text.Printf (printf) 
import Data.Maybe (fromMaybe) 
@@ -17,15 +17,15 @@ 
type Command = String 

data LogLine = LogLine { 
- getIP  :: S.ByteString, 
- getIdent :: S.ByteString, 
- getUser :: S.ByteString, 
- getDate :: S.ByteString, 
- getReq :: S.ByteString, 
- getStatus :: S.ByteString, 
- getBytes :: S.ByteString, 
- getPath :: S.ByteString, 
- getUA  :: S.ByteString 
+ getIP  :: !S.ByteString, 
+ getIdent :: !S.ByteString, 
+ getUser :: !S.ByteString, 
+ getDate :: !S.ByteString, 
+ getReq :: !S.ByteString, 
+ getStatus :: !S.ByteString, 
+ getBytes :: !S.ByteString, 
+ getPath :: !S.ByteString, 
+ getUA  :: !S.ByteString 
} deriving (Ord, Show, Eq) 

quote, lbrack, rbrack, space :: Parser Char 
@@ -39,14 +39,14 @@ 
    quote 
    res <- takeTill (== '\"') 
    quote 
- return res 
+ return $! res 

bracketedVal :: Parser S.ByteString 
bracketedVal = do 
    lbrack 
    res <- takeTill (== ']') 
    rbrack 
- return res 
+ return $! res 

val :: Parser S.ByteString 
val = takeTill (== ' ') 
@@ -67,14 +67,14 @@ 
    space 
    bytes <- val 
    (path,ua) <- option ("","") combined 
- return $ LogLine ip identity user date req status bytes path ua 
+ return $! LogLine ip identity user date req status bytes path ua 

combined :: Parser (S.ByteString,S.ByteString) 
combined = do 
    space 
- path <- quotedVal 
+ !path <- quotedVal 
    space 
- ua <- quotedVal 
+ !ua <- quotedVal 
    return (path,ua) 

countBytes :: [L.ByteString] -> Int 
@@ -84,11 +84,11 @@ 
      Just x -> (acc +) . maybe 0 fst . S.readInt . getBytes $ x 
      Nothing -> acc 

-countIPs :: [L.ByteString] -> M.Map S.ByteString Int 
+countIPs :: [L.ByteString] -> M.HashMap S.ByteString Int 
countIPs = foldl' count M.empty 
    where 
     count acc l = case AL.maybeResult $ AL.parse line l of 
-   Just x -> M.insertWith' (+) (getIP x) 1 acc 
+   Just x -> M.insertWith (+) (getIP x) 1 acc 
      Nothing -> acc 

--------------------------------------------------------------------------------- 

を私は構文解析に関連する表現を参照するサンクを含む、それらを避けるためにLogLine厳しいのフィールドを作りました。あなたが本当に怠け者にならない限り、フィールドを厳密にするのは良い習慣です。

私は、LogLineの個々のフィールドを実際に検査するまで解析の遅延を避けるために、できるだけ早く(変更の部分は$!です)作成するようにしました。

最後に、unordered-containers packageのより良いデータ構造HashMapに切り替えました。 Data.HashMap.Strictのすべての関数がvalue strictであることに注意してください。これは、平文insertWithを使用できることを意味します。

サブストリングをByteStringとすると、元のストリングを強制的にメモリに保存することになります(これは、JavaのStringの場合と同じです)。余分なメモリが確保されないようにするには、bytestringパッケージのcopy機能を使用してください。 (getIP x)の結果でcopyに電話をかけて、それが何か違いがあるかどうかを調べることができます。ここでのトレードオフは、スペースを節約するために文字列をコピーするために余分な計算を使用することです。

-A<high number>を使用すると、実行中のプログラム(つまりベンチマーク)のパフォーマンスが向上する傾向にありますが、実際のプログラムでは必ずしもそうではありません。同じことが-Hになります。少なくとも、より高い-Hの値(たとえば1G)は、プログラムのパフォーマンスに悪影響を与えません。

+0

-A16Mのスクリプトは現在85%の生産性で、300MBのメモリ使用量でピークに達しています(前の1.3GBと比較して)。私はそれをテストしましたが、この500万行のファイルを解析するのに40秒から50秒に減速しました。あなたの他の提案もテストしよう! –

+0

'-H'(512M-1G程度)を試し、' -A'をL2キャッシュのサイズに合わせることをお勧めします。 – tibbe

+0

-A2M -H1Gは〜53s(2MBのL2キャッシュを持つ)を与えます。奇妙なことに、-A16Mは40秒、-A50Mは36秒です。また、厳格なHashMapに切り替えることでパフォーマンスが向上しました。また、Gregory Collinのハッシュテーブルに再度切り替えたときに、別のブーストが追加されました。 –

0

最も明白な点は、最初のスクリプトではデータが見えるとすぐにデータを捨てることができますが、2番目のスクリプトは見たすべてのものを保持する必要があることです。したがって、第2のスクリプトは少なくともO(N)のメモリを使いますが、第1のスクリプトは一定の空間で実行できます。

ヒーププロファイリングをオンにしてみましたか?余分な割り当てがあなたのコードで起こる可能性があるところでいくつかの刺し傷を作ることができますが、ハードデータの代わりにはありません。

Data.Map.insertWith 'の呼び出しは疑いが持たれています。それぞれが既存のマップ余剰の一部を要件にレンダリングし、コピーと再調整を必要としますが、それは私にとっては当然の推測です。 insertWith '呼び出しが非難される場合は、インタースティシャルマップエントリが不要なため、は、マップ全体を1回のパスで(IPをカウントするためのインクリメントなしで)作成してから、カウント。そうすれば、マップを再調整する時間を無駄にすることはありません。また、キーデータ型がInt(少なくともIPv4アドレスであればそうです)に適合し、代わりにData.IntMapを使用することができます。これはメモリオーバーヘッドが非常に低くなります。

+0

正確ではありません!それは行を解析し、IPアドレスの小さな部分しか保持しません。解析された元のファイルのサイズは約1.3GBなので、ほとんどのファイルはメモリにロックされます!しかし、私は同意する、insertWithは最も可能性の高いperpです。 –

+0

ハスケルの初心者からのこの投稿はhttp://web.archiveorange.com/archive/v/aHum2LrqyulnPSegisUWも同様の質問に答えます。残念ながら私はあなたの要件が異なるので、そこに提案された解決策があなたの問題に役立つとは思わない! –

+0

私は同じ間違いをしますが、引用:foldとmapの挿入は "悪い考え"です!割り当て領域を増やしてみます。 –

関連する問題